Files
portal/tools/router-prehook.test.mjs
T
Дмитрий fb0309d357 feat(router): prehook inheritance + task_id + cost, drop ENFORCEMENT_TYPES (phase 2 task 14)
Spec §4.1 + §4.2 — Phase 2 Task 14:

- tools/router-prehook.mjs:
  - removed: ENFORCEMENT_TYPES + isEnforcementRequired (gate now uses
    NON_BLOCKING_TASK_TYPES on state.classification.task_type — Task 13).
  - buildStateFromClassification:
    + task_id: randomUUID() per turn (or caller-supplied taskId).
    + task_cost: {} placeholder (caller fills classifier_input/output_tokens
      when available; LLM helper does not yet thread tokens through — task
      17/20 will add).
    + inheritance: { inherited_from_task_id, inheritance_age_minutes } —
      written only on continuation (source: 'prefilter_inherited'); copied
      into the episode by observer-stop-hook in Task 16 (closes B5).
    - dropped enforcementRequired field — Tool gate decides solely on
      task_type + no_skill_found + skillInvokedThisTurn.
  - main(): read prevState (~/.claude/runtime/router-state-<session>.json)
    BEFORE overwrite; pass to classify({ prevState }); lift inheritance
    from classification result into the new state when prefilter inherited.
- tools/router-prehook.test.mjs: rewritten — 9 tests covering v4 shape,
  task_id randomness + override, inheritance present/absent, cost passthrough,
  ENFORCEMENT_TYPES + isEnforcementRequired no longer exported, UTF-8 smoke.

Tests: 9/9 prehook PASS. Consumer regressions: router-tool-gate (25) +
router-classifier (44) = 69 PASS — no regressions.
2026-05-25 14:28:25 +03:00

79 lines
3.1 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');
});
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('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');
});
});