164 lines
4.7 KiB
TypeScript
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']);
|
|
});
|
|
});
|