Files
brain/tools/enforce-rationalization-audit.test.mjs
T

230 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { findRationalizationPhrases, detectProdEditWithoutTest, audit, decide, stripQuotedContext } from './enforce-rationalization-audit.mjs';
describe('stripQuotedContext (false-positive guard for quoted citations)', () => {
it('removes inline-code backtick content', () => {
const result = stripQuotedContext('use `временно` keyword here');
expect(result.toLowerCase()).not.toContain('временно');
expect(result).toContain('use');
expect(result).toContain('keyword here');
});
it('removes fenced code block content (multi-line)', () => {
const result = stripQuotedContext('text before\n```\nblock with временно inside\n```\ntext after');
expect(result.toLowerCase()).not.toContain('временно');
expect(result).toContain('text before');
expect(result).toContain('text after');
});
it('removes Russian guillemet content', () => {
const result = stripQuotedContext('запрещаем «временно» в опциях');
expect(result.toLowerCase()).not.toContain('временно');
expect(result).toContain('запрещаем');
expect(result).toContain('в опциях');
});
it('removes straight double-quoted strings', () => {
const result = stripQuotedContext('phrase: "временно" detected');
expect(result.toLowerCase()).not.toContain('временно');
expect(result).toContain('phrase');
expect(result).toContain('detected');
});
it('preserves plain rationalization text outside quotes', () => {
const result = stripQuotedContext('временно сделаю фикс');
expect(result.toLowerCase()).toContain('временно');
});
it('handles mixed quoted + plain — strips quoted only', () => {
const result = stripQuotedContext('я скажу «временно» — но реально временно использую');
// first «временно» stripped; second plain remains
const lo = result.toLowerCase();
const matches = (lo.match(/временно/g) || []).length;
expect(matches).toBe(1);
});
it('returns empty string for non-string input', () => {
expect(stripQuotedContext(null)).toBe('');
expect(stripQuotedContext(undefined)).toBe('');
expect(stripQuotedContext(42)).toBe('');
});
});
describe('findRationalizationPhrases — does NOT flag quoted citations', () => {
it('skips inline-code citation', () => {
expect(findRationalizationPhrases('hook detects `временно` pattern')).toEqual([]);
});
it('skips guillemet citation', () => {
expect(findRationalizationPhrases('block options containing «временно» keyword')).toEqual([]);
});
it('skips fenced code block citation', () => {
expect(findRationalizationPhrases('see code:\n```\nphrase: временно\n```\nend')).toEqual([]);
});
it('skips straight-quote citation', () => {
expect(findRationalizationPhrases('match phrase: "временно" — flagged earlier')).toEqual([]);
});
it('STILL flags real rationalization outside quotes', () => {
expect(findRationalizationPhrases('я временно пропущу тест')).toContain('временно');
});
it('mixed: flags plain occurrence, ignores quoted occurrence', () => {
const hits = findRationalizationPhrases('сказал «временно» — реально временно сделал');
expect(hits).toContain('временно');
expect(hits.length).toBe(1);
});
});
describe('findRationalizationPhrases', () => {
it('detects "just this once" in mixed case', () => {
expect(findRationalizationPhrases('Hmm, Just This Once we will skip')).toContain('just this once');
});
it('detects "пока без" Russian', () => {
expect(findRationalizationPhrases('сделаем пока без тестов')).toContain('пока без');
});
it('detects multiple phrases in one text', () => {
const hits = findRationalizationPhrases('временно делаем потом разберусь');
expect(hits.length).toBeGreaterThanOrEqual(2);
});
it('returns empty array on clean text', () => {
expect(findRationalizationPhrases('coverage: skill:tdd')).toEqual([]);
});
});
describe('detectProdEditWithoutTest', () => {
it('flags prod edit without any test edit in turn', () => {
const uses = [{ name: 'Edit', input: { file_path: 'tools/foo.mjs' } }];
expect(detectProdEditWithoutTest(uses)).toEqual(['tools/foo.mjs']);
});
it('does NOT flag when test also edited', () => {
const uses = [
{ name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } },
{ name: 'Edit', input: { file_path: 'tools/foo.mjs' } },
];
expect(detectProdEditWithoutTest(uses)).toEqual([]);
});
it('does NOT flag for non-prod paths', () => {
expect(detectProdEditWithoutTest([{ name: 'Edit', input: { file_path: 'docs/x.md' } }])).toEqual([]);
});
});
describe('audit', () => {
it('flags rationalization phrases in assistant text', () => {
const entries = [
{ message: { role: 'user', content: 'go' } },
{ message: { role: 'assistant', content: [{ type: 'text', text: 'just this once без скила' }] } },
];
const flags = audit(entries);
expect(flags.find((f) => f.kind === 'rationalization-phrase')).toBeTruthy();
});
it('flags prod-edit-without-test', () => {
const entries = [
{ message: { role: 'user', content: 'go' } },
{ message: { role: 'assistant', content: [
{ type: 'tool_use', id: 't1', name: 'Edit', input: { file_path: 'tools/foo.mjs' } },
] } },
];
const flags = audit(entries);
expect(flags.find((f) => f.kind === 'prod-edit-without-test')).toBeTruthy();
});
it('flags weak commit messages (<12 chars)', () => {
const entries = [
{ message: { role: 'user', content: 'go' } },
{ message: { role: 'assistant', content: [
{ type: 'tool_use', id: 't1', name: 'Bash', input: { command: 'git commit -m "fix"' } },
] } },
];
const flags = audit(entries);
expect(flags.find((f) => f.kind === 'weak-commit-message')).toBeTruthy();
});
it('returns no flags for clean turn', () => {
const entries = [
{ message: { role: 'user', content: 'go' } },
{ message: { role: 'assistant', content: [
{ type: 'text', text: 'coverage: skill:tdd\nworking properly' },
{ type: 'tool_use', id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } },
{ type: 'tool_use', id: 't2', name: 'Edit', input: { file_path: 'tools/foo.mjs' } },
] } },
];
expect(audit(entries)).toEqual([]);
});
});
describe('vocab — new phrases', () => {
it('detects "давай разок"', () => {
expect(findRationalizationPhrases('давай разок без тестов')).toContain('давай разок');
});
it('detects "только сейчас"', () => {
expect(findRationalizationPhrases('только сейчас пропустим')).toContain('только сейчас');
});
it('detects "один раз без правил"', () => {
expect(findRationalizationPhrases('один раз без правил сделаем')).toContain('один раз без правил');
});
it('detects "на этот раз без"', () => {
expect(findRationalizationPhrases('на этот раз без скила')).toContain('на этот раз без');
});
it('detects "я знаю что не надо но"', () => {
expect(findRationalizationPhrases('я знаю что не надо но пропустим')).toContain('я знаю что не надо но');
});
});
describe('decide — escalation on 3rd flag', () => {
const sessionId = 'test-session';
const textWithPhrase = 'just this once';
it('does NOT block when priorFlagCount=0', () => {
const result = decide({ assistantText: textWithPhrase, sessionId, priorFlagCount: 0 });
expect(result.block).toBe(false);
expect(result.detected.length).toBeGreaterThan(0);
});
it('does NOT block when priorFlagCount=1', () => {
const result = decide({ assistantText: textWithPhrase, sessionId, priorFlagCount: 1 });
expect(result.block).toBe(false);
});
it('blocks when priorFlagCount=2 (3rd occurrence)', () => {
const result = decide({ assistantText: textWithPhrase, sessionId, priorFlagCount: 2 });
expect(result.block).toBe(true);
expect(result.message).toMatch(/rationali/i);
});
it('blocks when priorFlagCount=5 (subsequent occurrences)', () => {
const result = decide({ assistantText: textWithPhrase, sessionId, priorFlagCount: 5 });
expect(result.block).toBe(true);
});
it('does NOT block clean text even with priorFlagCount=10', () => {
const result = decide({ assistantText: 'coverage: skill:tdd', sessionId, priorFlagCount: 10 });
expect(result.block).toBe(false);
expect(result.detected).toEqual([]);
});
it('override=true suppresses block even on 3rd flag', () => {
const result = decide({ assistantText: textWithPhrase, sessionId, override: true, priorFlagCount: 2 });
expect(result.block).toBe(false);
});
});
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
describe('enforce-rationalization-audit — fail-CLOSE (М7 Фаза 4b, §2.1 Класс 2)', () => {
const here = dirname(fileURLToPath(import.meta.url));
const src = readFileSync(join(here, 'enforce-rationalization-audit.mjs'), 'utf8');
it('использует exitDisciplineDecision (fail-CLOSE wrapper Фазы 0)', () => {
expect(src.includes('exitDisciplineDecision')).toBe(true);
});
it('НЕ содержит fail-open catch → block:false (анти-SE2)', () => {
const failOpen = /catch\s*(?:\([^)]*\))?\s*\{[^}]*exitDecision\(\{\s*block:\s*false/s.test(src);
expect(failOpen).toBe(false);
});
});