397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
5.9 KiB
JavaScript
163 lines
5.9 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
import { tmpdir } from 'os';
|
|
import { join } from 'path';
|
|
import { readRouterState } from './observer-state-enricher.mjs';
|
|
|
|
describe('readRouterState', () => {
|
|
let baseDir;
|
|
|
|
beforeEach(() => {
|
|
baseDir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(baseDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('returns null when state file does not exist', () => {
|
|
expect(readRouterState('abc-123', { baseDir })).toBeNull();
|
|
});
|
|
|
|
it('reads state file when present', () => {
|
|
const state = {
|
|
sessionId: 'abc-123',
|
|
classification: { recommendedNode: '#62', recommendedChain: '#13' },
|
|
chainProgress: ['brainstorming'],
|
|
chainCompleted: false,
|
|
};
|
|
writeFileSync(join(baseDir, 'router-state-abc-123.json'), JSON.stringify(state));
|
|
const result = readRouterState('abc-123', { baseDir });
|
|
expect(result).toEqual(state);
|
|
});
|
|
|
|
it('returns null on malformed JSON', () => {
|
|
writeFileSync(join(baseDir, 'router-state-broken.json'), 'not-json');
|
|
expect(readRouterState('broken', { baseDir })).toBeNull();
|
|
});
|
|
|
|
it('returns null on missing sessionId', () => {
|
|
expect(readRouterState(null, { baseDir })).toBeNull();
|
|
expect(readRouterState('', { baseDir })).toBeNull();
|
|
});
|
|
|
|
it('uses ~/.claude/runtime/ as default baseDir', () => {
|
|
// Smoke-check: default baseDir resolution doesn't throw.
|
|
// Real-file reading covered above with explicit baseDir.
|
|
const result = readRouterState('non-existent-session-xyz');
|
|
// Either null (file doesn't exist there) or object — both fine.
|
|
expect(result === null || typeof result === 'object').toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('extractRouterFields', () => {
|
|
it('extracts the four fields from state, defaulting to null/empty', async () => {
|
|
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
|
const state = {
|
|
classification: { recommendedNode: '#62', recommendedChain: '#13' },
|
|
chainProgress: ['brainstorming', 'writing-plans'],
|
|
chainCompleted: false,
|
|
};
|
|
expect(extractRouterFields(state)).toEqual({
|
|
recommended_node: '#62',
|
|
recommended_chain: '#13',
|
|
chain_progress: ['brainstorming', 'writing-plans'],
|
|
chain_completed: false,
|
|
});
|
|
});
|
|
|
|
it('returns nulls/empty when state is null', async () => {
|
|
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
|
expect(extractRouterFields(null)).toEqual({
|
|
recommended_node: null,
|
|
recommended_chain: null,
|
|
chain_progress: [],
|
|
chain_completed: false,
|
|
});
|
|
});
|
|
|
|
it('handles missing classification block', async () => {
|
|
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
|
expect(extractRouterFields({ chainProgress: ['x'], chainCompleted: true })).toEqual({
|
|
recommended_node: null,
|
|
recommended_chain: null,
|
|
chain_progress: ['x'],
|
|
chain_completed: true,
|
|
});
|
|
});
|
|
|
|
it('treats empty string recommendedNode/recommendedChain as null', async () => {
|
|
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
|
expect(extractRouterFields({ classification: { recommendedNode: '', recommendedChain: '' } })).toEqual({
|
|
recommended_node: null,
|
|
recommended_chain: null,
|
|
chain_progress: [],
|
|
chain_completed: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('extractClassifierOutput — Pass 2 metrics (project-brain-factor-analysis-4passes)', () => {
|
|
it('surfaces latency_ms / retry_count_internal / llm_error / alternatives_considered when present', async () => {
|
|
const { extractClassifierOutput } = await import('./observer-state-enricher.mjs');
|
|
const state = {
|
|
classification: {
|
|
task_type: 'feature',
|
|
source: 'llm',
|
|
latency_ms: 742,
|
|
retry_count_internal: 0,
|
|
llm_error_type: null,
|
|
alternatives_considered: [
|
|
{ node: '#19', score: 0.8, reason: 'close match' },
|
|
{ node: '#62', score: 0.4, reason: 'mismatch domain' },
|
|
],
|
|
},
|
|
};
|
|
const out = extractClassifierOutput(state);
|
|
expect(out.latency_ms).toBe(742);
|
|
expect(out.retry_count_internal).toBe(0);
|
|
expect(out.llm_error).toBeNull();
|
|
expect(Array.isArray(out.alternatives_considered)).toBe(true);
|
|
expect(out.alternatives_considered).toHaveLength(2);
|
|
});
|
|
|
|
it('truncates alternatives_considered to top-3 to bound JSONL line size', async () => {
|
|
const { extractClassifierOutput } = await import('./observer-state-enricher.mjs');
|
|
const out = extractClassifierOutput({
|
|
classification: {
|
|
task_type: 'feature',
|
|
source: 'llm',
|
|
alternatives_considered: [
|
|
{ node: '#1' }, { node: '#2' }, { node: '#3' }, { node: '#4' }, { node: '#5' },
|
|
],
|
|
},
|
|
});
|
|
expect(out.alternatives_considered).toHaveLength(3);
|
|
expect(out.alternatives_considered[0].node).toBe('#1');
|
|
});
|
|
|
|
it('returns null fields on regex / prefilter / cache paths (no LLM hit)', async () => {
|
|
const { extractClassifierOutput } = await import('./observer-state-enricher.mjs');
|
|
const out = extractClassifierOutput({
|
|
classification: { task_type: 'conversation', source: 'prefilter' },
|
|
});
|
|
expect(out.latency_ms).toBeNull();
|
|
expect(out.retry_count_internal).toBeNull();
|
|
expect(out.llm_error).toBeNull();
|
|
expect(out.alternatives_considered).toBeNull();
|
|
});
|
|
|
|
it('captures llm_error category on degraded LLM path', async () => {
|
|
const { extractClassifierOutput } = await import('./observer-state-enricher.mjs');
|
|
const out = extractClassifierOutput({
|
|
classification: {
|
|
task_type: 'feature', source: 'regex',
|
|
llm_error_type: 'timeout', latency_ms: 30000, retry_count_internal: 4,
|
|
},
|
|
});
|
|
expect(out.llm_error).toBe('timeout');
|
|
expect(out.latency_ms).toBe(30000);
|
|
expect(out.retry_count_internal).toBe(4);
|
|
});
|
|
});
|