188 lines
6.4 KiB
TypeScript
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();
|
|
});
|
|
});
|