import { describe, it, expect } from 'vitest'; import { decide } from './enforce-tdd-gate.mjs'; function userMsg(text) { return { message: { role: 'user', content: text } }; } function assistantUses(uses) { return { message: { role: 'assistant', content: uses.map((u, i) => ({ type: 'tool_use', id: u.id || `t${i}`, name: u.name, input: u.input })) } }; } function toolResults(results) { return { message: { role: 'user', content: results.map((r) => ({ type: 'tool_result', tool_use_id: r.id, content: r.content, is_error: r.is_error || false })) } }; } describe('enforce-tdd-gate / decide', () => { it('allows non-production paths', () => { const r = decide({ toolName: 'Edit', filePath: 'docs/x.md', transcriptEntries: [], }); expect(r.block).toBe(false); }); it('allows test files themselves', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.test.mjs', transcriptEntries: [], }); expect(r.block).toBe(false); }); it('blocks prod edit with no preceding test edit', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [userMsg('do it')], }); expect(r.block).toBe(true); expect(r.message).toMatch(/without preceding test edit/); // 1A (2026-05-31): не рекламировать мёртвые override-фразы (findOverride — заглушка v4). expect(r.message).not.toMatch(/Override:/); }); it('blocks when test edited but no vitest RED observed', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('do it'), assistantUses([{ id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } }]), ], }); expect(r.block).toBe(true); expect(r.message).toMatch(/no vitest.*RED/); // 1A (2026-05-31): не рекламировать мёртвые override-фразы (findOverride — заглушка v4). expect(r.message).not.toMatch(/Override:/); }); it('allows after test edit + vitest RED', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('do it'), assistantUses([ { id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } }, { id: 't2', name: 'Bash', input: { command: 'npx vitest run tools/foo.test.mjs' } }, ]), toolResults([{ id: 't2', content: 'Tests 1 failed | 0 passed' }]), ], }); expect(r.block).toBe(false); }); it('allows when "fail" word in vitest stdout', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('do it'), assistantUses([ { id: 't1', name: 'Write', input: { file_path: 'tools/foo.test.mjs' } }, { id: 't2', name: 'Bash', input: { command: 'npx vitest run tools/foo.test.mjs' } }, ]), toolResults([{ id: 't2', content: 'FAIL tools/foo.test.mjs' }]), ], }); expect(r.block).toBe(false); }); it('allows when override phrase present', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [userMsg('срочно надо')], override: { phrase: 'срочно', suppresses: ['tdd-gate'] }, }); expect(r.block).toBe(false); }); it('blocks feature-classified prod edit without plan indicator', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('добавь фичу X'), assistantUses([{ id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } }]), ], classification: { task_type: 'feature' }, }); expect(r.block).toBe(true); expect(r.message).toMatch(/requires a plan/); // 1A (2026-05-31): не рекламировать мёртвые override-фразы (findOverride — заглушка v4). expect(r.message).not.toMatch(/Override:/); }); it('allows feature edit when Skill(superpowers:writing-plans) invoked', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('добавь фичу X'), assistantUses([ { id: 't0', name: 'Skill', input: { skill: 'superpowers:writing-plans' } }, { id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } }, { id: 't2', name: 'Bash', input: { command: 'npx vitest run tools/foo.test.mjs' } }, ]), toolResults([{ id: 't2', content: 'Tests 1 failed' }]), ], classification: { task_type: 'feature' }, }); expect(r.block).toBe(false); }); it('allows feature edit when plan file is referenced', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('добавь фичу X'), assistantUses([ { id: 't0', name: 'Read', input: { file_path: 'docs/superpowers/plans/2026-05-26-foo.md' } }, { id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } }, { id: 't2', name: 'Bash', input: { command: 'npx vitest run tools/foo.test.mjs' } }, ]), toolResults([{ id: 't2', content: 'Tests 1 failed' }]), ], classification: { task_type: 'feature' }, }); expect(r.block).toBe(false); }); it('does NOT require plan for non-feature task types', () => { const r = decide({ toolName: 'Edit', filePath: 'tools/foo.mjs', transcriptEntries: [ userMsg('chore'), assistantUses([ { id: 't1', name: 'Edit', input: { file_path: 'tools/foo.test.mjs' } }, { id: 't2', name: 'Bash', input: { command: 'npx vitest run tools/foo.test.mjs' } }, ]), toolResults([{ id: 't2', content: 'Tests 1 failed' }]), ], classification: { task_type: 'cleanup-but-not-strictly' }, }); expect(r.block).toBe(false); }); });