* test(#3144): bound the git/worktree cluster onto the process seam Migrates 180 unbounded sync spawn sites across 19 files. Every previously unbounded call now carries an explicit timeout with a comment giving the number and why. The migration is not a callee swap. execSync and execFileSync throw on a non-zero exit and the seam never does, so each site was classified first: sites that rely on the throw route to gitOrThrow, and sites that already read .status to detect an EXPECTED non-zero -- an intended cherry-pick conflict, a rev-parse outside a repo driving a skip -- route to the never-throwing runGit instead, which would otherwise throw on exactly the exit being probed for. Two same-named git() helpers in worktree-cleanup.test.cjs have different return contracts, one trimmed and one raw; both are preserved rather than unified. Collapses five hand-rolled throw wrappers onto one throwIfFailed in git-fixture.cjs, which gitOrThrow now also uses so the shape cannot drift. Allowlist drops 139 to 120; BASELINE lowered to match. * test(#3144): fix pre-PR review findings Documents throwIfFailed in the CONTEXT.md glossary and CONTRIBUTING.md -- it became the shared throw mechanism without either doc naming it. Routes the sixth and seventh hand-rolled copies of the throw shape through throwIfFailed (worktree-baseref-install, worktree-safety-reap); the first consolidation missed both. Converts ci-rebase-check's 8 fixture-setup calls from unchecked runGit to gitOrThrow so a failed setup step aborts where it fails rather than surfacing later as a confusing failure against the wrong subject. Adds 12 direct unit tests for throwIfFailed, which until now was only exercised transitively. Splits verify.test.cjs's non-git grep/sed bound off GIT_TIMEOUT_MS. --------- Co-authored-by: sim <sim@local>
105 lines
4.1 KiB
JavaScript
105 lines
4.1 KiB
JavaScript
/**
|
|
* Regression test for #2014: gsd-tools commit --files silently deletes
|
|
* planning files when a filename passed via --files does not exist on disk.
|
|
*
|
|
* Prior to this fix, when --files STATE.md was passed and STATE.md did not
|
|
* exist on disk, the code called `git rm --cached --ignore-unmatch STATE.md`
|
|
* which staged and committed a deletion. The caller passed explicit --files
|
|
* expecting only those specific files to be staged -- missing files should
|
|
* be skipped, not deleted.
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { createTempGitProject, cleanup, runGsdTools } = require('./helpers.cjs');
|
|
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
|
|
|
// 15000ms: git plumbing (add/commit/diff) on a small mkdtemp fixture repo —
|
|
// far over any observed duration for that class of call.
|
|
const GIT_TIMEOUT_MS = 15000;
|
|
|
|
describe('commit --files: missing files must not stage deletions (#2014)', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempGitProject();
|
|
// Commit STATE.md so it exists in git history
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n\nInitial state.\n');
|
|
gitOrThrow(['add', '.planning/STATE.md'], { cwd: tmpDir, timeoutMs: GIT_TIMEOUT_MS });
|
|
gitOrThrow(['commit', '-m', 'add STATE.md'], { cwd: tmpDir, timeoutMs: GIT_TIMEOUT_MS });
|
|
// Delete STATE.md from disk -- now missing but tracked in git
|
|
fs.unlinkSync(path.join(tmpDir, '.planning', 'STATE.md'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('passing --files for a missing tracked file does not commit a deletion', () => {
|
|
// STATE.md is tracked in git but deleted from disk.
|
|
// commit --files .planning/STATE.md should skip it (no deletion committed).
|
|
runGsdTools(
|
|
['commit', 'test commit', '--files', '.planning/STATE.md'],
|
|
tmpDir
|
|
);
|
|
|
|
// Check git log: the new commit (HEAD) must NOT have deleted STATE.md.
|
|
// git diff HEAD~1 HEAD --name-status shows what changed between commits.
|
|
let diffOutput = '';
|
|
try {
|
|
diffOutput = gitOrThrow(['diff', 'HEAD~1', 'HEAD', '--name-status'], { cwd: tmpDir, timeoutMs: GIT_TIMEOUT_MS });
|
|
} catch (e) {
|
|
// If nothing to commit, there is no HEAD~1 -- that's also acceptable
|
|
return;
|
|
}
|
|
assert.ok(
|
|
!diffOutput.includes('D\t.planning/STATE.md'),
|
|
'commit --files must not commit a deletion of a missing file, diff was:\n' + diffOutput
|
|
);
|
|
});
|
|
|
|
test('passing --files for a file that exists stages and commits it normally', () => {
|
|
// Create ROADMAP.md -- this file exists, should be staged normally
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n\nPhase 01.\n');
|
|
|
|
const result = runGsdTools(
|
|
['commit', 'add roadmap', '--files', '.planning/ROADMAP.md'],
|
|
tmpDir
|
|
);
|
|
|
|
const parsed = JSON.parse(result.output);
|
|
assert.strictEqual(parsed.committed, true, 'should have committed when file exists');
|
|
|
|
// Verify ROADMAP.md was added in the commit
|
|
const diffOutput = gitOrThrow(['diff', 'HEAD~1', 'HEAD', '--name-status'], { cwd: tmpDir, timeoutMs: GIT_TIMEOUT_MS });
|
|
assert.ok(
|
|
diffOutput.includes('A\t.planning/ROADMAP.md'),
|
|
'ROADMAP.md should appear as added in the commit'
|
|
);
|
|
});
|
|
|
|
test('--files with mix of existing and missing files only stages the existing ones', () => {
|
|
// ROADMAP.md exists on disk, STATE.md does not
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n');
|
|
|
|
runGsdTools(
|
|
['commit', 'partial files', '--files', '.planning/ROADMAP.md', '.planning/STATE.md'],
|
|
tmpDir
|
|
);
|
|
|
|
// The commit must not include a deletion of STATE.md
|
|
let diffOutput = '';
|
|
try {
|
|
diffOutput = gitOrThrow(['diff', 'HEAD~1', 'HEAD', '--name-status'], { cwd: tmpDir, timeoutMs: GIT_TIMEOUT_MS });
|
|
} catch (e) {
|
|
return; // nothing committed is fine
|
|
}
|
|
assert.ok(
|
|
!diffOutput.includes('D\t.planning/STATE.md'),
|
|
'missing file in --files list must not be committed as a deletion'
|
|
);
|
|
});
|
|
});
|