// 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 ""` 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'; import { childEnv } from '../helpers/child-env.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: childEnv({ 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); });