voyage/tests/hooks/test-lock-green.test.mjs
Kjell Tore Guttormsen a1e0e50451
fix: round 9 of the release review of 6.3.0, for 6.3.1
Round 9 (0f400d3..a10018f, WARN: 0 BLOCKER, 12 MAJOR): seven findings and
three brief notes, by operator choice. Each finding began with a test that
failed on a10018f on its assertion; suite 2230 (2228/0/2).

- A check refused for its comment (a quote, $, backtick or backslash) keeps
  its command and its reason, and every reader refuses it out loud: a nested
  ## Verification bullet stays a criterion (PLAN_VERIFICATION_NOT_RUNNABLE)
  and a brief SC warns (BRIEF_SC_NOT_RUNNABLE). It was folded into a
  sub-point or read as prose, a regression from a10018f. Readers that took a
  command as runnable (the end-to-end lists) skip a refused one.
- The verdict gate reads the last fenced block's info string and body from
  one reading, and exits 2 unless the info string is exactly json: a last
  block opened as ```json {x} held a BLOCKER the gate passed with exit 0
  (also in 6.3.0). A last block never closed is exit 2 too.
- One fence reader (findings-schema fencedBlocks) for the gate and every
  trailing-json reader: a fence closes only on a run at least as long as its
  opener, as in Markdown, so a four-backtick example of a json block is one
  block; CRLF and an indented fence read alike in the gate and the readers.
- Files: a description runs on past a comma and onto the next line until a
  new entry starts (a list item on a new line, or a backticked path), and a
  plain path followed by a colon starts its description. Measured over all
  939 Files: fields in the .md files under the marketplace and the test repo
  (560 of them in the 48 plan.md files outside worktrees): 0 read
  differently, tests-only changed in 0.
- The test lock compares --cmd by its words, so other blanks between the
  same words are the step's command.
- release-check decides "two files" on real paths: one review.md reached
  through a symlinked --repo is one file.
- Brief notes: red proof's stopped control run has its own test (green at
  once, red with the branch deleted); the two tests that assumed jest is not
  installed run with a PATH that finds only git (red on a10018f with a jest
  on PATH, green now); the Files: reader's doc states the measured counts
  (520 fields in 48 plan files at rounds 7-8, not "182 plans"; 560 now).

Not in this round (operator 2026-10-11): a144e9ce, 1dc2e785, dc3a467e,
2fa88853, 26873fa5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:49:01 +02:00

151 lines
7.9 KiB
JavaScript

// tests/hooks/test-lock-green.test.mjs
// The test lock's green mark (operator decision 2026-10-08, voyage-lekerepo
// trial point 7). The stop guard blocked Stop three times while the executor
// waited for the controller in the background, though the step's test was
// already green. Now:
// - `test-lock.mjs green --cmd "<Verify>"` RUNS the step's test command and
// marks the lock "seen green" only on exit 0 (a command proves it, never a
// word);
// - the stop guard lets a green-marked lock stop; the write guard still
// refuses the locked files — the lock still protects the test;
// - `test-lock.mjs pending` clears the mark when a fix round starts.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { runHook } from '../helpers/hook-helper.mjs';
import { ROOT, makeRepo, parseJsonOut } from './guard-helpers.mjs';
import { pathWith } from '../helpers/bin-path.mjs';
const CLI = join(ROOT, 'lib', 'util', 'test-lock.mjs');
const STOP = join(ROOT, 'hooks', 'scripts', 'stop-test-lock.mjs');
const PRE_WRITE = join(ROOT, 'hooks', 'scripts', 'pre-write-executor.mjs');
const SESS = `sess-green-${process.pid}-${Date.now()}`;
const CMD = 'node --test tests/step.test.mjs';
function cli(repo, args, env = {}) {
const r = spawnSync(process.execPath, [CLI, ...args], { cwd: repo, encoding: 'utf8', env: { ...process.env, CLAUDE_CODE_SESSION_ID: SESS, ...env } });
return { code: r.status, stdout: r.stdout, stderr: r.stderr };
}
function lockedRepo(testBody) {
const repo = makeRepo('main');
mkdirSync(join(repo, 'tests'), { recursive: true });
writeFileSync(join(repo, 'tests', 'step.test.mjs'), [
"import { test } from 'node:test';",
"import { strict as assert } from 'node:assert';",
`test('step', () => { ${testBody} });`,
'',
].join('\n'));
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3', '--cmd', CMD]).code, 0);
return repo;
}
const stop = (repo) => ({ hook_event_name: 'Stop', session_id: SESS, cwd: repo, stop_hook_active: false });
const edit = (file_path) => ({ hook_event_name: 'PreToolUse', tool_name: 'Edit', tool_input: { file_path, old_string: 'x', new_string: 'z' } });
test('green: the step\'s test passes -> the lock is marked green, Stop goes through, the test file stays locked', async () => {
const repo = lockedRepo('assert.equal(1, 1);');
const g = cli(repo, ['green', '--cmd', CMD]);
assert.equal(g.code, 0, g.stderr);
assert.ok(JSON.parse(cli(repo, ['status']).stdout).green_at, 'status shows green_at');
assert.equal(parseJsonOut((await runHook(STOP, stop(repo))).stdout), null, 'a green lock does not block Stop');
const w = await runHook(PRE_WRITE, edit(join(repo, 'tests', 'step.test.mjs')));
assert.equal(w.code, 2, 'the test file is still locked against edits');
});
test('green: the test fails -> exit 1, no mark, Stop is still refused', async () => {
const repo = lockedRepo('assert.equal(1, 2);');
assert.equal(cli(repo, ['green', '--cmd', CMD]).code, 1);
assert.equal(JSON.parse(cli(repo, ['status']).stdout).green_at, null);
assert.equal(parseJsonOut((await runHook(STOP, stop(repo))).stdout)?.decision, 'block');
});
test('green: a --cmd that is not the stored test command is never run -> exit 2, no mark', () => {
const repo = lockedRepo('assert.equal(1, 1);');
const g = cli(repo, ['green', '--cmd', 'echo ok']);
assert.equal(g.code, 2, g.stderr);
assert.equal(JSON.parse(cli(repo, ['status']).stdout).green_at, null);
});
test('green: no lock to mark -> exit 1', () => {
const repo = makeRepo('main');
assert.equal(cli(repo, ['green', '--cmd', CMD]).code, 1);
});
test('pending: a fix round clears the mark, and Stop is refused again', async () => {
const repo = lockedRepo('assert.equal(1, 1);');
assert.equal(cli(repo, ['green', '--cmd', CMD]).code, 0);
assert.equal(cli(repo, ['pending']).code, 0);
assert.equal(JSON.parse(cli(repo, ['status']).stdout).green_at, null);
assert.equal(parseJsonOut((await runHook(STOP, stop(repo))).stdout)?.decision, 'block');
});
test('re-lock of the same step starts without a mark', () => {
const repo = lockedRepo('assert.equal(1, 1);');
assert.equal(cli(repo, ['green', '--cmd', CMD]).code, 0);
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3', '--cmd', CMD]).code, 0);
assert.equal(JSON.parse(cli(repo, ['status']).stdout).green_at, null);
});
test('trekexecute: D1 marks the lock green through the command, a fix round marks it pending', () => {
const md = readFileSync(join(ROOT, 'commands', 'trekexecute.md'), 'utf8');
assert.match(md, /test-lock\.mjs green\n/);
assert.match(md, /test-lock\.mjs lock --files \{test files\} --step \{N\} --cmd "\{Verify command\}"/);
assert.match(md, /test-lock\.mjs pending/);
});
test('green: exit 127 from the test program itself is a red -> exit 1, no mark (release review of 6.3.0, round 7)', () => {
const repo = makeRepo('main');
mkdirSync(join(repo, 'tests'), { recursive: true });
// The tool the step builds is not there yet: bash runs, and exits 127 for it.
writeFileSync(join(repo, 'tests', 'e2e.sh'), './bin/tool\n');
const cmd = 'bash tests/e2e.sh';
assert.equal(cli(repo, ['lock', '--files', 'tests/e2e.sh', '--step', '3', '--cmd', cmd]).code, 0);
const g = cli(repo, ['green', '--cmd', cmd]);
assert.equal(g.code, 1, g.stderr);
assert.match(g.stderr, /exited 127/);
assert.equal(JSON.parse(cli(repo, ['status']).stdout).green_at, null);
});
test('green (release review of 6.3.0, round 8): a trailing # comment on either side is no part of the command', () => {
for (const [lockCmd, greenCmd] of [[`${CMD} # the step's test`, CMD], [CMD, `${CMD} # the step's test`]]) {
const repo = makeRepo('main');
mkdirSync(join(repo, 'tests'), { recursive: true });
writeFileSync(join(repo, 'tests', 'step.test.mjs'), "import { test } from 'node:test';\ntest('step', () => {});\n");
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3', '--cmd', lockCmd]).code, 0);
const g = cli(repo, ['green', '--cmd', greenCmd]);
assert.equal(g.code, 0, `lock "${lockCmd}", green "${greenCmd}": ${g.stderr}`);
}
});
test('green (release review of 6.3.0, round 8): a test program that is not installed is reported as could not start', () => {
const repo = makeRepo('main');
mkdirSync(join(repo, 'tests'), { recursive: true });
writeFileSync(join(repo, 'tests', 'step.test.mjs'), 'x\n');
// `jest` is on the allowlist; the PATH finds only git, so it cannot start on
// any machine, one with jest installed too (round 9).
assert.equal(cli(repo, ['lock', '--files', 'tests/step.test.mjs', '--step', '3', '--cmd', 'jest tests/step.test.mjs']).code, 0);
const bin = pathWith(['git']);
try {
const g = cli(repo, ['green', '--cmd', 'jest tests/step.test.mjs'], { PATH: bin.path });
assert.equal(g.code, 2, g.stderr);
assert.match(g.stderr, /could not start/, g.stderr);
} finally { bin.cleanup(); }
});
test('green (release review of 6.3.0, round 9 / e36a533e): the same words with other blanks between them are the step\'s command', () => {
// runCommand splits a command on any blank, so a double space or a tab from
// the plan span runs the very same words; the --cmd check compared strings.
for (const greenCmd of ['node --test tests/step.test.mjs', 'node\t--test tests/step.test.mjs', ` ${CMD}\t`]) {
const repo = lockedRepo('assert.equal(1, 1);');
const g = cli(repo, ['green', '--cmd', greenCmd]);
assert.equal(g.code, 0, `green ${JSON.stringify(greenCmd)}: ${g.stderr}`);
}
// The control: other words are still refused.
const repo = lockedRepo('assert.equal(1, 1);');
const g = cli(repo, ['green', '--cmd', 'node --test tests/other.test.mjs']);
assert.equal(g.code, 2, g.stderr);
assert.match(g.stderr, /not the step's test command/, g.stderr);
});