import { describe, it, expect } from 'vitest'; import { buildStateFromClassification } from './router-prehook.mjs'; describe('buildStateFromClassification — Phase 2 Task 14', () => { it('builds full state object (v4 shape: task_id + task_cost, no enforcementRequired)', () => { const cls = { task_type: 'feature', recommended_node: '#19', source: 'llm' }; const s = buildStateFromClassification(cls, { sessionId: 'abc', promptHash: '12345' }); expect(s.sessionId).toBe('abc'); expect(s.promptHash).toBe('12345'); expect(s.classification).toEqual(cls); expect(s.skillInvokedThisTurn).toBe(false); expect(s.chainProgress).toEqual([]); expect(s.timestamp).toBeDefined(); expect(typeof s.task_id).toBe('string'); expect(s.task_cost).toEqual({}); expect(s.enforcementRequired).toBeUndefined(); }); it('emits a fresh task_id per call (random)', () => { const cls = { task_type: 'feature' }; const a = buildStateFromClassification(cls, { sessionId: 's', promptHash: 'h' }); const b = buildStateFromClassification(cls, { sessionId: 's', promptHash: 'h' }); expect(a.task_id).not.toBe(b.task_id); }); it('honors externally supplied taskId (caller wants determinism)', () => { const s = buildStateFromClassification( { task_type: 'feature' }, { sessionId: 's', promptHash: 'h', taskId: 'pinned-1' }, ); expect(s.task_id).toBe('pinned-1'); }); // brain-retro #7 C2 (2026-05-27): prompt_length lets router-tool-gate // detect short ambiguous prompts where AskUserQuestion would beat improvising. it('persists prompt_length from options into state (C2 — brain-retro #7)', () => { const s = buildStateFromClassification( { task_type: 'feature' }, { sessionId: 's', promptHash: 'h', promptLength: 17 }, ); expect(s.prompt_length).toBe(17); }); it('defaults prompt_length to null when caller omits it (back-compat)', () => { const s = buildStateFromClassification( { task_type: 'feature' }, { sessionId: 's', promptHash: 'h' }, ); expect(s.prompt_length).toBeNull(); }); it('writes inheritance block on continuation (B5)', () => { const s = buildStateFromClassification( { task_type: 'feature', source: 'prefilter_inherited' }, { sessionId: 's', promptHash: 'h', inheritedFrom: 'prev', ageMin: 5 }, ); expect(s.inheritance.inherited_from_task_id).toBe('prev'); expect(s.inheritance.inheritance_age_minutes).toBe(5); }); it('omits inheritance block when not a continuation', () => { const s = buildStateFromClassification( { task_type: 'feature', source: 'llm' }, { sessionId: 's', promptHash: 'h' }, ); expect(s.inheritance).toBeUndefined(); }); it('threads cost block through when caller provides it', () => { const s = buildStateFromClassification( { task_type: 'feature' }, { sessionId: 's', promptHash: 'h', cost: { classifier_input_tokens: 1234, classifier_output_tokens: 200 } }, ); expect(s.task_cost.classifier_input_tokens).toBe(1234); expect(s.task_cost.classifier_output_tokens).toBe(200); }); }); describe('buildCostFromClassifierUsage — A1 cost tracking (2026-05-26)', () => { it('builds classifier cost block from Anthropic API usage shape', async () => { const { buildCostFromClassifierUsage } = await import('./router-prehook.mjs'); const usage = { input_tokens: 5000, output_tokens: 120 }; const cost = buildCostFromClassifierUsage(usage); expect(cost.classifier_input_tokens).toBe(5000); expect(cost.classifier_output_tokens).toBe(120); }); it('honors cache_read / cache_creation tokens (Anthropic prompt caching)', async () => { const { buildCostFromClassifierUsage } = await import('./router-prehook.mjs'); const usage = { input_tokens: 100, output_tokens: 50, cache_read_input_tokens: 4500, cache_creation_input_tokens: 500, }; const cost = buildCostFromClassifierUsage(usage); expect(cost.classifier_input_tokens).toBe(100); expect(cost.classifier_output_tokens).toBe(50); expect(cost.classifier_cache_read_input_tokens).toBe(4500); expect(cost.classifier_cache_creation_input_tokens).toBe(500); }); it('returns empty object on null/undefined usage', async () => { const { buildCostFromClassifierUsage } = await import('./router-prehook.mjs'); expect(buildCostFromClassifierUsage(null)).toEqual({}); expect(buildCostFromClassifierUsage(undefined)).toEqual({}); expect(buildCostFromClassifierUsage({})).toEqual({}); }); }); describe('ENFORCEMENT_TYPES legacy export removed (D1 closure)', () => { it('does not export ENFORCEMENT_TYPES', async () => { const mod = await import('./router-prehook.mjs'); expect(mod.ENFORCEMENT_TYPES).toBeUndefined(); }); it('does not export isEnforcementRequired', async () => { const mod = await import('./router-prehook.mjs'); expect(mod.isEnforcementRequired).toBeUndefined(); }); }); describe('UTF-8 cyrillic stdin (regression — Stage 3 fix 1)', () => { it('module loads with UTF-8 helper wired (smoke)', async () => { const mod = await import('./router-prehook.mjs'); expect(typeof mod.buildStateFromClassification).toBe('function'); }); });