397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.4 KiB
JavaScript
67 lines
2.4 KiB
JavaScript
/**
|
|
* PreToolUse(Edit|Write) wrapper for tools/tdd-real-test-verifier.mjs.
|
|
* Router-gate v4 spec §3.11.
|
|
*
|
|
* Blocks Edit/Write on a *.test.* / *.spec.* file when the proposed content
|
|
* lacks expect() / it() OR doesn't reference any of the prod files edited
|
|
* this session (the sentinel-gaming guard from spec §3.11).
|
|
*
|
|
* Fail-CLOSE: an internal error blocks (security-hook default).
|
|
*/
|
|
import { fileURLToPath } from 'url';
|
|
import { join } from 'path';
|
|
import { existsSync, readFileSync } from 'fs';
|
|
import {
|
|
readStdin,
|
|
parseEventJson,
|
|
exitDecision,
|
|
runtimeDir,
|
|
} from './enforce-hook-helpers.mjs';
|
|
import { verifyRealTestContent } from './tdd-real-test-verifier.mjs';
|
|
|
|
const TEST_FILE_RE = /\.(?:test|spec)\.[a-z0-9]+$/i;
|
|
|
|
function readEditedFiles(sessionId) {
|
|
try {
|
|
const p = join(runtimeDir(), `edited-files-${sessionId || 'unknown'}.json`);
|
|
if (!existsSync(p)) return [];
|
|
const j = JSON.parse(readFileSync(p, 'utf-8'));
|
|
return Array.isArray(j.files) ? j.files : [];
|
|
} catch { return []; }
|
|
}
|
|
|
|
export function decide({ filePath, content, editedFiles }) {
|
|
const fp = String(filePath || '').split('\\').join('/');
|
|
if (!TEST_FILE_RE.test(fp)) return { block: false, reason: null };
|
|
const r = verifyRealTestContent(content, editedFiles || []);
|
|
if (r.valid) return { block: false, reason: null };
|
|
return { block: true, reason: r.reason };
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
const raw = await readStdin();
|
|
const event = parseEventJson(raw);
|
|
if (event.tool_name !== 'Edit' && event.tool_name !== 'Write') {
|
|
return exitDecision({ block: false });
|
|
}
|
|
const filePath = event.tool_input?.file_path || '';
|
|
const content = event.tool_input?.content || event.tool_input?.new_string || '';
|
|
const sessionId = event.session_id || 'unknown';
|
|
const editedFiles = readEditedFiles(sessionId);
|
|
const r = decide({ filePath, content, editedFiles });
|
|
if (r.block) {
|
|
return exitDecision({
|
|
block: true,
|
|
message: `[tdd-real-test-verifier] proposed test file fails real-test check: ${r.reason}. Write a test that asserts behaviour (expect + it/test) and references one of the edited prod files.`,
|
|
});
|
|
}
|
|
return exitDecision({ block: false });
|
|
} catch {
|
|
return exitDecision({ block: true, message: '[tdd-real-test-verifier] внутренняя ошибка — fail-CLOSE' });
|
|
}
|
|
}
|
|
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
if (isCli) main();
|