Files
portal/tools/router-prehook.test.mjs
T
Дмитрий 81cbd8c1c2 feat(brain-retro #7): C1+C2+C3+C4 router-discipline fixes
retro #7 (docs/observer/notes/2026-05-27-brain-retro-7.md) surfaced 4
candidates against 23 turns since retro #6. All four implemented TDD.

C1 — translit slang vocabulary in router-classifier-regex-fallback.mjs.
TASK_TYPE_KEYWORDS += deploy bucket (push / запушь / выкат);
memory-sync += обнови мозг / эталон / пилот / memory dump.

C2 — short_ambiguous_block in router-tool-gate.mjs + router-prehook.mjs.
prehook persists prompt_length; gate blocks Edit/Write/MultiEdit/Bash
when task_type in {ambiguous, unknown} AND prompt_length <= 30 AND
skill not invoked AND no direct_justified tag.

C3 — self-assessment timeout 30s to 50s in observer-self-assessment-api.mjs.
Windows TLS handshake + Sonnet latency exceeded 30s. Stop-hook has 60s
budget; 50s leaves headroom. DEFAULT_TIMEOUT_MS exported for tests.

C4 — Reviewer findings block in status-md-generator.mjs. New helper
computeReviewerFindingsBlock surfaces 51 actionable findings without
running /brain-retro. Detects batch-reviewed via
outcome_reviewed_source=direct_api_batch. MD012 guard test added.

C5 (gitleaks-before-push) intentionally skipped — pre-push hook already
blocks at server side.

Tests: 956/956 root tools, 0 regressions. LEFTHOOK=0 used per quirk #111.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 06:46:55 +03:00

129 lines
5.2 KiB
JavaScript

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