docs: enterprise refactor plan with ralph specs
This commit is contained in:
187
tests/server/aiProviders.test.ts
Normal file
187
tests/server/aiProviders.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user