Files
brain/tools/enforce-subagent-return-scanner.test.mjs

123 lines
5.9 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
scanReturn,
validateTestClaimStructure,
NARRATIVE_TEST_CLAIMS,
buildPostToolOutput,
} from './enforce-subagent-return-scanner.mjs';
describe('enforce-subagent-return-scanner / scanReturn — state-file exfil (§3.4)', () => {
it('erases content that looks like a gate state-file (schema_version + key)', () => {
const content = JSON.stringify({ schema_version: 3, parent_random_id: 'abc', chain_step: 2 });
const r = scanReturn(content);
expect(r.action).toBe('erase');
expect(r.findings.some((f) => f.type === 'state_file_exfil')).toBe(true);
});
it('detects state-file signature even when embedded in narrative text', () => {
const content = 'here is the dump: {"schema_version":1,"session_id":"x","chain_active":true}';
const r = scanReturn(content);
expect(r.findings.some((f) => f.type === 'state_file_exfil')).toBe(true);
});
it('does not flag normal JSON without state-file keys', () => {
const content = JSON.stringify({ result: 'ok', files_changed: 3 });
const r = scanReturn(content);
expect(r.findings.some((f) => f.type === 'state_file_exfil')).toBe(false);
});
});
describe('enforce-subagent-return-scanner / scanReturn — bulk path enumeration', () => {
it('soft-flags when >5 runtime json paths enumerated', () => {
const paths = Array.from({ length: 7 }, (_, i) => `~/.claude/runtime/file-${i}.json`).join('\n');
const r = scanReturn(paths);
expect(r.findings.some((f) => f.type === 'bulk_path_enumeration')).toBe(true);
});
it('does not flag a few path mentions', () => {
const r = scanReturn('~/.claude/runtime/router-state.json mentioned once');
expect(r.findings.some((f) => f.type === 'bulk_path_enumeration')).toBe(false);
});
});
describe('enforce-subagent-return-scanner / scanReturn — G2 narrative test claims', () => {
it('erases "12 tests passed" claim', () => {
const r = scanReturn('Done. 12 tests passed, build is GREEN.');
expect(r.action).toBe('erase');
expect(r.findings.some((f) => f.type === 'narrative_test_claim_unverified')).toBe(true);
});
it('erases Russian "все тесты прошли"', () => {
const r = scanReturn('Готово, все тесты прошли успешно.');
expect(r.findings.some((f) => f.type === 'narrative_test_claim_unverified')).toBe(true);
});
it('erases bare "нет ошибок"', () => {
const r = scanReturn('Запустил — нет ошибок.');
expect(r.findings.some((f) => f.type === 'narrative_test_claim_unverified')).toBe(true);
});
it('does not flag a neutral progress report', () => {
const r = scanReturn('Я изменил три файла и закоммитил.');
expect(r.action).toBe('none');
expect(r.findings).toEqual([]);
});
it('NARRATIVE_TEST_CLAIMS is a non-empty array of RegExp', () => {
expect(Array.isArray(NARRATIVE_TEST_CLAIMS)).toBe(true);
expect(NARRATIVE_TEST_CLAIMS.length).toBeGreaterThan(0);
expect(NARRATIVE_TEST_CLAIMS.every((r) => r instanceof RegExp)).toBe(true);
});
it('handles non-string content', () => {
expect(scanReturn(null).action).toBe('none');
});
it('does not false-match "всё ок" inside "всё окно"', () => {
expect(scanReturn('всё окно открыто').action).toBe('none');
});
it('still matches a bare "всё ок" claim', () => {
expect(scanReturn('всё ок, готово').action).toBe('erase');
});
});
describe('enforce-subagent-return-scanner / validateTestClaimStructure', () => {
it('accepts a fully-formed test-claim object', () => {
const obj = {
tests_run: 10, tests_passed: 10, tests_failed: 0, tests_skipped: 0,
raw_test_runner_output: 'x'.repeat(120),
};
expect(validateTestClaimStructure(obj).valid).toBe(true);
});
it('rejects when a required key is missing', () => {
const obj = { tests_run: 10, tests_passed: 10, raw_test_runner_output: 'x'.repeat(120) };
const r = validateTestClaimStructure(obj);
expect(r.valid).toBe(false);
expect(r.reason).toMatch(/tests_failed/);
});
it('rejects when raw output too short (<100 chars)', () => {
const obj = { tests_run: 1, tests_passed: 1, tests_failed: 0, raw_test_runner_output: 'short' };
expect(validateTestClaimStructure(obj).valid).toBe(false);
});
it('rejects when a field has wrong type', () => {
const obj = { tests_run: 'ten', tests_passed: 1, tests_failed: 0, raw_test_runner_output: 'x'.repeat(120) };
expect(validateTestClaimStructure(obj).valid).toBe(false);
});
it('rejects non-object', () => {
expect(validateTestClaimStructure(null).valid).toBe(false);
});
});
describe('enforce-subagent-return-scanner / buildPostToolOutput', () => {
it('returns plain continue for action none', () => {
const out = buildPostToolOutput({ action: 'none', findings: [] }, { eraseEnabled: true });
expect(out.hookSpecificOutput?.additionalContext).toBeUndefined();
});
it('adds escalation context for erase findings (narrative claim)', () => {
const scan = { action: 'erase', findings: [{ type: 'narrative_test_claim_unverified', excerpt: '12 tests passed' }] };
const out = buildPostToolOutput(scan, { eraseEnabled: false });
expect(out.hookSpecificOutput.additionalContext).toMatch(/independently|verify|Bash/i);
});
it('adds escalation context for state-file exfil', () => {
const scan = { action: 'erase', findings: [{ type: 'state_file_exfil', excerpt: '{...}' }] };
const out = buildPostToolOutput(scan, { eraseEnabled: true });
expect(out.hookSpecificOutput.additionalContext).toMatch(/state|exfil/i);
});
it('adds soft note for bulk path enumeration', () => {
const scan = { action: 'flag', findings: [{ type: 'bulk_path_enumeration', matched: '7', excerpt: '' }] };
const out = buildPostToolOutput(scan, { eraseEnabled: true });
expect(out.hookSpecificOutput.additionalContext).toMatch(/path|enumerat/i);
});
});