// tests/lib/park-attempt-files.test.mjs // A step sent back to the plan leaves its new files on disk; before the fresh // build of the re-planned step they are moved into the report dir, never // deleted (voyage-lekerepo trial 5, finding 4). import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const CLI = join(ROOT, 'lib/util/park-attempt-files.mjs'); function git(cwd, ...args) { const r = spawnSync('git', args, { cwd, encoding: 'utf8' }); assert.equal(r.status, 0, r.stderr); return r.stdout; } function repo() { const dir = mkdtempSync(join(tmpdir(), 'voyage-park-')); git(dir, 'init', '-q'); git(dir, 'config', 'user.email', 't@example.invalid'); git(dir, 'config', 'user.name', 't'); mkdirSync(join(dir, 'bin')); writeFileSync(join(dir, 'bin/slug.mjs'), 'old\n'); git(dir, 'add', 'bin/slug.mjs'); git(dir, 'commit', '-q', '-m', 'base'); return dir; } // The shape `git diff {start_sha}` gives with the new files registered --intent-to-add. const DIFF = [ 'diff --git a/bin/slug.mjs b/bin/slug.mjs', 'index 3367afd..e69de29 100644', '--- a/bin/slug.mjs', '+++ b/bin/slug.mjs', '@@ -1 +1 @@', '-old', '+new', 'diff --git a/tests/batch.sh b/tests/batch.sh', 'new file mode 100755', 'index 0000000..1111111', '--- /dev/null', '+++ b/tests/batch.sh', '@@ -0,0 +1 @@', '+cat', 'diff --git a/tests/cli-stdin.test.mjs b/tests/cli-stdin.test.mjs', 'new file mode 100644', '--- /dev/null', '+++ b/tests/cli-stdin.test.mjs', '@@ -0,0 +1 @@', '+t', '', ].join('\n'); function run(dir, extra = []) { writeFileSync(join(dir, 'upward.diff'), DIFF); return spawnSync(process.execPath, [CLI, '--diff', join(dir, 'upward.diff'), '--to', join(dir, 'parked'), '--cwd', dir, ...extra], { encoding: 'utf8' }); } test('park: the step\'s untracked new files move into --to with their paths; tracked files stay', () => { const dir = repo(); try { mkdirSync(join(dir, 'tests')); writeFileSync(join(dir, 'tests/batch.sh'), 'stale | cat\n'); writeFileSync(join(dir, 'tests/cli-stdin.test.mjs'), 'stale test\n'); writeFileSync(join(dir, 'bin/slug.mjs'), 'new\n'); const r = run(dir); assert.equal(r.status, 0, r.stderr); const out = JSON.parse(r.stdout); assert.deepEqual(out.parked, ['tests/batch.sh', 'tests/cli-stdin.test.mjs']); assert.equal(existsSync(join(dir, 'tests/batch.sh')), false, 'the stale file is out of the tree'); assert.equal(readFileSync(join(dir, 'parked/tests/batch.sh'), 'utf8'), 'stale | cat\n', 'and kept, never deleted'); assert.equal(readFileSync(join(dir, 'bin/slug.mjs'), 'utf8'), 'new\n', 'a tracked file is not the helper\'s to move'); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('park: a new file that is tracked now, or already gone, is kept and named, never moved', () => { const dir = repo(); try { mkdirSync(join(dir, 'tests')); writeFileSync(join(dir, 'tests/batch.sh'), 'committed by the operator\n'); git(dir, 'add', 'tests/batch.sh'); git(dir, 'commit', '-q', '-m', 'operator'); const r = run(dir); assert.equal(r.status, 0, r.stderr); const out = JSON.parse(r.stdout); assert.deepEqual(out.parked, []); assert.deepEqual(out.kept.map((k) => k.path), ['tests/batch.sh', 'tests/cli-stdin.test.mjs']); assert.match(out.kept[0].reason, /tracked/); assert.match(out.kept[1].reason, /not on disk/); assert.equal(existsSync(join(dir, 'tests/batch.sh')), true); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('park: a path that leaves the repository is refused, never moved', () => { const dir = repo(); try { const evil = DIFF.replaceAll('tests/batch.sh', '../outside.sh'); writeFileSync(join(dir, 'upward.diff'), evil); const r = spawnSync(process.execPath, [CLI, '--diff', join(dir, 'upward.diff'), '--to', join(dir, 'parked'), '--cwd', dir], { encoding: 'utf8' }); assert.equal(r.status, 0, r.stderr); const out = JSON.parse(r.stdout); assert.ok(out.kept.some((k) => k.path === '../outside.sh' && /outside the repository/.test(k.reason)), r.stdout); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('park: an unreadable diff or missing arguments exit 2 and move nothing', () => { const dir = repo(); try { const r = spawnSync(process.execPath, [CLI, '--diff', join(dir, 'nope.diff'), '--to', join(dir, 'parked'), '--cwd', dir], { encoding: 'utf8' }); assert.equal(r.status, 2); assert.equal(spawnSync(process.execPath, [CLI], { encoding: 'utf8' }).status, 2); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('trekexecute: a re-planned step parks the earlier attempt\'s new files before the fresh build', () => { const md = readFileSync(join(ROOT, 'commands/trekexecute.md'), 'utf8'); const at = md.indexOf('A step at `fix_rounds` 4 on `--resume` was re-planned'); assert.ok(at > 0); const para = md.slice(at, at + 1200); assert.match(para, /lib\/util\/park-attempt-files\.mjs/); assert.match(para, /step-\{N\}-upward\.diff/); assert.match(para, /never deleted/); });