// tests/lib/fix-round-4-5b-part-2-prose.test.mjs // Prose pins for fix round 4-5b, part 2 (plan // .claude/projects/2026-10-27-fix-round-4-5b-part-2, decision 88): the fixes // that are text a session reads. The tags in brackets are the part-2 ids from // the round's brief; each tagged test fails on an assertion at 52c382c, the // round's start point. The keep-behaviour checks sit inside the tagged test as // "Control:" assertions. // DLC /trekexecute's Sub-step G says what `{hash}` is: the full commit id // from `git rev-parse HEAD` after the step's last commit, never the short // id `git commit` prints (done-line --check refuses a short id with // exit 2). The progress line stores that full id, and Handover 5's // `commit` row says so. // No static import of a new export: the file must load at 52c382c. // Fix the SOURCE a pin guards, not the pin. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const readCache = new Map(); /** A repo file's text; the file must exist. Read from disk once per run. */ function read(rel) { if (!readCache.has(rel)) { assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`); readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8')); } return readCache.get(rel); } /** Whitespace (line breaks too) collapsed to one space. */ const flat = (s) => s.replace(/\s+/g, ' ').trim(); /** The text from `heading` up to the next heading of the same or a higher level; the heading must exist. */ function section(text, heading) { const at = text.indexOf(heading); assert.ok(at >= 0, `missing heading "${heading}"`); const level = heading.match(/^#+/)[0].length; const rest = text.slice(at + heading.length); const next = rest.search(new RegExp(`\\n#{1,${level}} `)); return heading + (next >= 0 ? rest.slice(0, next) : rest); } const EXEC = 'commands/trekexecute.md'; const CONTRACTS = 'docs/HANDOVER-CONTRACTS.md'; // --- DLC --------------------------------------------------------------------------- test('[DLC] Sub-step G records the full commit id from git rev-parse HEAD after the checkpoint', () => { const g = section(read(EXEC), '#### Sub-step G — Checkpoint, done line, unlock'); const gFlat = flat(g); // Red: the checkpoint says how {hash} is read, before the done line runs. const checkpointAt = g.indexOf('**Checkpoint.**'); const doneLineAt = g.indexOf('**The done line**'); assert.ok(checkpointAt >= 0 && doneLineAt > checkpointAt, 'Sub-step G must hold **Checkpoint.** before **The done line**'); const revParseAt = g.indexOf('`git rev-parse HEAD`'); assert.ok(revParseAt > checkpointAt && revParseAt < doneLineAt, 'Sub-step G must name `git rev-parse HEAD` in the checkpoint item, after **Checkpoint.** and before the done-line check'); assert.ok(gFlat.includes('full commit id'), 'Sub-step G must say {hash} is the full commit id'); assert.ok(gFlat.includes('never the short id'), 'Sub-step G must say {hash} is never the short id `git commit` prints'); assert.ok(gFlat.includes('Read it again after any later commit of the step'), 'Sub-step G must say the id is read again after a later commit of the step'); const commitLines = g.split('\n').filter((l) => l.includes('`steps.{N}.commit = {hash}`')); assert.ok(commitLines.length > 0, 'Sub-step G must hold the `steps.{N}.commit = {hash}` progress line'); for (const l of commitLines) { assert.ok(l.includes('(the full commit id)'), `the progress line must say it stores the full commit id: ${l}`); } const handover5 = section(read(CONTRACTS), '## Handover 5 '); const rows = handover5.split('\n').filter((l) => l.startsWith('| `commit` |')); assert.equal(rows.length, 1, `Handover 5 must hold exactly one \`commit\` row; found ${rows.length}`); assert.ok(rows[0].includes('full commit id'), `the Handover 5 \`commit\` row must say "full commit id": ${rows[0]}`); // Control: the done-line check keeps its literal, Phase 7.5 keeps reading the // stored id, and Sub-step G asks for no abbreviated id anywhere. assert.ok(g.includes('done-line.mjs --check --step {N} --report {report_path} --commit {hash}'), 'Control: Sub-step G keeps the done-line --check literal with --commit {hash}'); const p75 = section(read(EXEC), '## Phase 7.5 — Manifest audit (independent)'); assert.ok(p75.includes('--commit {steps.N.commit}'), 'Control: Phase 7.5 keeps --commit {steps.N.commit}'); assert.doesNotMatch(g, /--short|%h|--abbrev/, 'Control: Sub-step G must not ask for an abbreviated commit id'); });