* test: red — bounded git subprocess + structured worktree warnings (#3281) Regression tests for #3281: worktree-related git subprocess calls have no timeout bound, and timeout/error outcomes are not surfaced as structured signals. Failing assertions: - planWorktreePrune / listLinkedWorktreePaths / snapshotWorktreeInventory must return reason=git_timed_out (not generic git_list_failed) when execGit returns timedOut:true — enables callers to distinguish timeout from auth failure - executeWorktreePrunePlan must include timedOut:true in result when the git prune call itself times out Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(worktree): bounded git subprocess + structured warning surfacing (#3281) Root cause (PRED.k014): execGit / execGitDefault called spawnSync with no timeout, so `git worktree list --porcelain` against a hung/locked repo blocked the parent process indefinitely. Downstream callers in core.cjs and verify.cjs then swallowed any resulting failure silently via catch { /* intentionally empty */ } (PRED.k302). Fix: - worktree-safety.cjs: execGitDefault now passes timeout:10000 to spawnSync. Detects SIGTERM+ETIMEDOUT and returns { timedOut:true } in the result shape. readWorktreeList maps timedOut:true -> reason:'git_timed_out' (distinct from generic git_list_failed) so callers can emit a structured warning. executeWorktreePrunePlan propagates timedOut:true as a first-class result field. - core.cjs: execGit receives the same timeout+timedOut treatment (PRED.k014 uniform-fix discipline). pruneOrphanedWorktrees now emits a [gsd-tools] WARNING to stderr when the git prune call times out instead of silent-catch. - verify.cjs: Check 11 branches on worktreeHealth.ok to surface W018 warning when the worktree list times out, instead of silent-catch on ok:false. Backward-compatible: exitCode/stdout/stderr continue to work for all existing callers; timedOut and error are additive new fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * changeset: pr=3283 for #3281 * fix(verify): rename W020 for worktree-timeout warning to avoid W018 collision W018 is already used for milestone archive drift (Check 12). The new worktree-health-degraded timeout warning was assigned W018, causing warning-code ambiguity in triage. Rename to W020 (next available code). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -722,16 +722,38 @@ function normalizeMd(content) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function execGit(cwd, args) {
|
||||
// Default timeout for worktree-related git subprocess calls (matches worktree-safety.cjs).
|
||||
// Prevents `git worktree list --porcelain` and similar calls from blocking the parent
|
||||
// process indefinitely when git is stalled (locked index, hung remote, NFS mount freeze).
|
||||
// Callers can override via an options bag if needed.
|
||||
const DEFAULT_GIT_TIMEOUT_MS = 10000;
|
||||
|
||||
/**
|
||||
* Execute a git command with a bounded timeout.
|
||||
*
|
||||
* Return shape: { exitCode, stdout, stderr, timedOut, error }
|
||||
* - timedOut: true when spawnSync reports SIGTERM + ETIMEDOUT — callers must
|
||||
* branch on this to surface a structured warning (PRED.k302).
|
||||
* - error: spawnSync error object or null
|
||||
*
|
||||
* Backward-compatible: existing callers that only read exitCode/stdout/stderr
|
||||
* continue to work unchanged.
|
||||
*/
|
||||
function execGit(cwd, args, options = {}) {
|
||||
const timeout = options.timeout ?? DEFAULT_GIT_TIMEOUT_MS;
|
||||
const result = spawnSync('git', args, {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8',
|
||||
timeout,
|
||||
});
|
||||
const timedOut = result.signal === 'SIGTERM' && result.error?.code === 'ETIMEDOUT';
|
||||
return {
|
||||
exitCode: result.status ?? 1,
|
||||
stdout: (result.stdout ?? '').toString().trim(),
|
||||
stderr: (result.stderr ?? '').toString().trim(),
|
||||
timedOut,
|
||||
error: result.error ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -778,7 +800,16 @@ function pruneOrphanedWorktrees(repoRoot) {
|
||||
{ allowDestructive: false },
|
||||
{ execGit, parseWorktreePorcelain }
|
||||
);
|
||||
executeWorktreePrunePlan(plan, { execGit });
|
||||
const pruneResult = executeWorktreePrunePlan(plan, { execGit });
|
||||
if (pruneResult && pruneResult.timedOut) {
|
||||
// AC2: surface structured warning instead of silently swallowing the timeout.
|
||||
// Uses process.stderr.write to match the [gsd-tools] WARNING prefix style.
|
||||
process.stderr.write(
|
||||
'[gsd-tools] WARNING: worktree health check degraded' +
|
||||
' — git worktree prune timed out after 10s.' +
|
||||
' Orphaned worktree metadata may remain until the next successful run.\n'
|
||||
);
|
||||
}
|
||||
} catch { /* never crash the caller */ }
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user