397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
6.0 KiB
JavaScript
171 lines
6.0 KiB
JavaScript
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);
|
|
});
|
|
});
|