Files
tilbudgivern/backend/__tests__/aiValidationJobService.test.js

87 lines
3.4 KiB
JavaScript

const { AiValidationJobService } = require('../src/services/aiValidationJobService');
const waitFor = async (predicate, timeoutMs = 1000) => {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const result = predicate();
if (result) return result;
await new Promise(resolve => setTimeout(resolve, 5));
}
throw new Error('Timed out waiting for validation job');
};
describe('AiValidationJobService', () => {
test('returns immediately and exposes the completed result through polling', async () => {
const service = new AiValidationJobService();
const job = await service.createJob('test', async () => ({ status: 'passed' }));
expect(job).toMatchObject({ type: 'test', status: 'queued' });
const completed = await waitFor(() => {
const current = service.getJob(job.id, 'test');
return current?.status === 'completed' ? current : null;
});
expect(completed.result).toEqual({ status: 'passed' });
});
test('runs Codex jobs one at a time and reports failures', async () => {
const service = new AiValidationJobService();
let releaseFirst;
const first = await service.createJob('first', () => new Promise(resolve => {
releaseFirst = resolve;
}));
const second = await service.createJob('second', async () => {
throw new Error('Codex fejlede');
});
await waitFor(() => service.getJob(first.id)?.status === 'running');
expect(service.getJob(second.id)).toMatchObject({ status: 'queued', queuePosition: 1 });
releaseFirst({ ok: true });
const failed = await waitFor(() => {
const current = service.getJob(second.id);
return current?.status === 'failed' ? current : null;
});
expect(failed.error).toBe('Codex fejlede');
});
test('aggregates subscription usage and includes every AI feature', async () => {
const service = new AiValidationJobService();
service.db = {
pool: {
execute: jest.fn().mockResolvedValue([[
{
feature_type: 'risk_review', status: 'completed', model: 'gpt-5.6-terra',
reasoning_effort: 'medium', usage_payload: JSON.stringify({
inputTokens: 100, cachedInputTokens: 40, outputTokens: 20
}),
created_at: '2026-07-22T10:00:00.000Z', started_at: '2026-07-22T10:00:00.000Z',
completed_at: '2026-07-22T10:00:10.000Z'
},
{
feature_type: 'project-flow', status: 'failed', model: 'gpt-5.6-terra',
reasoning_effort: 'medium', usage_payload: null,
created_at: '2026-07-22T09:00:00.000Z', started_at: '2026-07-22T09:00:00.000Z',
completed_at: '2026-07-22T09:00:03.000Z'
}
]])
}
};
const usage = await service.getSubscriptionUsage(30);
expect(usage.features).toHaveLength(11);
expect(usage.summary).toMatchObject({
runs: 2, completedRuns: 1, failedRuns: 1, inputTokens: 100,
cachedInputTokens: 40, outputTokens: 20, totalTokens: 120
});
expect(usage.features.find(feature => feature.key === 'risk_review')).toMatchObject({
runs: 1, totalTokens: 120, averageDurationSeconds: 10
});
expect(usage.features.find(feature => feature.key === 'final_review')).toMatchObject({
runs: 1, failedRuns: 1
});
expect(usage.features.find(feature => feature.key === 'excel_review')).toMatchObject({ runs: 0 });
});
});