diff --git a/eslint-rules/no-adhoc-timeout-literal.allowlist.json b/eslint-rules/no-adhoc-timeout-literal.allowlist.json index 3c04f1103..58fd17217 100644 --- a/eslint-rules/no-adhoc-timeout-literal.allowlist.json +++ b/eslint-rules/no-adhoc-timeout-literal.allowlist.json @@ -21,7 +21,6 @@ "tests/check-glossary-refs.test.cjs", "tests/check-predicate.test.cjs", "tests/check-tdd-review-checkpoint-e2e.test.cjs", - "tests/ci-rebase-check.test.cjs", "tests/cjs-command-router-adapter.test.cjs", "tests/code-review-pipeline-regression.test.cjs", "tests/code-review.test.cjs", @@ -50,7 +49,6 @@ "tests/gsd-mcp-server-bin.test.cjs", "tests/gsd-secret-read-guard.test.cjs", "tests/gsd-statusline.test.cjs", - "tests/gsd-validate-commit-crash-policy.test.cjs", "tests/gsd-write-guard.test.cjs", "tests/health-validation.test.cjs", "tests/hooks-commonjs-marker.test.cjs", @@ -87,7 +85,6 @@ "tests/plan-review-convergence.test.cjs", "tests/plugin-manifest.test.cjs", "tests/policy-160-route0-resume.test.cjs", - "tests/pr-branch-planning-filter.test.cjs", "tests/prohibition-enforcement.test.cjs", "tests/prompt-injection-scan.security.test.cjs", "tests/qa/tdd-walk.cjs", @@ -95,7 +92,6 @@ "tests/read-guard.test.cjs", "tests/read-injection-scanner.property.test.cjs", "tests/read-injection-scanner.security.test.cjs", - "tests/reapply-verify-hunks.test.cjs", "tests/release-tarball-smoke.install.test.cjs", "tests/representative-corpus.test.cjs", "tests/review-lane-invocation.test.cjs", @@ -108,14 +104,11 @@ "tests/security.test.cjs", "tests/shared-hooks-dir-resolution.test.cjs", "tests/shell-command-projection-dispatch.test.cjs", - "tests/ship-notes-wedged-pr.test.cjs", - "tests/slug-derivation-drift-guard.test.cjs", "tests/state-document.test.cjs", "tests/state-todos-render.test.cjs", "tests/task-command-router-resolve-content.test.cjs", "tests/task-content-resolution.test.cjs", "tests/task-content-resolver-grammar-parity.test.cjs", "tests/teams-status.test.cjs", - "tests/windsurf-hooks-bridge.test.cjs", - "tests/worktree-safety.test.cjs" + "tests/windsurf-hooks-bridge.test.cjs" ] diff --git a/tests/ci-rebase-check.test.cjs b/tests/ci-rebase-check.test.cjs index f5a312786..6db356651 100644 --- a/tests/ci-rebase-check.test.cjs +++ b/tests/ci-rebase-check.test.cjs @@ -27,7 +27,16 @@ const NODE = process.execPath; const { cleanup } = require('./helpers.cjs'); const { gitOrThrow } = require('./helpers/git-fixture.cjs'); // #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs. -const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); +const { GIT_TIMEOUT_MS, QUICK_SPAWN_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); + +/** + * The real ci-rebase-check.cjs script run below (fetch+merge against a real + * git fixture remote) is heavier than a QUICK_SPAWN_TIMEOUT_MS-class call — + * it does real git network/merge work, not a trivial mocked/in-process + * operation. Kept as its own pre-existing value, unchanged by this + * migration (#4514). + */ +const CI_REBASE_SCRIPT_TIMEOUT_MS = 20000; // --------------------------------------------------------------------------- // Helper: run a small inline Node snippet that requires the run() helper @@ -54,7 +63,7 @@ function evalRunHelper(stmts) { } ${stmts} `; - const r = spawnSync(NODE, ['-e', code], { encoding: 'utf8', timeout: 10_000 }); + const r = spawnSync(NODE, ['-e', code], { encoding: 'utf8', timeout: QUICK_SPAWN_TIMEOUT_MS }); return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' }; } @@ -157,7 +166,7 @@ describe('ci-rebase-check: fetch-retry loop resolves when git fetch succeeds', ( const r = spawnSync(NODE, [SCRIPT], { cwd: workDir, encoding: 'utf8', - timeout: 20_000, + timeout: CI_REBASE_SCRIPT_TIMEOUT_MS, env: { ...process.env, GITHUB_BASE_REF: 'main', diff --git a/tests/gsd-validate-commit-crash-policy.test.cjs b/tests/gsd-validate-commit-crash-policy.test.cjs index 210981e95..9d57f0398 100644 --- a/tests/gsd-validate-commit-crash-policy.test.cjs +++ b/tests/gsd-validate-commit-crash-policy.test.cjs @@ -31,6 +31,17 @@ const path = require('node:path'); const { createTempDir, cleanup, TEST_ENV_BASE } = require('./helpers.cjs'); const { runHook, OUTCOME } = require('./helpers/process-seam.cjs'); +/** + * gsd-validate-commit.sh is invoked via bash and internally fans out to + * nested `node -e` calls (the classifier/config-read/JSON-extraction + * sites this file's own header docblock describes) -- conceptually the + * same shape `HOOK_FANOUT_TIMEOUT_MS` (60000ms) describes, but this hook + * sits at a much lighter pre-existing value. Not reclassified/raised: no + * bench citation exists for whether this specific hook needs more. + * Pre-existing value, unchanged by this migration (#4514). + */ +const VALIDATE_COMMIT_HOOK_TIMEOUT_MS = 15000; + const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-validate-commit.sh'); const CONFORMING_COMMIT_PAYLOAD = JSON.stringify({ @@ -94,7 +105,7 @@ function runValidateCommit({ payload, cwd, env } = {}) { cwd, env: { ...process.env, ...TEST_ENV_BASE, ...env }, input: payload, - timeoutMs: 15000, + timeoutMs: VALIDATE_COMMIT_HOOK_TIMEOUT_MS, }); } diff --git a/tests/helpers/timeouts.cjs b/tests/helpers/timeouts.cjs index d7c491258..fb39959b7 100644 --- a/tests/helpers/timeouts.cjs +++ b/tests/helpers/timeouts.cjs @@ -110,6 +110,23 @@ const INSTALL_TIMEOUT_MS = 120000; */ const SEAM_DEFAULT_TIMEOUT_MS = 60000; +/** + * A cheap, lightweight subprocess/hook invocation whose own work is + * trivial -- a synchronous in-process guard hook, a fully-mocked shell + * harness (git/gh functions stubbed out), a small `node -e` snippet, or a + * lint script scanning a tiny temp fixture -- with no real git/network/ + * fan-out work inside. 10000ms leaves generous headroom over each call + * site's sub-second observed worst case (#4514, batch 3 of the ad hoc + * timeout literal migration, epic #4445 -- consolidates 5 call sites + * across 4 files that had all independently arrived at this exact value). + * + * Deliberately NOT the same as `PROBE_TIMEOUT_MS` (15000ms): consolidating + * onto that would RAISE these sites' bound with no bench citation, which + * this migration's scope (naming, not tuning) does not permit. This norm + * is for the even-lighter end of the spectrum PROBE_TIMEOUT_MS occupies. + */ +const QUICK_SPAWN_TIMEOUT_MS = 10000; + module.exports = { PROBE_TIMEOUT_MS, HOOK_FANOUT_TIMEOUT_MS, @@ -118,4 +135,5 @@ module.exports = { BUILD_TIMEOUT_MS, INSTALL_TIMEOUT_MS, SEAM_DEFAULT_TIMEOUT_MS, + QUICK_SPAWN_TIMEOUT_MS, }; diff --git a/tests/pr-branch-planning-filter.test.cjs b/tests/pr-branch-planning-filter.test.cjs index 5b0271785..57bb1bf70 100644 --- a/tests/pr-branch-planning-filter.test.cjs +++ b/tests/pr-branch-planning-filter.test.cjs @@ -84,6 +84,14 @@ const CONFIG_SCHEMA_MANIFEST_PATH = path.join( REPO_ROOT, 'gsd-core', 'bin', 'shared', 'config-schema.manifest.json', ); +/** + * The cherry-pick recipe below runs the REAL create_pr_branch loop + * extracted from pr-branch.md against a real git fixture (rev-list, + * checkout, cherry-pick) -- genuine git work, not a mocked/trivial + * operation. Pre-existing value, unchanged by this migration (#4514). + */ +const CHERRY_PICK_RECIPE_TIMEOUT_MS = 30000; + // ── Shared git-fixture primitives (L2 + L4) ──────────────────────────────── function git(args, cwd) { @@ -379,7 +387,7 @@ describe('#2971 — pr-branch.md planning.pr_strict filter (failing-first)', () const filterPaths = strict ? ['.planning/'] : transientDirs.map((d) => `.planning/${d}/`); const script = buildRecipeScript(filterPaths); try { - const stdout = execFileSync('sh', ['-c', script], { cwd: repoDir, encoding: 'utf8', timeout: 30000 }); + const stdout = execFileSync('sh', ['-c', script], { cwd: repoDir, encoding: 'utf8', timeout: CHERRY_PICK_RECIPE_TIMEOUT_MS }); return { status: 0, stdout, stderr: '' }; } catch (err) { return { diff --git a/tests/reapply-verify-hunks.test.cjs b/tests/reapply-verify-hunks.test.cjs index 3cf99b827..aa17e588f 100644 --- a/tests/reapply-verify-hunks.test.cjs +++ b/tests/reapply-verify-hunks.test.cjs @@ -1099,6 +1099,12 @@ let savedHome; let savedUserProfile; let restoreConfigEnv; +/** + * Runs the real verify-reapply-patches.cjs CLI against patches+config + * fixture dirs. Pre-existing value, unchanged by this migration (#4514). + */ +const VERIFY_REAPPLY_TIMEOUT_MS = 60000; + function writeFile(absPath, content) { fs.mkdirSync(path.dirname(absPath), { recursive: true }); fs.writeFileSync(absPath, content); @@ -1110,7 +1116,7 @@ function runVerifier() { '--patches-dir', patchesDir, '--config-dir', configDir, '--json', - ], { timeoutMs: 60_000 }); + ], { timeoutMs: VERIFY_REAPPLY_TIMEOUT_MS }); return { status: r.exitCode, report: r.stdout && r.stdout.length ? JSON.parse(r.stdout) : null, @@ -1238,6 +1244,14 @@ function resetFixture() { fs.mkdirSync(pristineDir); } +/** + * Runs the real verify-reapply-patches.cjs CLI against patches+config+ + * pristine fixture dirs -- a distinct describe-block scope from the other + * runVerifier() above (different fixture, different pre-existing budget). + * Pre-existing value, unchanged by this migration (#4514). + */ +const VERIFY_REAPPLY_PRISTINE_TIMEOUT_MS = 30000; + function runVerifier() { const r = runNode([ SCRIPT, @@ -1245,7 +1259,7 @@ function runVerifier() { '--config-dir', configDir, '--pristine-dir', pristineDir, '--json', - ], { timeoutMs: 30_000 }); + ], { timeoutMs: VERIFY_REAPPLY_PRISTINE_TIMEOUT_MS }); return { status: r.exitCode, report: r.stdout && r.stdout.length ? JSON.parse(r.stdout) : null, diff --git a/tests/ship-notes-wedged-pr.test.cjs b/tests/ship-notes-wedged-pr.test.cjs index 15c4c363e..dd03e2b98 100644 --- a/tests/ship-notes-wedged-pr.test.cjs +++ b/tests/ship-notes-wedged-pr.test.cjs @@ -5,6 +5,7 @@ const fs = require('node:fs'); const path = require('node:path'); const { stripFencedCode, scanFencedBlocks } = require('../gsd-core/bin/lib/markdown-sectionizer.cjs'); const { cleanup, createTempDir, readFileNormalized } = require('./helpers.cjs'); +const { QUICK_SPAWN_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const SHIP_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'ship.md'); @@ -76,7 +77,7 @@ function runTrackShipping(responses) { const result = spawnSync('bash', ['-c', `${preamble}\n${extractTrackShippingScript()}`], { cwd: tmpDir, encoding: 'utf8', - timeout: 10000, + timeout: QUICK_SPAWN_TIMEOUT_MS, env: { ...process.env, CURRENT_BRANCH: 'fix/ship-note', diff --git a/tests/slug-derivation-drift-guard.test.cjs b/tests/slug-derivation-drift-guard.test.cjs index b76f174e5..80048c1c1 100644 --- a/tests/slug-derivation-drift-guard.test.cjs +++ b/tests/slug-derivation-drift-guard.test.cjs @@ -34,6 +34,7 @@ const { generateSlugInternal } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib const { getPhaseDirFromPhaseId } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'phase-id.cjs')); const { slugify: qaSmellRatchetSlugify } = require(path.join(ROOT, 'scripts', 'qa-smell-ratchet.cjs')); const { createTempDir, cleanup } = require('./helpers.cjs'); +const { QUICK_SPAWN_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const { splitLines } = require(path.join(ROOT, 'gsd-core', 'bin', 'lib', 'text-lines.cjs')); const { MAX_REGEX_LITERAL_LEN, resetRegexScanStats, getRegexScanStats } = require(path.join(ROOT, 'scripts', 'lib', 'drift-scan.cjs')); @@ -439,7 +440,7 @@ describe('CLI (main()) — the process.exitCode/stderr surface scanRepo alone do const res = spawnSync(process.execPath, [path.join(tmpRoot, 'scripts', 'lint-slug-derivation-drift.cjs')], { encoding: 'utf8', - timeout: 10000, + timeout: QUICK_SPAWN_TIMEOUT_MS, }); assert.equal(res.status, 1, 'main() must set a non-zero process.exitCode when a violation is found'); @@ -470,7 +471,7 @@ describe('CLI (main()) — the process.exitCode/stderr surface scanRepo alone do const res = spawnSync(process.execPath, [path.join(tmpRoot, 'scripts', 'lint-slug-derivation-drift.cjs')], { encoding: 'utf8', - timeout: 10000, + timeout: QUICK_SPAWN_TIMEOUT_MS, }); assert.equal(res.status, 0); diff --git a/tests/worktree-safety.test.cjs b/tests/worktree-safety.test.cjs index ecd24c2ee..64010364d 100644 --- a/tests/worktree-safety.test.cjs +++ b/tests/worktree-safety.test.cjs @@ -4980,6 +4980,7 @@ const path = require('node:path'); const { cleanup } = require('./helpers.cjs'); const { runHook: seamRunHook } = require('./helpers/process-seam.cjs'); const { gitOrThrow } = require('./helpers/git-fixture.cjs'); +const { QUICK_SPAWN_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-worktree-path-guard.js'); const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js'); @@ -5036,15 +5037,17 @@ function makeWorktree(mainRepo, branchName) { * Run the hook with a given payload, returning the spawnSync result. */ function runHook(cwd, payload) { - // 10000ms: previously UNBOUNDED (no `timeout` option passed to spawnSync). - // gsd-worktree-path-guard.js is a synchronous, in-process path-guard hook - // (fs/path checks against a JSON stdin payload) — no subprocess or network - // work of its own. 10s leaves generous headroom over its sub-second - // worst case even on a heavily contended CI runner. + // QUICK_SPAWN_TIMEOUT_MS (10000ms): previously UNBOUNDED (no `timeout` + // option passed to spawnSync). gsd-worktree-path-guard.js is a + // synchronous, in-process path-guard hook (fs/path checks against a JSON + // stdin payload) — no subprocess or network work of its own. 10s leaves + // generous headroom over its sub-second worst case even on a heavily + // contended CI runner. See tests/helpers/timeouts.cjs for the shared + // norm this value was promoted to (#4514). const r = seamRunHook(HOOK_PATH, [], { cwd, input: JSON.stringify(payload), - timeoutMs: 10_000, + timeoutMs: QUICK_SPAWN_TIMEOUT_MS, }); return { status: r.exitCode, stdout: r.stdout, stderr: r.stderr }; }