- park-attempt-files: parks in the git dir (outside the work tree, so Sub-step C.2 never registers a parked file again), reads a path with a space, names a new file under a header it cannot read, parks a symlink, never overwrites a parked file, and exits 1 while a file the attempt made is still in the tree; the upward diff is saved in one form whatever the git config. - brief-validator: BRIEF_SC_NOT_RUNNABLE only when a later backticked span is a command; a criterion that only names a flag or a path stays prose (Handover 1 says so now). - review ids: counted over the final list (runContract), as fill-finding-ids counts review.md; trimmed_count matches rows by id; engine-findings and fidelity-diff use computeFindingIds; the --workflow dedup keeps one source's different findings apart, the decision-23 put-back stays one copy per triplet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
7.8 KiB
JavaScript
196 lines
7.8 KiB
JavaScript
// 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, symlinkSync } 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, diff = DIFF) {
|
|
writeFileSync(join(dir, '.git', 'upward.diff'), diff);
|
|
return spawnSync(process.execPath, [CLI, '--diff', join(dir, '.git', 'upward.diff'), '--step', '1', '--attempt', '1', '--cwd', dir], { encoding: 'utf8' });
|
|
}
|
|
|
|
// Parked outside the work tree, in the git dir, so a later `git ls-files --others` never lists them.
|
|
const parkedPath = (dir, path) => join(dir, '.git', 'voyage', 'parked', 'step-1-attempt-1', path);
|
|
|
|
test('park: the step\'s untracked new files move into the git dir 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(parkedPath(dir, 'tests/batch.sh'), 'utf8'), 'stale | cat\n', 'and kept, never deleted');
|
|
assert.equal(git(dir, 'ls-files', '--others', '--exclude-standard').trim(), '', 'nothing parked shows up as a new file');
|
|
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 r = run(dir, DIFF.replaceAll('tests/batch.sh', '../outside.sh'));
|
|
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: a path with a space is read and parked', () => {
|
|
const dir = repo();
|
|
try {
|
|
mkdirSync(join(dir, 'tests'));
|
|
writeFileSync(join(dir, 'tests/my batch.sh'), 'stale\n');
|
|
const r = run(dir, DIFF.replaceAll('tests/batch.sh', 'tests/my batch.sh'));
|
|
assert.equal(r.status, 0, r.stderr);
|
|
assert.ok(JSON.parse(r.stdout).parked.includes('tests/my batch.sh'), r.stdout);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('park: a new file under a header it cannot read is named, and exit 1 stops the run (review of 3238327)', () => {
|
|
const dir = repo();
|
|
try {
|
|
mkdirSync(join(dir, 'tests'));
|
|
writeFileSync(join(dir, 'tests/batch.sh'), 'stale\n');
|
|
const r = run(dir, DIFF.replace('diff --git a/tests/batch.sh b/tests/batch.sh', 'diff --git c/tests/batch.sh w/tests/batch.sh'));
|
|
assert.equal(r.status, 1, r.stdout);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.ok(out.kept.some((k) => /header not read/.test(k.reason)), r.stdout);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('park: an attempt file still on disk because its target exists is exit 1, and the parked copy is never overwritten', () => {
|
|
const dir = repo();
|
|
try {
|
|
mkdirSync(join(dir, 'tests'));
|
|
writeFileSync(join(dir, 'tests/batch.sh'), 'second\n');
|
|
mkdirSync(dirname(parkedPath(dir, 'tests/batch.sh')), { recursive: true });
|
|
writeFileSync(parkedPath(dir, 'tests/batch.sh'), 'first\n');
|
|
const r = run(dir);
|
|
assert.equal(r.status, 1, r.stdout);
|
|
assert.equal(readFileSync(parkedPath(dir, 'tests/batch.sh'), 'utf8'), 'first\n');
|
|
assert.equal(existsSync(join(dir, 'tests/batch.sh')), true);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('park: a symlink the attempt made is parked, not called absent', () => {
|
|
const dir = repo();
|
|
try {
|
|
mkdirSync(join(dir, 'tests'));
|
|
symlinkSync('nowhere', join(dir, 'tests/batch.sh'));
|
|
const r = run(dir, DIFF.replace('new file mode 100755', 'new file mode 120000'));
|
|
assert.equal(r.status, 0, r.stdout);
|
|
assert.ok(JSON.parse(r.stdout).parked.includes('tests/batch.sh'), 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'), '--step', '1', '--attempt', '1', '--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 + 1600);
|
|
assert.match(para, /lib\/util\/park-attempt-files\.mjs/);
|
|
assert.match(para, /step-\{N\}-upward\.diff/);
|
|
assert.match(para, /never deleted/);
|
|
assert.match(para, /Exit 1 means a file the attempt made is still in the tree/);
|
|
});
|