/** * Tests for AI providers with mocked HTTP calls. */ import { ClaudeProvider } from '../../src/server/enrichment/ClaudeProvider'; import { OpenAIProvider } from '../../src/server/enrichment/OpenAIProvider'; import { OllamaProvider } from '../../src/server/enrichment/OllamaProvider'; import type { IAnomaly, IEnrichmentContext } from '../../src/core/interfaces'; function makeAnomaly(): IAnomaly { return { id: 'anom_test', type: 'http_error', severity: 'high', observationId: 'obs_1', actionTrace: [], description: 'HTTP 500 error on /api/users', evidence: { rawErrors: ['GET /api/users → 500'] }, timestamp: Date.now(), }; } function makeContext(): IEnrichmentContext { return { domSnapshot: 'Error', httpLog: [{ url: '/api/users', status: 500, method: 'GET', durationMs: 120 }], consoleErrors: ['Uncaught TypeError: Cannot read properties of undefined'], actionTrace: [], pageTitle: 'Dashboard', url: 'http://localhost:3000/dashboard', }; } const VALID_ENRICHMENT_JSON = JSON.stringify({ rootCause: 'Null pointer in user data fetch', userImpact: 'Users cannot load the dashboard', suggestedFix: 'Add null check before accessing user.name', confidence: 'high', }); // ─── ClaudeProvider ─────────────────────────────────────────────────────────── describe('ClaudeProvider', () => { const origFetch = global.fetch; afterEach(() => { global.fetch = origFetch; }); it('enriches anomaly with valid response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ content: [{ type: 'text', text: VALID_ENRICHMENT_JSON }], }), }) as unknown as typeof fetch; const provider = new ClaudeProvider('test-api-key'); const result = await provider.enrich(makeAnomaly(), makeContext()); expect(result.rootCause).toBe('Null pointer in user data fetch'); expect(result.userImpact).toBe('Users cannot load the dashboard'); expect(result.confidence).toBe('high'); expect(result.provider).toBe('claude'); expect(result.generatedAt).toBeLessThanOrEqual(Date.now()); }); it('throws on non-OK API response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 401, text: () => Promise.resolve('Unauthorized'), }) as unknown as typeof fetch; const provider = new ClaudeProvider('bad-key'); await expect(provider.enrich(makeAnomaly(), makeContext())).rejects.toThrow('Anthropic API error: 401'); }); it('falls back gracefully on non-JSON response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ content: [{ type: 'text', text: 'Sorry, I cannot help with that.' }], }), }) as unknown as typeof fetch; const provider = new ClaudeProvider('test-key'); const result = await provider.enrich(makeAnomaly(), makeContext()); expect(result.rootCause).toBeTruthy(); expect(result.provider).toBe('claude'); expect(result.confidence).toBe('low'); }); }); // ─── OpenAIProvider ─────────────────────────────────────────────────────────── describe('OpenAIProvider', () => { const origFetch = global.fetch; afterEach(() => { global.fetch = origFetch; }); it('enriches anomaly with valid response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ choices: [{ message: { content: VALID_ENRICHMENT_JSON } }], }), }) as unknown as typeof fetch; const provider = new OpenAIProvider('test-openai-key'); const result = await provider.enrich(makeAnomaly(), makeContext()); expect(result.rootCause).toBe('Null pointer in user data fetch'); expect(result.confidence).toBe('high'); expect(result.provider).toBe('openai'); }); it('throws on non-OK API response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 429, text: () => Promise.resolve('Rate limit exceeded'), }) as unknown as typeof fetch; const provider = new OpenAIProvider('test-key'); await expect(provider.enrich(makeAnomaly(), makeContext())).rejects.toThrow('OpenAI API error: 429'); }); it('falls back on missing choices', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ choices: [] }), }) as unknown as typeof fetch; const provider = new OpenAIProvider('test-key'); const result = await provider.enrich(makeAnomaly(), makeContext()); expect(result.provider).toBe('openai'); }); }); // ─── OllamaProvider ─────────────────────────────────────────────────────────── describe('OllamaProvider', () => { const origFetch = global.fetch; afterEach(() => { global.fetch = origFetch; }); it('enriches anomaly with valid response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ response: VALID_ENRICHMENT_JSON }), }) as unknown as typeof fetch; const provider = new OllamaProvider('http://localhost:11434'); const result = await provider.enrich(makeAnomaly(), makeContext()); expect(result.rootCause).toBe('Null pointer in user data fetch'); expect(result.provider).toBe('ollama'); }); it('throws on non-OK Ollama response', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 503, }) as unknown as typeof fetch; const provider = new OllamaProvider(); await expect(provider.enrich(makeAnomaly(), makeContext())).rejects.toThrow('Ollama API error: 503'); }); it('falls back on non-JSON response text', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ response: 'No valid JSON here' }), }) as unknown as typeof fetch; const provider = new OllamaProvider(); const result = await provider.enrich(makeAnomaly(), makeContext()); expect(result.provider).toBe('ollama'); expect(result.rootCause).toBeTruthy(); }); });