test(#4515): migrate installer-runs batch to named timeout constants (#4575)

* test(#4515): migrate installer-runs batch to named timeout constants

Batch 4 of the ad hoc timeout literal migration (epic #4445). Replaces
every bare numeric timeout/timeoutMs object-literal property across
tests/install.test.cjs, tests/install-minimal-hooks.test.cjs,
tests/fragment-single-edit-propagation.install.test.cjs,
tests/install-regressions.test.cjs, tests/install-runtime-artifacts.test.cjs,
tests/npm-integrity-gate.test.cjs, tests/faulty-deps.test.cjs,
tests/release-tarball-smoke.install.test.cjs,
tests/install-write-confinement.test.cjs, tests/plugin-manifest.test.cjs,
and tests/packaging-shipped-scripts-require-only-shipped.test.cjs with a
named constant, per eslint-rules/no-adhoc-timeout-literal.cjs. Removes
these 11 files from the rule's allowlist.

Adds one new shared constant to tests/helpers/timeouts.cjs for a
fixture-JSON value (in seconds, not ms) mimicking a Claude Code
settings.json hook-entry's own timeout field, shared across two files in
this batch. Every other new constant is file-local, each carrying a
comment explaining why its call site is a distinct operational class from
the existing shared norms even where its digits numerically coincide with
one. No src/bin file touched, no numeric timeout value changed anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: combine gsd-worktree-path-guard's git rev-parse calls to cut subprocess count under CI load

Discovered while verifying PR #4575 (a full test (windows-latest) failure in an unrelated test, tests/kilo-upgrades.test.cjs's worktree-path-guard rejection test). Root cause: this hook's up-to-4 sequential git spawns (git-dir, branch, worktree-toplevel, file-toplevel), invoked inside a native-plugin's own 8000ms-capped subprocess wrapper, left zero margin for node/git startup overhead — the guard is deliberately fail-open on any timeout, so CI-load-induced latency in this chain silently downgrades a security block into an allow. Combines the first three git rev-parse calls (git-dir, branch via --abbrev-ref, worktree-toplevel) into ONE spawn instead of three, using git's documented multi-flag rev-parse output (one line per flag, in order) — verified empirically including the detached-HEAD edge case. No timeout value changed; this reduces subprocess COUNT, not any tolerance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: add changeset fragment for the worktree-path-guard fix

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-09 11:45:44 -04:00
committed by GitHub
parent 5823d2ec7a
commit 30468f16fe
15 changed files with 221 additions and 97 deletions

View File

@@ -163,17 +163,37 @@ process.stdin.on('end', () => {
// returns a path containing .git/worktrees/ as a component.
// In the main repo or a submodule it returns .git (or a path without /worktrees/).
// This approach works even when cwd is a subdirectory of the worktree.
const gitDirResult = git(['rev-parse', '--git-dir'], cwd);
// Combined into one spawn — git rev-parse accepts multiple query flags in
// one invocation and prints one line of output per flag, in the exact
// order given, reducing this guard's worst-case subprocess count under
// CI/load contention (three spawns collapse into one). `--abbrev-ref HEAD`
// is used instead of `symbolic-ref --short HEAD` because it is combinable
// (a single `rev-parse` call) and behaviorally equivalent for this guard's
// branch-acceptance check, including on detached HEAD: `--abbrev-ref`
// returns the literal string `HEAD` there (exit 0), which the acceptance
// regex below also rejects — the same guard outcome as symbolic-ref's
// exit-128/empty-stdout failure. Do not change any timeout value as part
// of this change, only the spawn count.
const combinedResult = git(['rev-parse', '--git-dir', '--abbrev-ref', 'HEAD', '--show-toplevel'], cwd);
// #3911: a timeout/spawn-failure result is indistinguishable from a clean
// "not a git repo" answer by status/stdout alone — reportIfUndetermined
// is a no-op on a genuine negative and only fires the diagnostic when the
// probe itself could not run. The allow() below is UNCHANGED either way.
reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --git-dir', gitDirResult);
if (gitDirResult.status !== 0 || !gitDirResult.stdout) {
reportIfUndetermined(
'gsd-worktree-path-guard',
'git rev-parse --git-dir --abbrev-ref HEAD --show-toplevel',
combinedResult
);
if (combinedResult.status !== 0 || !combinedResult.stdout) {
allow(undefined); // not a git repo — pass through
}
const gitDir = gitDirResult.stdout.trim();
const combinedLines = combinedResult.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
if (combinedLines.length < 3) {
allow(undefined); // malformed/short output — can't determine root, fail open
}
const [gitDir, branch, wtTopRaw] = combinedLines;
// A linked worktree's --git-dir contains .git/worktrees/ as a path component
const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
if (!isLinkedWorktree) {
@@ -186,23 +206,14 @@ process.stdin.on('end', () => {
// created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
// on the user's own branch, so the guard must be a no-op there. Detached HEAD
// / error → not GSD-managed → no-op.
const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
reportIfUndetermined('gsd-worktree-path-guard', 'git symbolic-ref --short HEAD', branchResult);
const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
// #3021: accept worktree-wf_<runid>-<n> branches (Workflow backend's naming).
if (!/^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/.test(branch)) {
allow(undefined); // not a GSD-managed executor worktree — no-op
}
// Get the raw --show-toplevel output for the worktree (cwd).
// wtTopRaw: the raw --show-toplevel output for the worktree (cwd).
// We keep it raw (not path.resolve'd) to compare directly with the
// file's toplevel — same git binary, same format, no normalization needed.
const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (worktree cwd)', wtTopResult);
if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
allow(undefined); // can't determine root — fail open
}
const wtTopRaw = wtTopResult.stdout.trim();
// #2595 (review Major 3): read the field TYPED. `?.file_path || ''` let a
// non-string through — `[]` and `{}` are truthy, so they survived the