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']); }); });