Files
Autonomous-Bug-Explorer/tests/server/aiProviders.test.ts

188 lines
6.4 KiB
TypeScript

/**
* 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: '<body>Error</body>',
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();
});
});