Wave-1 order 4 (P7, P12, the P9 test lock, the P3 plan-approval guard). All command hooks; none runs a model. - P12: pre-bash-executor answers permissionDecision "ask" before merge into main, push, tag/publish and destructive git (holds in auto mode). post-approval-log writes exactly what the person was asked to the K2 approval log: merge/push to main as `merge`, tag/publish as `release`, with a one-hour expiry; nothing when no person was asked. - P3: pre-plan-gate runs the K3 plan gate on a Voyage plan leaving plan mode and sends a refused plan back; "not measured" asks, never passes. - P9: lib/util/test-lock.mjs (K8). pre-write-executor refuses edits to locked test files; stop-test-lock refuses "done" for the locking session (stop_hook_active guarded). - P7: post-edit-check runs the target repo's own formatter/linter per edit, only during a Voyage plan run or after opt-in (.claude/voyage-guards.json); lib/verification/step-check.mjs (K9) fails the step on findings; no tool reads "not measured", never green. - STORM cap matcher also names `Agent` (the tool's name since 2.1.63). - Mod precondition stated in README, SECURITY.md and every PreToolUse stop guard: Voyage's guards are deterministic only when no installed mod handles `tool.check`. K2, K3 and K5 are stubbed in tests until orders 2 and 3 are merged. Two independent review rounds; every finding has a regression test. Suite 1600 (1598 pass / 0 fail / 2 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.6 KiB
JavaScript
106 lines
4.6 KiB
JavaScript
// tests/lib/step-check.test.mjs
|
|
// K9 — the step check (lib/verification/step-check.mjs --cwd <dir>). At step
|
|
// end it runs the repo's own tools over the changed files (and the repo-wide
|
|
// ones, such as the type checker), reads the post-edit guard's log, and
|
|
// answers 0 = green, 1 = findings, 2 = "not measured" (never green). JSON on
|
|
// stdout. The architecture-rule runner (K5, wave-1 order 3) is optional: when
|
|
// it is missing, it is listed as not measured.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { ROOT, makeRepo, gitIn, fakeTool, tmp, pluginCopy } from '../hooks/guard-helpers.mjs';
|
|
|
|
const CLI = join(ROOT, 'lib', 'verification', 'step-check.mjs');
|
|
|
|
function run(cli, args, env = {}) {
|
|
const r = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin', ...env } });
|
|
let json = null;
|
|
try { json = JSON.parse(r.stdout); } catch { json = null; }
|
|
return { code: r.status, stdout: r.stdout, stderr: r.stderr, json };
|
|
}
|
|
|
|
function toolRepo() {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, '.prettierrc'), '{}');
|
|
writeFileSync(join(repo, '.gitignore'), 'node_modules/\n');
|
|
gitIn(repo, 'add', '.prettierrc', '.gitignore');
|
|
gitIn(repo, 'commit', '-q', '-m', 'tools');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
|
return repo;
|
|
}
|
|
|
|
test('K9 green (0): the changed files pass the repo\'s own tools', () => {
|
|
const repo = toolRepo();
|
|
writeFileSync(join(repo, 'a.js'), 'ok\n');
|
|
const r = run(CLI, ['--cwd', repo]);
|
|
assert.equal(r.code, 0, r.stdout + r.stderr);
|
|
assert.equal(r.json.status, 'green');
|
|
assert.ok(r.json.checks.some((c) => c.name === 'prettier' && c.status === 'clean'));
|
|
});
|
|
|
|
test('K9 findings (1): a changed file the formatter rejects', () => {
|
|
const repo = toolRepo();
|
|
writeFileSync(join(repo, 'a.js'), 'BAD\n');
|
|
const r = run(CLI, ['--cwd', repo]);
|
|
assert.equal(r.code, 1, r.stdout);
|
|
assert.equal(r.json?.status, 'findings');
|
|
});
|
|
|
|
test('K9 findings (1): the post-edit log holds a finding for a changed file no tool can re-check', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, 'a.js'), 'x\n');
|
|
mkdirSync(join(repo, '.git', 'voyage'), { recursive: true });
|
|
appendFileSync(join(repo, '.git', 'voyage', 'post-edit.jsonl'),
|
|
JSON.stringify({ ts: new Date().toISOString(), file: 'a.js', status: 'finding', results: [{ name: 'prettier', status: 'finding' }] }) + '\n');
|
|
const r = run(CLI, ['--cwd', repo]);
|
|
assert.equal(r.code, 1, r.stdout);
|
|
assert.equal(r.json?.status, 'findings', 'exit 1 must be a finding, not a crash');
|
|
});
|
|
|
|
test('K9 not measured (2): a repo with no tools at all -> never green', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, 'a.js'), 'BAD\n');
|
|
const r = run(CLI, ['--cwd', repo]);
|
|
assert.equal(r.code, 2, r.stdout);
|
|
assert.equal(r.json.status, 'not_measured');
|
|
});
|
|
|
|
test('K9 not measured (2): not a git repository', () => {
|
|
const r = run(CLI, ['--cwd', tmp()]);
|
|
assert.equal(r.code, 2);
|
|
});
|
|
|
|
test('K9 usage error (2): no --cwd', () => {
|
|
assert.equal(run(CLI, []).code, 2);
|
|
});
|
|
|
|
test('K9 --full: checks tracked files, not only the diff', () => {
|
|
const repo = toolRepo();
|
|
writeFileSync(join(repo, 'old.js'), 'BAD\n');
|
|
gitIn(repo, 'add', 'old.js');
|
|
gitIn(repo, 'commit', '-q', '-m', 'old');
|
|
assert.notEqual(run(CLI, ['--cwd', repo]).json.status, 'findings', 'old.js is not in the diff');
|
|
assert.equal(run(CLI, ['--cwd', repo, '--full']).code, 1);
|
|
});
|
|
|
|
test('K5 optional: arch-rules missing is listed as not measured and does not turn green into red', () => {
|
|
const plugin = pluginCopy({ 'lib/verification/arch-rules.mjs': 'missing' });
|
|
const repo = toolRepo();
|
|
writeFileSync(join(repo, 'a.js'), 'ok\n');
|
|
const r = run(join(plugin, 'lib', 'verification', 'step-check.mjs'), ['--cwd', repo]);
|
|
assert.equal(r.code, 0, r.stdout);
|
|
const arch = r.json.checks.find((c) => c.name === 'arch-rules');
|
|
assert.equal(arch.status, 'not_measured');
|
|
});
|
|
|
|
test('K5 optional: arch-rules stub reporting a breach (exit 1) makes the step red', () => {
|
|
const plugin = pluginCopy({ 'lib/verification/arch-rules.mjs': 'stub' });
|
|
const repo = toolRepo();
|
|
writeFileSync(join(repo, 'a.js'), 'ok\n');
|
|
const r = run(join(plugin, 'lib', 'verification', 'step-check.mjs'), ['--cwd', repo], { STUB_EXIT: '1' });
|
|
assert.equal(r.code, 1, r.stdout);
|
|
assert.equal(r.json?.checks.find((c) => c.name === 'arch-rules')?.status, 'finding');
|
|
});
|