// tests/lib/done-line.test.mjs // The done line (P9 (4)): a step's "done" is written by a script, and only when // the test command exits 0. Exit 0 = written, 1 = the command failed (nothing // written), 2 = could not run (nothing written, never green). import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const CLI = join(ROOT, 'lib', 'verification', 'done-line.mjs'); function sh(cwd, cmd, args) { const r = spawnSync(cmd, args, { cwd, encoding: 'utf8' }); assert.equal(r.status, 0, `${cmd} ${args.join(' ')}: ${r.stderr}`); return r.stdout.trim(); } function repo() { const dir = mkdtempSync(join(tmpdir(), 'done-line-')); sh(dir, 'git', ['init', '-q']); sh(dir, 'git', ['config', 'user.email', 't@example.invalid']); sh(dir, 'git', ['config', 'user.name', 'test']); mkdirSync(join(dir, 'tests')); writeFileSync(join(dir, 'tests', 'ok.test.mjs'), "import { test } from 'node:test';\ntest('ok', () => {});\n"); writeFileSync(join(dir, 'tests', 'bad.test.mjs'), "import { test } from 'node:test';\ntest('bad', () => { throw new Error('red'); });\n"); sh(dir, 'git', ['add', 'tests']); sh(dir, 'git', ['commit', '-q', '-m', 'tests']); return dir; } const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' }); test('done line: the test command exits 0 -> one DONE line appended, exit 0', () => { const dir = repo(); const report = join(dir, 'step-3.report.md'); try { writeFileSync(report, '# Step 3 report\n'); const r = run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/ok.test.mjs', '--report', report]); assert.equal(r.status, 0, r.stdout + r.stderr); const head = sh(dir, 'git', ['rev-parse', 'HEAD']); const lines = readFileSync(report, 'utf8').split('\n').filter((l) => l.startsWith('DONE ')); assert.equal(lines.length, 1); assert.match(lines[0], new RegExp(`^DONE step 3: \`node --test tests/ok\\.test\\.mjs\` exit 0 at ${head} \\d{4}-\\d{2}-\\d{2}T`)); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('done line: the test command fails -> nothing written, exit 1', () => { const dir = repo(); const report = join(dir, 'step-3.report.md'); try { writeFileSync(report, '# Step 3 report\n'); const r = run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/bad.test.mjs', '--report', report]); assert.equal(r.status, 1, r.stdout + r.stderr); assert.equal(readFileSync(report, 'utf8'), '# Step 3 report\n'); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('done line: a command outside the test-runner allowlist is not run -> exit 2, nothing written', () => { const dir = repo(); const report = join(dir, 'step-3.report.md'); try { for (const cmd of ['echo done', 'node --test tests/bad.test.mjs || true', 'true']) { const r = run(['--cwd', dir, '--step', '3', '--cmd', cmd, '--report', report]); assert.equal(r.status, 2, `${cmd}: ${r.stdout}${r.stderr}`); } assert.equal(existsSync(report), false); assert.equal(run(['--cwd', dir, '--cmd', 'node --test tests/ok.test.mjs', '--report', report]).status, 2, 'no --step'); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('done line: --check finds the line for that step only', () => { const dir = repo(); const report = join(dir, 'step-3.report.md'); try { assert.equal(run(['--check', '--step', '3', '--report', report]).status, 1, 'no report file = not done'); writeFileSync(report, '# Step 3 report\nDONE step 3 (typed by hand)\n'); assert.equal(run(['--check', '--step', '3', '--report', report]).status, 1, 'a hand-typed line is not the script line'); // A well-formed line typed by hand, at a sha that is not the step's commit. writeFileSync(report, `# Step 3 report\nDONE step 3: \`node --test tests/ok.test.mjs\` exit 0 at ${'0'.repeat(40)} 2026-10-06T12:00:00.000Z\n`); const head = sh(dir, 'git', ['rev-parse', 'HEAD']); assert.equal(run(['--check', '--step', '3', '--report', report, '--commit', head]).status, 1, 'the line must be at the step commit'); run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/ok.test.mjs', '--report', report]); assert.equal(run(['--check', '--step', '3', '--report', report]).status, 0); assert.equal(run(['--check', '--step', '3', '--report', report, '--commit', head]).status, 0); assert.equal(run(['--check', '--step', '4', '--report', report]).status, 1); } finally { rmSync(dir, { recursive: true, force: true }); } }); // ---- M11 sibling: only a full 40- or 64-hex commit id ---- test('[M11] sibling: the done line accepts a 64-hex commit id', () => { // A SHA-256 repository writes a 64-hex HEAD into the line; --check must read it. const dir = mkdtempSync(join(tmpdir(), 'done-line-sha256-')); const report = join(dir, 'step-3.report.md'); const sha = '0123abcd'.padEnd(64, 'e'); try { writeFileSync(report, `# Step 3 report\nDONE step 3: \`node --test tests/ok.test.mjs\` exit 0 at ${sha} 2026-10-06T12:00:00.000Z\n`); const plain = run(['--check', '--step', '3', '--report', report]); assert.equal(plain.status, 0, `a 64-hex line is a done line: ${plain.stdout}${plain.stderr}`); const bound = run(['--check', '--step', '3', '--report', report, '--commit', sha]); assert.equal(bound.status, 0, `the line at that 64-hex commit: ${bound.stdout}${bound.stderr}`); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('[M11] sibling: the done line refuses a 7-hex id given with --commit, naming 40 or 64 hex', () => { // A short id can also be a branch name (DJ3); a line typed at a short id // must not pass the check bound to that short id. const dir = mkdtempSync(join(tmpdir(), 'done-line-short-')); const report = join(dir, 'step-3.report.md'); try { writeFileSync(report, '# Step 3 report\nDONE step 3: `node --test tests/ok.test.mjs` exit 0 at deadbee 2026-10-06T12:00:00.000Z\n'); const r = run(['--check', '--step', '3', '--report', report, '--commit', 'deadbee']); assert.equal(r.status, 2, `a 7-hex --commit could not be checked (exit 2, never green): ${r.stdout}${r.stderr}`); assert.match(r.stdout + r.stderr, /40 or 64 hex/, `the refusal names 40 or 64 hex: ${r.stdout}${r.stderr}`); } finally { rmSync(dir, { recursive: true, force: true }); } });