// tests/hooks/merge-evidence.test.mjs // [MEV] fix round 4.5b2 part B, Step 11: the guard's merge question says how // the run was tested and how it is undone. Phase 8 of /trekexecute writes an // evidence file (lib/review/merge-evidence.mjs write) under the git common // dir, named by the head sha of the branch being merged; the Bash guard // (hooks/scripts/pre-bash-executor.mjs) reads it with hooks/lib/merge-evidence.mjs // into two lines of the question it already asks: // Tested by: End-to-end `` exit ; Verification /; review at . // Undone by: `git revert -m 1 ` (two-way). | (one-way: ). | (door not stated). // Fail-safe: the `ask` decision is computed before the reader runs and never // changes; a missing, stale, foreign or hostile file, or a field of the wrong // type, reads "no run evidence" or "not measured", never a green line. // The decision is `ask` at the base too, so it is a Control in each case, // never a tagged case of its own. Every child gets childEnv(); every fixture // that reaches the verdict gate keeps HEAD at the reviewed commit (R6). import { after, test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { appendFileSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, utimesSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, isAbsolute, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { runHookWithEnv } from '../helpers/hook-helper.mjs'; import { childEnv } from '../helpers/child-env.mjs'; import { pluginCopy } from './guard-helpers.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const PRE_BASH = join(ROOT, 'hooks', 'scripts', 'pre-bash-executor.mjs'); const READER = join(ROOT, 'hooks', 'lib', 'merge-evidence.mjs'); const WRITER = join(ROOT, 'lib', 'review', 'merge-evidence.mjs'); // The permission mode in which Claude Code shows no prompts, read from the // guard's own constant: this file only feeds the guard its own value back. const NO_PROMPT_MODE = (() => { const m = readFileSync(PRE_BASH, 'utf8').match(/const NO_PROMPT_MODE = '([A-Za-z]+)';/); assert.ok(m, 'the guard names its no-prompt permission mode'); return m[1]; })(); const UNDONE_TWO_WAY = 'Undone by: `git revert -m 1 ` (two-way).'; const UNDONE_NOT_STATED = 'Undone by: `git revert -m 1 ` (door not stated).'; const NO_EVIDENCE = 'Tested by: no run evidence.\nUndone by: no run evidence.'; const UNREADABLE = 'Tested by: not measured (the run evidence file could not be read).\nUndone by: not measured.'; const made = []; after(() => { for (const b of made) rmSync(b, { recursive: true, force: true }); }); function g(cwd, ...args) { const r = spawnSync('git', args, { cwd, encoding: 'utf8', env: childEnv() }); if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`); return r.stdout.trim(); } const run = (args, opts = {}) => spawnSync(process.execPath, args, { encoding: 'utf8', env: childEnv(), ...opts }); const parse = (r) => { try { return JSON.parse(r.stdout); } catch { return {}; } }; /** * A throwaway repo on `main` (README) with a branch `feat` one commit ahead * (a.mjs), checked out on main. No hooks, no signing. */ function mevRepo() { const base = realpathSync(mkdtempSync(join(tmpdir(), 'merge-evidence-mev-'))); made.push(base); const repo = join(base, 'repo'); mkdirSync(repo, { recursive: true }); mkdirSync(join(base, 'hooks-none')); g(repo, 'init', '-q', '-b', 'main'); appendFileSync(join(repo, '.git', 'config'), '[user]\n\tname = Test\n\temail = test@example.invalid\n' + `[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "${join(base, 'hooks-none')}"\n`); writeFileSync(join(repo, 'README.md'), '# t\n'); g(repo, 'add', 'README.md'); g(repo, 'commit', '-q', '-m', 'init'); const mainSha = g(repo, 'rev-parse', 'HEAD'); g(repo, 'checkout', '-q', '-b', 'feat'); writeFileSync(join(repo, 'a.mjs'), 'export const a = 1;\n'); g(repo, 'add', 'a.mjs'); g(repo, 'commit', '-q', '-m', 'feat: the work'); const featSha = g(repo, 'rev-parse', 'HEAD'); g(repo, 'checkout', '-q', 'main'); const common = join(repo, '.git'); const evDir = join(common, 'voyage', 'merge-evidence'); return { base, repo, mainSha, featSha, common, evDir, evPath: (sha = featSha) => join(evDir, `${sha}.json`), /** Write an evidence file by hand (`raw` text, or an object as JSON). */ put: (body, sha = featSha) => { mkdirSync(evDir, { recursive: true }); writeFileSync(join(evDir, `${sha}.json`), typeof body === 'string' ? body : JSON.stringify(body, null, 2)); }, }; } /** A complete evidence object for `sha` on branch feat. */ function evidence(sha, over = {}) { return { v: 1, branch: 'feat', branch_head_sha: sha, written_at: '2026-11-02T10:00:00.000Z', end_to_end: { command: '/bin/bash tests/e2e.sh', exit: 0 }, verification: { passed: 5, total: 5 }, review: { verdict: 'ALLOW', sha }, door: { type: 'two-way', text: 'a revert restores the old gate' }, ...over, }; } const tested = (sha, { cmd = '/bin/bash tests/e2e.sh', exit = '0', ver = '5/5', verdict = 'ALLOW' } = {}) => `Tested by: End-to-end \`${cmd}\` exit ${exit}; Verification ${ver}; review ${verdict} at ${sha.slice(0, 8)}.`; /** The guard on a Bash call, run in `cwd`; `script` and `env` pick a plugin copy and its environment. */ async function ask(cwd, { command = 'git merge --no-ff feat', mode, script = PRE_BASH, env = {} } = {}) { const input = { hook_event_name: 'PreToolUse', tool_name: 'Bash', cwd, tool_input: { command } }; if (mode) input.permission_mode = mode; const r = await runHookWithEnv(script, input, env); let out = null; try { out = JSON.parse(String(r.stdout).trim()); } catch { out = null; } return { ...r, decision: out?.hookSpecificOutput?.permissionDecision ?? null, reason: String(out?.hookSpecificOutput?.permissionDecisionReason ?? ''), }; } /** A minimal ALLOW review of `sha` (shape copied from tests/lib/review-answers.test.mjs `review`). */ function review(sha) { const id = '1'.padStart(40, 'a'); const json = JSON.stringify({ verdict: 'ALLOW', counts: { BLOCKER: 0, MAJOR: 0, MINOR: 1, SUGGESTION: 0 }, allow_blocked_by: [], findings: [{ id, severity: 'MINOR', rule_key: 'MISSING_TEST', file: 'a.mjs', line: 1, title: 't', detail: 'd', recommended_action: 'Add a test.' }], }, null, 2); return `--- type: trekreview review_version: "1.1" task: "t" slug: s project_dir: .claude/projects/2026-11-02-s/ brief_path: .claude/projects/2026-11-02-s/brief.md scope_sha_end: ${sha} reviewed_files_count: 1 verdict: ALLOW findings: - ${id} --- # Review: t ## Executive Summary x ## Coverage | File | Treatment | Reason | |------|-----------|--------| | a.mjs | summary-only | default | ## Remediation Summary x \`\`\`json ${json} \`\`\` `; } /** A plan with an End-to-end step and an optional `## Doors` list (lines). */ function plan({ e2e = '/bin/bash tests/e2e-x.sh', doors } = {}) { return `# Plan > Generated by trekplan v6.5.0 on 2026-11-02 — \`plan_version: 1.9\` ## Implementation Plan ### Step 1: the work - **Files:** \`a.mjs\` - **Verify:** \`node --test tests/a.test.mjs\` → expected: pass ### Step 2: End to end - **End-to-end:** \`${e2e}\` → expected: \`e2e: ok\`, exit 0. ${doors ? `\n## Doors\n\n${doors.join('\n')}\n` : ''} ## Verification \`\`\`bash ${e2e} \`\`\` `; } /** A project folder outside the repo; files written only when given. */ function project(fx, { planText, progress, reviewText } = {}) { const dir = mkdtempSync(join(fx.base, 'project-')); if (planText !== undefined) writeFileSync(join(dir, 'plan.md'), planText); if (progress !== undefined) writeFileSync(join(dir, 'progress.json'), JSON.stringify(progress, null, 2)); if (reviewText !== undefined) writeFileSync(join(dir, 'review.md'), reviewText); return dir; } const PROGRESS = { schema_version: '1', status: 'completed', end_to_end: 'verified', plan_verification: { status: 'passed', summary: { total: 7, passed: 7, failed: 0, blocked: 0, unrunnable: 0 }, failed_criteria: [] }, }; /** The evidence writer's CLI; the module must exist (so a missing one fails on this assertion, not in the child). */ function write(repo, branch, proj) { assert.ok(existsSync(WRITER), 'lib/review/merge-evidence.mjs must exist'); return run([WRITER, 'write', '--repo', repo, '--branch', branch, '--project', proj, '--json']); } // ---- [MEV] the lines --------------------------------------------------------------- test('[MEV] the lines: the merge question holds the Tested by and Undone by lines from an evidence file of the merged branch head', async () => { const fx = mevRepo(); fx.put(evidence(fx.featSha)); const r = await ask(fx.repo); assert.equal(r.decision, 'ask', `Control: the merge into main is asked about: ${r.stdout}${r.stderr}`); assert.ok(r.reason.includes(`${tested(fx.featSha)}\n${UNDONE_TWO_WAY}`), `the two lines, in order:\n${r.reason}`); // Control: the question the guard already asks is still there, the lines after it. assert.ok(r.reason.startsWith('Voyage asks before this command (P12 approval guard): merge into main.'), r.reason); assert.ok(r.reason.indexOf('Way forward:') < r.reason.indexOf('Tested by:'), r.reason); }); // ---- [MEV] the door ---------------------------------------------------------------- test('[MEV] the door: a one-way door in the plan\'s ## Doors prints it; none prints "door not stated"', async () => { const fx = mevRepo(); const oneWay = project(fx, { planText: plan({ doors: ['- one-way: v1 files no longer read'] }), progress: PROGRESS }); const w = write(fx.repo, 'feat', oneWay); assert.equal(w.status, 0, `writer: ${w.stdout}${w.stderr}`); const r = await ask(fx.repo); assert.equal(r.decision, 'ask', `Control: ${r.stdout}${r.stderr}`); assert.ok(r.reason.includes('Undone by: `git revert -m 1 ` (one-way: v1 files no longer read).'), r.reason); const none = project(fx, { planText: plan(), progress: PROGRESS }); const w2 = write(fx.repo, 'feat', none); assert.equal(w2.status, 0, `writer: ${w2.stdout}${w2.stderr}`); const n = await ask(fx.repo); assert.equal(n.decision, 'ask', `Control: ${n.stdout}${n.stderr}`); assert.ok(n.reason.includes(UNDONE_NOT_STATED), n.reason); // Control: with no door in the plan, the CHANGELOG's first section decides when it holds exactly one door line. writeFileSync(join(fx.repo, 'CHANGELOG.md'), '# Changelog\n\n## Unreleased\n\n### Doors\n\n- two-way: the new lines can be reverted\n\n## v1.0.0\n\n### Doors\n\n- one-way: old\n'); assert.equal(write(fx.repo, 'feat', none).status, 0); assert.ok((await ask(fx.repo)).reason.includes(UNDONE_TWO_WAY), 'Control: the CHANGELOG door line'); writeFileSync(join(fx.repo, 'CHANGELOG.md'), '# Changelog\n\n## Unreleased\n\n### Doors\n\n- two-way: a\n- one-way: b\n'); assert.equal(write(fx.repo, 'feat', none).status, 0); assert.ok((await ask(fx.repo)).reason.includes(UNDONE_NOT_STATED), 'Control: two door lines in the CHANGELOG state no door'); }); // ---- [MEV] fail-safe --------------------------------------------------------------- test('[MEV] fail-safe: no file, a stale or foreign file, an unreadable field or a reader that will not load says "no run evidence" or "not measured", and still asks', async () => { const fx = mevRepo(); // No file. const none = await ask(fx.repo); assert.equal(none.decision, 'ask', `Control: ${none.stdout}${none.stderr}`); assert.ok(none.reason.includes(NO_EVIDENCE), `no file: ${none.reason}`); // Stale: the file at the branch head's name holds another head. fx.put(evidence(fx.mainSha)); const stale = await ask(fx.repo); assert.equal(stale.decision, 'ask', `Control: ${stale.stdout}${stale.stderr}`); assert.ok(stale.reason.includes(NO_EVIDENCE), `stale head sha: ${stale.reason}`); assert.ok(!stale.reason.includes('Tested by: End-to-end'), stale.reason); // Stale: evidence for the old head, then the branch moved on. fx.put(evidence(fx.featSha)); g(fx.repo, 'checkout', '-q', 'feat'); writeFileSync(join(fx.repo, 'b.mjs'), 'export const b = 2;\n'); g(fx.repo, 'add', 'b.mjs'); g(fx.repo, 'commit', '-q', '-m', 'feat: more'); g(fx.repo, 'checkout', '-q', 'main'); const moved = await ask(fx.repo); assert.equal(moved.decision, 'ask', `Control: ${moved.stdout}${moved.stderr}`); assert.ok(moved.reason.includes(NO_EVIDENCE), `the branch moved on: ${moved.reason}`); const head = g(fx.repo, 'rev-parse', 'feat'); // Foreign: another format version, or another branch's name. fx.put(evidence(head, { v: 2 }), head); assert.ok((await ask(fx.repo)).reason.includes(NO_EVIDENCE), 'a file of another version'); fx.put(evidence(head, { branch: 'other' }), head); assert.ok((await ask(fx.repo)).reason.includes(NO_EVIDENCE), 'a file for another branch'); // Unreadable fields: each reads "not measured", none prints a number or a verdict. fx.put(evidence(head, { end_to_end: { command: 42, exit: '0' }, verification: { passed: '7', total: 7 }, review: { verdict: 'GREEN', sha: 'abc' }, door: { type: 'sideways', text: 'x' }, }), head); const bad = await ask(fx.repo); assert.equal(bad.decision, 'ask', `Control: ${bad.stdout}${bad.stderr}`); assert.ok(bad.reason.includes('Tested by: End-to-end command not measured exit not measured; Verification not measured; review not measured at not measured.'), bad.reason); assert.ok(bad.reason.includes(UNDONE_NOT_STATED), bad.reason); // Missing fields read the same. fx.put({ v: 1, branch: 'feat', branch_head_sha: head }, head); const missing = await ask(fx.repo); assert.ok(missing.reason.includes('Tested by: End-to-end command not measured exit not measured; Verification not measured; review not measured at not measured.'), missing.reason); // More passed than total is no count. fx.put(evidence(head, { verification: { passed: 9, total: 7 } }), head); assert.ok((await ask(fx.repo)).reason.includes('Verification not measured;'), 'passed > total'); // Non-printable and non-ASCII characters print as `?`. fx.put(evidence(head, { end_to_end: { command: 'e2e\u0007ü\nrm', exit: 0 } }), head); const odd = await ask(fx.repo); assert.ok(odd.reason.includes('End-to-end `e2e???rm` exit 0;'), odd.reason); // Bad JSON. fx.put('{"v": 1, "branch": ', head); const json = await ask(fx.repo); assert.equal(json.decision, 'ask', `Control: ${json.stdout}${json.stderr}`); assert.ok(json.reason.includes(UNREADABLE), `bad JSON: ${json.reason}`); // A merge with no single branch named reads "no run evidence". fx.put(evidence(head), head); for (const command of ['git merge --no-ff', 'git merge --no-ff feat other', 'git merge -m feat']) { const r = await ask(fx.repo, { command }); assert.equal(r.decision, 'ask', `Control: ${command}`); assert.ok(r.reason.includes(NO_EVIDENCE), `${command}: ${r.reason}`); } // Control: option values are skipped, so the branch is still found. const opts = await ask(fx.repo, { command: 'git merge --no-ff -s ort -X ours feat' }); assert.ok(opts.reason.includes(tested(head)), opts.reason); // A reader that will not load: the question still asks and says so. const plugin = pluginCopy({ 'hooks/lib/merge-evidence.mjs': 'missing' }); made.push(plugin); const gone = await ask(fx.repo, { script: join(plugin, 'hooks', 'scripts', 'pre-bash-executor.mjs') }); assert.equal(gone.decision, 'ask', `Control: ${gone.stdout}${gone.stderr}`); assert.ok(gone.reason.includes('(run evidence not loaded)'), gone.reason); }); test('[MEV] a hostile evidence path still asks: a FIFO, a symlink and a 10 MiB file read "not measured"', async () => { const fx = mevRepo(); // A good file elsewhere: followed, the symlink would print the green line. const elsewhere = join(fx.base, 'good.json'); writeFileSync(elsewhere, JSON.stringify(evidence(fx.featSha))); mkdirSync(fx.evDir, { recursive: true }); symlinkSync(elsewhere, fx.evPath()); const link = await ask(fx.repo); assert.equal(link.decision, 'ask', `Control: ${link.stdout}${link.stderr}`); assert.ok(link.reason.includes(UNREADABLE), `a symlink: ${link.reason}`); assert.ok(!link.reason.includes(tested(fx.featSha)), link.reason); rmSync(fx.evPath()); const fifo = spawnSync('mkfifo', [fx.evPath()], { encoding: 'utf8', env: childEnv() }); assert.equal(fifo.status, 0, `mkfifo: ${fifo.stderr}`); const pipe = await ask(fx.repo); assert.equal(pipe.decision, 'ask', `Control: a FIFO does not hang the guard: ${pipe.code} ${pipe.stdout}${pipe.stderr}`); assert.ok(pipe.reason.includes(UNREADABLE), `a FIFO: ${pipe.reason}`); rmSync(fx.evPath()); // Valid JSON with the good fields, padded past 10 MiB. writeFileSync(fx.evPath(), JSON.stringify(evidence(fx.featSha)) + ' '.repeat(10 * 1024 * 1024)); const big = await ask(fx.repo); assert.equal(big.decision, 'ask', `Control: ${big.stdout}${big.stderr}`); assert.ok(big.reason.includes(UNREADABLE), `a 10 MiB file: ${big.reason}`); assert.ok(!big.reason.includes(tested(fx.featSha)), big.reason); // A directory at the evidence path. rmSync(fx.evPath()); mkdirSync(fx.evPath()); const dir = await ask(fx.repo); assert.equal(dir.decision, 'ask', `Control: ${dir.stdout}${dir.stderr}`); assert.ok(dir.reason.includes(UNREADABLE), `a directory: ${dir.reason}`); rmSync(fx.evPath(), { recursive: true }); // Control: unattended, the merge is still refused, evidence or not. fx.put(evidence(fx.featSha)); const unattended = await ask(fx.repo, { mode: NO_PROMPT_MODE }); assert.equal(unattended.code, 2, `Control: unattended refuses the merge: ${unattended.stdout}${unattended.stderr}`); assert.equal(unattended.decision, null, 'Control: no ask where no one can be asked'); assert.match(unattended.stderr, /BLOCKED: merge into main with no one to ask/); }); // ---- [MEV] round trip -------------------------------------------------------------- test('[MEV] round trip: a file written by merge-evidence.mjs write is read back by the guard into the same two lines', async () => { const fx = mevRepo(); const proj = project(fx, { planText: plan({ doors: ['- two-way: a revert restores the old gate'] }), progress: PROGRESS, reviewText: review(fx.featSha), }); // HEAD at the reviewed commit while the writer asks the verdict gate (R6). g(fx.repo, 'checkout', '-q', 'feat'); const w = write(fx.repo, 'feat', proj); assert.equal(w.status, 0, `writer: ${w.stdout}${w.stderr}`); const out = parse(w); assert.equal(out.written, true, w.stdout); assert.equal(out.path, fx.evPath(), w.stdout); const file = JSON.parse(readFileSync(fx.evPath(), 'utf8')); assert.equal(file.v, 1); assert.equal(file.branch, 'feat'); assert.equal(file.branch_head_sha, fx.featSha); assert.match(String(file.written_at), /^\d{4}-\d\d-\d\dT/); assert.deepEqual(file.end_to_end, { command: '/bin/bash tests/e2e-x.sh', exit: 0 }); assert.deepEqual(file.verification, { passed: 7, total: 7 }); assert.deepEqual(file.review, { verdict: 'ALLOW', sha: fx.featSha }); assert.deepEqual(file.door, { type: 'two-way', text: 'a revert restores the old gate' }); g(fx.repo, 'checkout', '-q', 'main'); const r = await ask(fx.repo); assert.equal(r.decision, 'ask', `Control: ${r.stdout}${r.stderr}`); assert.ok(r.reason.includes(`${tested(fx.featSha, { cmd: '/bin/bash tests/e2e-x.sh', ver: '7/7' })}\n${UNDONE_TWO_WAY}`), r.reason); // Control: an End-to-end that is not verified writes no exit; a review the gate does not pass writes no verdict. g(fx.repo, 'checkout', '-q', 'feat'); writeFileSync(join(proj, 'progress.json'), JSON.stringify({ ...PROGRESS, end_to_end: 'not-verified' })); writeFileSync(join(proj, 'review.md'), review(fx.mainSha)); assert.equal(write(fx.repo, 'feat', proj).status, 0); g(fx.repo, 'checkout', '-q', 'main'); const nv = await ask(fx.repo); assert.ok(nv.reason.includes('Tested by: End-to-end `/bin/bash tests/e2e-x.sh` exit not measured; Verification 7/7; review not measured at '), nv.reason); // Control: a branch name that is not a plain name is refused, and nothing is written. for (const branch of ['..feat', 'fe..at', '-feat', 'fe\u0001at', 'nope']) { const bad = write(fx.repo, branch, proj); assert.equal(bad.status, 1, `refused ${JSON.stringify(branch)}: ${bad.stdout}${bad.stderr}`); assert.equal(parse(bad).written, false, bad.stdout); assert.equal(typeof parse(bad).reason, 'string', bad.stdout); } }); // ---- [MEV] where the file lives ---------------------------------------------------- test('[MEV] the evidence file sits under the git common dir, and git status --porcelain in the fixture shows nothing new', async () => { const fx = mevRepo(); // The run in a linked worktree, main in the first one (Phase 8 merges with `git -C`). const wt = join(fx.base, 'wt'); g(fx.repo, 'worktree', 'add', '-q', wt, 'feat'); const proj = project(fx, { planText: plan(), progress: PROGRESS, reviewText: review(fx.featSha) }); const before = [g(fx.repo, 'status', '--porcelain', '--untracked-files=all'), g(wt, 'status', '--porcelain', '--untracked-files=all')]; const w = write(wt, 'feat', proj); assert.equal(w.status, 0, `writer: ${w.stdout}${w.stderr}`); const raw = g(wt, 'rev-parse', '--git-common-dir'); const common = realpathSync(isAbsolute(raw) ? raw : resolve(wt, raw)); const path = parse(w).path; assert.equal(realpathSync(path), join(common, 'voyage', 'merge-evidence', `${fx.featSha}.json`), w.stdout); assert.ok(!realpathSync(path).startsWith(`${realpathSync(wt)}/`), 'not in the worktree'); const afterStatus = [g(fx.repo, 'status', '--porcelain', '--untracked-files=all'), g(wt, 'status', '--porcelain', '--untracked-files=all')]; assert.deepEqual(afterStatus, before, 'git status shows nothing new in either tree'); // Control: the guard in main's tree finds the file the run's tree wrote. const r = await ask(fx.repo); assert.equal(r.decision, 'ask', `Control: ${r.stdout}${r.stderr}`); assert.ok(r.reason.includes(`${tested(fx.featSha, { cmd: '/bin/bash tests/e2e-x.sh', ver: '7/7' })}\n${UNDONE_NOT_STATED}`), r.reason); }); // ---- [MEV] the reader's architecture rule ------------------------------------------ /** What the reader must never hold: a process spawn, a `../../lib/` import, a non-ASCII byte. */ function scanReader(path) { const buf = readFileSync(path); const text = buf.toString('latin1'); const found = []; if (/child_process/.test(text)) found.push('child_process'); if (/['"`]\.\.\/\.\.\/lib\//.test(text)) found.push('../../lib/ import'); if (buf.some((b) => b > 0x7f)) found.push('non-ASCII byte'); return found; } test('[MEV] the reader is ASCII and spawns nothing', () => { assert.ok(existsSync(READER), 'hooks/lib/merge-evidence.mjs must exist'); assert.deepEqual(scanReader(READER), [], 'the real reader'); const dir = mkdtempSync(join(tmpdir(), 'merge-evidence-scan-')); made.push(dir); const copy = join(dir, 'merge-evidence.mjs'); const plants = [ ["import { spawnSync } from 'node:child_process';\n", 'child_process'], ["const x = require('node:child_process');\n", 'child_process'], ["import { parseDoors } from '../../lib/util/doors.mjs';\n", '../../lib/ import'], ['// café\n', 'non-ASCII byte'], ]; for (const [line, want] of plants) { copyFileSync(READER, copy); appendFileSync(copy, line); assert.deepEqual(scanReader(copy), [want], `planted: ${line.trim()}`); } copyFileSync(READER, copy); assert.deepEqual(scanReader(copy), [], 'the clean copy'); }); // ---- [MEV] the writer's clean-up --------------------------------------------------- test('[MEV] the writer removes an evidence file older than 30 days and keeps a fresh one', () => { const fx = mevRepo(); mkdirSync(fx.evDir, { recursive: true }); const old = join(fx.evDir, `${'a'.repeat(40)}.json`); const fresh = join(fx.evDir, `${'b'.repeat(40)}.json`); const notes = join(fx.evDir, 'notes.txt'); for (const p of [old, fresh, notes]) writeFileSync(p, '{}\n'); const longAgo = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000); utimesSync(old, longAgo, longAgo); utimesSync(notes, longAgo, longAgo); const w = write(fx.repo, 'feat', project(fx, { planText: plan(), progress: PROGRESS })); assert.equal(w.status, 0, `writer: ${w.stdout}${w.stderr}`); assert.ok(!existsSync(old), 'the evidence file older than 30 days is removed'); assert.ok(existsSync(fresh), 'the fresh evidence file is kept'); assert.ok(existsSync(notes), 'Control: a file not named .json is never removed'); assert.ok(existsSync(fx.evPath()), 'the new evidence file is written'); });