* 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>
This commit is contained in:
@@ -9,9 +9,13 @@ const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { runGsdTools, createTempProject, createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { detectChildRepos } = require('../gsd-core/bin/lib/init.cjs');
|
||||
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
||||
|
||||
// 15000ms: git plumbing (init/config/add/commit/worktree/clone) on a small
|
||||
// mkdtemp fixture repo — far over any observed duration for that class of call.
|
||||
const GIT_TIMEOUT_MS = 15000;
|
||||
|
||||
// ─── detectChildRepos ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -32,8 +36,8 @@ describe('detectChildRepos', () => {
|
||||
const repo2 = path.join(tmpDir, 'repo-b');
|
||||
fs.mkdirSync(repo1);
|
||||
fs.mkdirSync(repo2);
|
||||
execSync('git init', { cwd: repo1, stdio: 'pipe' });
|
||||
execSync('git init', { cwd: repo2, stdio: 'pipe' });
|
||||
gitOrThrow(['init'], { cwd: repo1, timeoutMs: GIT_TIMEOUT_MS });
|
||||
gitOrThrow(['init'], { cwd: repo2, timeoutMs: GIT_TIMEOUT_MS });
|
||||
|
||||
const repos = detectChildRepos(tmpDir);
|
||||
assert.strictEqual(repos.length, 2);
|
||||
@@ -46,7 +50,7 @@ describe('detectChildRepos', () => {
|
||||
const notRepo = path.join(tmpDir, 'just-a-dir');
|
||||
fs.mkdirSync(gitRepo);
|
||||
fs.mkdirSync(notRepo);
|
||||
execSync('git init', { cwd: gitRepo, stdio: 'pipe' });
|
||||
gitOrThrow(['init'], { cwd: gitRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
|
||||
const repos = detectChildRepos(tmpDir);
|
||||
assert.strictEqual(repos.length, 1);
|
||||
@@ -56,7 +60,7 @@ describe('detectChildRepos', () => {
|
||||
test('skips hidden directories', () => {
|
||||
const hiddenRepo = path.join(tmpDir, '.hidden-repo');
|
||||
fs.mkdirSync(hiddenRepo);
|
||||
execSync('git init', { cwd: hiddenRepo, stdio: 'pipe' });
|
||||
gitOrThrow(['init'], { cwd: hiddenRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
|
||||
const repos = detectChildRepos(tmpDir);
|
||||
assert.strictEqual(repos.length, 0);
|
||||
@@ -103,7 +107,7 @@ describe('init new-workspace', () => {
|
||||
test('detects child git repos in cwd', () => {
|
||||
const repo = path.join(tmpDir, 'my-repo');
|
||||
fs.mkdirSync(repo);
|
||||
execSync('git init', { cwd: repo, stdio: 'pipe' });
|
||||
gitOrThrow(['init'], { cwd: repo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
|
||||
const result = runGsdTools('init new-workspace', tmpDir);
|
||||
const data = JSON.parse(result.output);
|
||||
@@ -272,18 +276,18 @@ describe('workspace worktree integration', () => {
|
||||
// Create a source git repo with a commit
|
||||
sourceRepo = path.join(tmpDir, 'source-repo');
|
||||
fs.mkdirSync(sourceRepo);
|
||||
execSync('git init', { cwd: sourceRepo, stdio: 'pipe' });
|
||||
execSync('git config user.email "test@test.com"', { cwd: sourceRepo, stdio: 'pipe' });
|
||||
execSync('git config user.name "Test"', { cwd: sourceRepo, stdio: 'pipe' });
|
||||
gitOrThrow(['init'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
gitOrThrow(['config', 'user.email', 'test@test.com'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
gitOrThrow(['config', 'user.name', 'Test'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
fs.writeFileSync(path.join(sourceRepo, 'README.md'), '# Test Repo\n');
|
||||
execSync('git add -A', { cwd: sourceRepo, stdio: 'pipe' });
|
||||
execSync('git commit -m "initial"', { cwd: sourceRepo, stdio: 'pipe' });
|
||||
gitOrThrow(['add', '-A'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
gitOrThrow(['commit', '-m', 'initial'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up worktrees before removing tmp dir
|
||||
try {
|
||||
execSync('git worktree prune', { cwd: sourceRepo, stdio: 'pipe' });
|
||||
gitOrThrow(['worktree', 'prune'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
} catch { /* best-effort */ }
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
@@ -294,9 +298,9 @@ describe('workspace worktree integration', () => {
|
||||
fs.mkdirSync(path.join(wsPath, '.planning'));
|
||||
|
||||
// Create worktree
|
||||
execSync(`git worktree add "${path.join(wsPath, 'source-repo')}" -b workspace/test`, {
|
||||
gitOrThrow(['worktree', 'add', path.join(wsPath, 'source-repo'), '-b', 'workspace/test'], {
|
||||
cwd: sourceRepo,
|
||||
stdio: 'pipe',
|
||||
timeoutMs: GIT_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// Verify worktree was created
|
||||
@@ -314,8 +318,8 @@ describe('workspace worktree integration', () => {
|
||||
fs.mkdirSync(wsPath);
|
||||
|
||||
// Clone repo
|
||||
execSync(`git clone "${sourceRepo}" "${path.join(wsPath, 'source-repo')}"`, {
|
||||
stdio: 'pipe',
|
||||
gitOrThrow(['clone', sourceRepo, path.join(wsPath, 'source-repo')], {
|
||||
timeoutMs: GIT_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// Verify clone
|
||||
@@ -332,24 +336,24 @@ describe('workspace worktree integration', () => {
|
||||
fs.mkdirSync(wsPath);
|
||||
|
||||
// Create worktree
|
||||
execSync(`git worktree add "${path.join(wsPath, 'source-repo')}" -b workspace/removable`, {
|
||||
gitOrThrow(['worktree', 'add', path.join(wsPath, 'source-repo'), '-b', 'workspace/removable'], {
|
||||
cwd: sourceRepo,
|
||||
stdio: 'pipe',
|
||||
timeoutMs: GIT_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
assert.ok(fs.existsSync(path.join(wsPath, 'source-repo', 'README.md')));
|
||||
|
||||
// Remove worktree
|
||||
execSync(`git worktree remove "${path.join(wsPath, 'source-repo')}"`, {
|
||||
gitOrThrow(['worktree', 'remove', path.join(wsPath, 'source-repo')], {
|
||||
cwd: sourceRepo,
|
||||
stdio: 'pipe',
|
||||
timeoutMs: GIT_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
// Verify worktree is gone
|
||||
assert.ok(!fs.existsSync(path.join(wsPath, 'source-repo')));
|
||||
|
||||
// Verify worktree list doesn't include it
|
||||
const worktrees = execSync('git worktree list', { cwd: sourceRepo, encoding: 'utf8' });
|
||||
const worktrees = gitOrThrow(['worktree', 'list'], { cwd: sourceRepo, timeoutMs: GIT_TIMEOUT_MS });
|
||||
assert.ok(!worktrees.includes('removable-ws'));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user