Files
portal/tools/router-tool-gate.test.mjs
T
Дмитрий c7e02eeac9 feat(router): подключить UTF-8 helper к трём хукам (stage 3 follow-up 1)
router-prehook, router-stop-gate, router-tool-gate теперь читают stdin
через readStdinAsUtf8 (StringDecoder). Русский в промпте корректно
доходит до Anthropic API и в state-файл — никаких mojibake типа
'посмотри'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:36:14 +03:00

107 lines
3.8 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
shouldBlock,
decodeRoutingTag,
isReadOnlyBash,
decideDecision,
} from './router-tool-gate.mjs';
const enforcementState = {
enforcementRequired: true,
skillInvokedThisTurn: false,
classification: { taskType: 'feature', recommendedNode: '#19', recommendedChain: 'L1' },
chainProgress: [],
};
describe('isReadOnlyBash', () => {
it('detects ls / cat / grep / git status as read-only', () => {
expect(isReadOnlyBash('ls -la')).toBe(true);
expect(isReadOnlyBash('cat file.txt')).toBe(true);
expect(isReadOnlyBash('grep "x" file')).toBe(true);
expect(isReadOnlyBash('git status')).toBe(true);
expect(isReadOnlyBash('git log')).toBe(true);
expect(isReadOnlyBash('git rev-parse HEAD')).toBe(true);
});
it('does not classify git commit / push as read-only', () => {
expect(isReadOnlyBash('git commit -m "x"')).toBe(false);
expect(isReadOnlyBash('git push origin main')).toBe(false);
});
it('does not classify rm / cp / mv as read-only', () => {
expect(isReadOnlyBash('rm file')).toBe(false);
expect(isReadOnlyBash('cp a b')).toBe(false);
});
});
describe('decodeRoutingTag', () => {
it('parses direct_justified=true with reason', () => {
const r = decodeRoutingTag('<!-- routing: direct_justified=true reason="micro fix per user override" -->');
expect(r.directJustified).toBe(true);
expect(r.reason).toContain('micro fix');
});
it('returns null on missing tag', () => {
expect(decodeRoutingTag('just a regular response')).toBeNull();
});
it('rejects direct_justified=true WITHOUT reason', () => {
const r = decodeRoutingTag('<!-- routing: direct_justified=true -->');
expect(r).toBeNull();
});
});
describe('shouldBlock', () => {
it('blocks Edit on enforcement state without skill invoked', () => {
expect(shouldBlock('Edit', enforcementState, '', { warnOnly: false })).toBe(true);
});
it('does NOT block when skill invoked this turn', () => {
const state = { ...enforcementState, skillInvokedThisTurn: true };
expect(shouldBlock('Edit', state, '', { warnOnly: false })).toBe(false);
});
it('does NOT block when enforcement not required', () => {
const state = { ...enforcementState, enforcementRequired: false };
expect(shouldBlock('Edit', state, '', { warnOnly: false })).toBe(false);
});
it('does NOT block when routing-tag has direct_justified=true with reason', () => {
expect(shouldBlock('Edit', enforcementState, '<!-- routing: direct_justified=true reason="testing" -->', { warnOnly: false })).toBe(false);
});
it('does NOT block read-only Bash', () => {
expect(shouldBlock('Bash', enforcementState, '', { warnOnly: false, bashCommand: 'ls' })).toBe(false);
});
it('warn-only mode never blocks (always returns false)', () => {
expect(shouldBlock('Edit', enforcementState, '', { warnOnly: true })).toBe(false);
});
});
describe('decideDecision', () => {
it('returns decision: block with message when shouldBlock=true', () => {
const r = decideDecision('Edit', enforcementState, '', { warnOnly: false });
expect(r.decision).toBe('block');
expect(r.reason).toMatch(/#19/);
});
it('returns empty (proceed) when shouldBlock=false', () => {
const r = decideDecision('Edit', { ...enforcementState, skillInvokedThisTurn: true }, '', { warnOnly: false });
expect(r.decision).toBeUndefined();
});
it('warn-only mode logs to stderr but does not block', () => {
const r = decideDecision('Edit', enforcementState, '', { warnOnly: true });
expect(r.decision).toBeUndefined();
expect(r.warning).toMatch(/#19/);
});
});
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-tool-gate.mjs');
expect(typeof mod.shouldBlock).toBe('function');
});
});