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