incident-tools.test.ts220 lines · main
| 1 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 2 | |
| 3 | import { getIncidentTools } from './incident-tools' |
| 4 | |
| 5 | // Mock IS_PLATFORM |
| 6 | vi.mock('common', () => ({ |
| 7 | IS_PLATFORM: true, |
| 8 | })) |
| 9 | |
| 10 | describe('ai/tools/incident-tools', () => { |
| 11 | let mockFetch: ReturnType<typeof vi.fn> |
| 12 | let mockAbortSignal: AbortSignal |
| 13 | |
| 14 | beforeEach(() => { |
| 15 | vi.clearAllMocks() |
| 16 | mockFetch = vi.fn() |
| 17 | global.fetch = mockFetch as typeof fetch |
| 18 | |
| 19 | // Mock AbortSignal.timeout |
| 20 | mockAbortSignal = new AbortController().signal |
| 21 | if (!AbortSignal.timeout) { |
| 22 | AbortSignal.timeout = vi.fn(() => mockAbortSignal) as any |
| 23 | } |
| 24 | }) |
| 25 | |
| 26 | describe('getIncidentTools', () => { |
| 27 | it('should return an object with get_active_incidents tool', () => { |
| 28 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 29 | |
| 30 | expect(tools).toBeDefined() |
| 31 | expect(tools.get_active_incidents).toBeDefined() |
| 32 | }) |
| 33 | |
| 34 | it('should have correct description for get_active_incidents', () => { |
| 35 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 36 | |
| 37 | expect(tools.get_active_incidents.description).toContain('Check for active incidents') |
| 38 | expect(tools.get_active_incidents.description).toContain('Briven service') |
| 39 | }) |
| 40 | |
| 41 | it('should have empty input schema', () => { |
| 42 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 43 | const schema = tools.get_active_incidents.inputSchema |
| 44 | |
| 45 | // The schema is a Zod object that accepts empty object |
| 46 | expect(schema).toBeDefined() |
| 47 | expect((schema as any)._def.typeName).toBe('ZodObject') |
| 48 | }) |
| 49 | |
| 50 | describe('execute function', () => { |
| 51 | it('should return empty incidents when not on platform', async () => { |
| 52 | const common = await import('common') |
| 53 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(false) |
| 54 | |
| 55 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 56 | const result = await (tools.get_active_incidents.execute as any)({}) |
| 57 | |
| 58 | expect(result).toEqual({ |
| 59 | incidents: [], |
| 60 | message: 'Incident checking is only available on Briven platform.', |
| 61 | }) |
| 62 | expect(mockFetch).not.toHaveBeenCalled() |
| 63 | }) |
| 64 | |
| 65 | it('should fetch incidents from correct URL', async () => { |
| 66 | const common = await import('common') |
| 67 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 68 | |
| 69 | mockFetch.mockResolvedValue({ |
| 70 | ok: true, |
| 71 | json: async () => [], |
| 72 | }) |
| 73 | |
| 74 | const tools = getIncidentTools({ baseUrl: 'https://example.com/dashboard' }) |
| 75 | if (!tools.get_active_incidents.execute) throw new Error('execute is undefined') |
| 76 | await tools.get_active_incidents.execute({}, { toolCallId: 'test', messages: [] }) |
| 77 | |
| 78 | expect(mockFetch).toHaveBeenCalledWith( |
| 79 | 'https://example.com/dashboard/api/incident-status', |
| 80 | { |
| 81 | signal: expect.any(AbortSignal), |
| 82 | } |
| 83 | ) |
| 84 | }) |
| 85 | |
| 86 | it('should return message when no incidents', async () => { |
| 87 | const common = await import('common') |
| 88 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 89 | |
| 90 | mockFetch.mockResolvedValue({ |
| 91 | ok: true, |
| 92 | json: async () => [], |
| 93 | }) |
| 94 | |
| 95 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 96 | const result = await (tools.get_active_incidents.execute as any)({}) |
| 97 | |
| 98 | expect(result).toEqual({ |
| 99 | incidents: [], |
| 100 | message: expect.stringContaining('No active incidents'), |
| 101 | }) |
| 102 | }) |
| 103 | |
| 104 | it('should return incident summaries when incidents exist', async () => { |
| 105 | const common = await import('common') |
| 106 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 107 | |
| 108 | const mockIncidents = [ |
| 109 | { |
| 110 | name: 'Database slowness', |
| 111 | status: 'investigating', |
| 112 | impact: 'minor', |
| 113 | active_since: '2024-01-01T10:00:00Z', |
| 114 | extra_field: 'should be filtered', |
| 115 | }, |
| 116 | ] |
| 117 | |
| 118 | mockFetch.mockResolvedValue({ |
| 119 | ok: true, |
| 120 | json: async () => mockIncidents, |
| 121 | }) |
| 122 | |
| 123 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 124 | const result = await (tools.get_active_incidents.execute as any)({}) |
| 125 | |
| 126 | expect((result as any).incidents).toEqual([ |
| 127 | { |
| 128 | name: 'Database slowness', |
| 129 | status: 'investigating', |
| 130 | impact: 'minor', |
| 131 | active_since: '2024-01-01T10:00:00Z', |
| 132 | }, |
| 133 | ]) |
| 134 | expect((result as any).message).toContain('1 active incident') |
| 135 | expect((result as any).message).toContain('status.supabase.com') |
| 136 | }) |
| 137 | |
| 138 | it('should handle multiple incidents', async () => { |
| 139 | const common = await import('common') |
| 140 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 141 | |
| 142 | const mockIncidents = [ |
| 143 | { |
| 144 | name: 'Database issue', |
| 145 | status: 'investigating', |
| 146 | impact: 'major', |
| 147 | active_since: '2024-01-01T10:00:00Z', |
| 148 | }, |
| 149 | { |
| 150 | name: 'Storage issue', |
| 151 | status: 'identified', |
| 152 | impact: 'minor', |
| 153 | active_since: '2024-01-01T11:00:00Z', |
| 154 | }, |
| 155 | ] |
| 156 | |
| 157 | mockFetch.mockResolvedValue({ |
| 158 | ok: true, |
| 159 | json: async () => mockIncidents, |
| 160 | }) |
| 161 | |
| 162 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 163 | const result = await (tools.get_active_incidents.execute as any)({}) |
| 164 | |
| 165 | expect((result as any).incidents).toHaveLength(2) |
| 166 | expect((result as any).message).toContain('2 active incidents') |
| 167 | }) |
| 168 | |
| 169 | it('should handle fetch errors', async () => { |
| 170 | const common = await import('common') |
| 171 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 172 | |
| 173 | mockFetch.mockRejectedValue(new Error('Network error')) |
| 174 | |
| 175 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 176 | const result = await (tools.get_active_incidents.execute as any)({}) |
| 177 | |
| 178 | expect(result).toEqual({ |
| 179 | incidents: [], |
| 180 | error: 'Unable to check incident status at this time.', |
| 181 | }) |
| 182 | }) |
| 183 | |
| 184 | it('should handle non-ok responses', async () => { |
| 185 | const common = await import('common') |
| 186 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 187 | |
| 188 | mockFetch.mockResolvedValue({ |
| 189 | ok: false, |
| 190 | status: 500, |
| 191 | }) |
| 192 | |
| 193 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 194 | const result = await (tools.get_active_incidents.execute as any)({}) |
| 195 | |
| 196 | expect(result).toEqual({ |
| 197 | incidents: [], |
| 198 | error: 'Unable to check incident status at this time.', |
| 199 | }) |
| 200 | }) |
| 201 | |
| 202 | it('should use timeout signal', async () => { |
| 203 | const common = await import('common') |
| 204 | vi.spyOn(common, 'IS_PLATFORM', 'get').mockReturnValue(true) |
| 205 | |
| 206 | mockFetch.mockResolvedValue({ |
| 207 | ok: true, |
| 208 | json: async () => [], |
| 209 | }) |
| 210 | |
| 211 | const tools = getIncidentTools({ baseUrl: 'https://supabase.com/dashboard' }) |
| 212 | if (!tools.get_active_incidents.execute) throw new Error('execute is undefined') |
| 213 | await tools.get_active_incidents.execute({}, { toolCallId: 'test', messages: [] }) |
| 214 | |
| 215 | const callArgs = mockFetch.mock.calls[0] |
| 216 | expect(callArgs[1].signal).toBeInstanceOf(AbortSignal) |
| 217 | }) |
| 218 | }) |
| 219 | }) |
| 220 | }) |