164 lines
4.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
DEFAULT_CONFIG,
admitScoringRun,
fetchScoringRun,
listScoringRuns,
pollUntilTerminal,
} from '../lib/api/client';
import type { CreateScoringRunRequest, ScoringRunResponse } from '../lib/api/types';
import { GOLDEN_FIXTURES } from '../lib/fixtures/golden';
describe('Dev Console API Client', () => {
it('sends only non-secret headers during run admission', async () => {
const mockPost = vi.fn().mockResolvedValue({
data: {
run_id: 'test-run-123',
tenant_id: 'omni-test-tenant-42',
subject_ref: 'ems-lead-1',
scoring_config_version: '1.0.0',
status: 'pending',
status_url: '/v1/scoring-runs/test-run-123',
created_at: '2026-08-28T12:00:00Z',
},
error: undefined,
response: { status: 202 },
});
const mockClient = {
POST: mockPost,
GET: vi.fn(),
} as any;
const payload: CreateScoringRunRequest = {
...GOLDEN_FIXTURES[0].payload,
rule_overrides: [
{
id: 'demo-rule-1',
name: 'Demo budget signal',
target: 'custom_field',
custom_field_key: 'budget',
operator: 'gte',
value: 5000000,
points: 15,
enabled: true,
},
],
};
const res = await admitScoringRun(
mockClient,
payload,
{
idempotencyKey: 'idem-123',
dotbApp: 'dev-console',
dotbFeature: 'fixture-test',
},
DEFAULT_CONFIG,
);
expect(res.status).toBe(202);
expect(res.data?.run_id).toBe('test-run-123');
expect(mockPost).toHaveBeenCalledWith('/v1/scoring-runs', {
params: {
header: {
'X-Tenant-VKey': '',
'Idempotency-Key': 'idem-123',
'X-DotB-App': 'dev-console',
'X-DotB-Feature': 'fixture-test',
},
},
body: payload,
});
});
it('passes pagination parameters and parses total count when listing scoring runs', async () => {
const mockHeaders = new Headers({
'x-total-count': '35',
});
const mockRuns = [{ run_id: 'run-1' }, { run_id: 'run-2' }] as ScoringRunResponse[];
const mockGet = vi.fn().mockResolvedValue({
data: mockRuns,
error: undefined,
response: { status: 200, headers: mockHeaders },
});
const mockClient = { GET: mockGet } as any;
const result = await listScoringRuns(mockClient, 'tenant-1', 31, 60);
expect(mockGet).toHaveBeenCalledWith('/v1/scoring-runs', {
params: {
header: { 'X-Tenant-Id': 'tenant-1' },
query: { limit: 31, offset: 60 },
},
});
expect(result.items).toEqual(mockRuns);
expect(result.total).toBe(35);
});
it('falls back to items length if X-Total-Count header is missing', async () => {
const mockRuns = [{ run_id: 'run-1' }] as ScoringRunResponse[];
const mockGet = vi.fn().mockResolvedValue({
data: mockRuns,
error: undefined,
response: { status: 200, headers: new Headers() },
});
const mockClient = { GET: mockGet } as any;
const result = await listScoringRuns(mockClient, 'tenant-1');
expect(result.items).toEqual(mockRuns);
expect(result.total).toBe(1);
});
it('polls until reaching terminal status', async () => {
let callCount = 0;
const mockGet = vi.fn().mockImplementation(() => {
callCount++;
const status = callCount === 1 ? 'pending' : callCount === 2 ? 'running' : 'completed';
return Promise.resolve({
data: {
run_id: 'poll-run-1',
tenant_id: 'tenant-1',
subject_ref: 'lead-1',
scoring_config_version: '1.0.0',
parent_run_id: null,
status,
rule_score: 80,
ai_score: 90,
final_score: 88,
effective_score: 88,
scoring_mode: 'medium_ai',
degraded: false,
ai_metadata: null,
confidence: 0.9,
confidence_threshold: 0.8,
effective_config: null,
error_code: null,
retryable: null,
version: 1,
started_at: '2026-08-28T12:00:00Z',
completed_at: '2026-08-28T12:00:05Z',
created_at: '2026-08-28T12:00:00Z',
updated_at: '2026-08-28T12:00:05Z',
} as ScoringRunResponse,
error: undefined,
response: { status: 200 },
});
});
const mockClient = {
GET: mockGet,
POST: vi.fn(),
} as any;
const ticks: string[] = [];
const finalRun = await pollUntilTerminal(mockClient, 'poll-run-1', {
intervalMs: 5,
onTick: (run) => ticks.push(run.status),
});
expect(finalRun.status).toBe('completed');
expect(callCount).toBe(3);
expect(ticks).toEqual(['pending', 'running', 'completed']);
});
});