From d6788a6805788a39e78b7be43ecfb789b9a99647 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 12 Sep 2026 21:33:26 -0400 Subject: [PATCH] test(#4523): migrate config/env/locking/perf batch to named timeout constants (#4673) Batch 12 of the ad hoc timeout literal migration (epic #4445). Replaces every bare numeric timeout/timeoutMs object-literal property in tests/check-env.test.cjs, tests/config-get-default.test.cjs, tests/federated-config.test.cjs, tests/gsd-check-update-worker-platform-gate.test.cjs, tests/gsd-mcp-server-bin.test.cjs, tests/health-validation.test.cjs, tests/locking-bugs-1909-1916-1925-1927.test.cjs, tests/perf-316-state-lock-buffer-alloc.test.cjs, tests/perf-317-context-monitor-fs.test.cjs, and tests/pi-config-dir-env-override.test.cjs with a named constant, per eslint-rules/no-adhoc-timeout-literal.cjs. Removes these 10 files from the rule's allowlist. Ground truth via eslint matched the issue's stated 26 sites across 10 files exactly. Reuses PROBE_TIMEOUT_MS, GENERATOR_SCRIPT_TIMEOUT_MS, GSD_TOOLS_CLI_MODERATE_TIMEOUT_MS, and INSTALL_TIMEOUT_MS across 4 files. No new shared constants needed -- every recurring value across files was independently verified to be a genuinely different operation class, per this migration's standing rule that numeric coincidence is never identity. Adds 11 new file-local constants, three of which are not real subprocess timeouts at all (a config-merge fixture value, and two node:test per-test timeout options bounding ReDoS/lock-retry regression backstops). Per the issue's explicit mandate, gsd-check-update-worker-platform-gate.test.cjs now imports (read-only) NPM_VIEW_TIMEOUT_MS from gsd-core/bin/check-latest-version.cjs for disclosure -- this file and that production module once independently guessed the same 15000ms value, causing the PR #4428 Windows double-SIGKILL collision. No site in this file's current bare literals actually wraps a live npm-view call needing margin arithmetic, so the import documents the historical relationship honestly rather than fabricating a computation. No src/bin file edited (only a read-only import added), no numeric value changed anywhere. Co-authored-by: sim Co-authored-by: Claude Sonnet 5 --- .../no-adhoc-timeout-literal.allowlist.json | 10 ---- tests/check-env.test.cjs | 3 +- tests/config-get-default.test.cjs | 13 ++++- tests/federated-config.test.cjs | 13 ++++- ...check-update-worker-platform-gate.test.cjs | 35 +++++++++++-- tests/gsd-mcp-server-bin.test.cjs | 3 +- tests/health-validation.test.cjs | 17 +++++- .../locking-bugs-1909-1916-1925-1927.test.cjs | 14 +++-- .../perf-316-state-lock-buffer-alloc.test.cjs | 13 ++++- tests/perf-317-context-monitor-fs.test.cjs | 52 +++++++++++++++---- tests/pi-config-dir-env-override.test.cjs | 17 +++++- 11 files changed, 153 insertions(+), 37 deletions(-) diff --git a/eslint-rules/no-adhoc-timeout-literal.allowlist.json b/eslint-rules/no-adhoc-timeout-literal.allowlist.json index 3cb484a6f..175dab7f0 100644 --- a/eslint-rules/no-adhoc-timeout-literal.allowlist.json +++ b/eslint-rules/no-adhoc-timeout-literal.allowlist.json @@ -1,24 +1,14 @@ [ "tests/assumption-delta-checkpoint-e2e.test.cjs", "tests/assumption-delta.test.cjs", - "tests/check-env.test.cjs", "tests/check-predicate.test.cjs", "tests/cjs-command-router-adapter.test.cjs", "tests/code-review-pipeline-regression.test.cjs", "tests/code-review.test.cjs", - "tests/config-get-default.test.cjs", "tests/dispatcher.test.cjs", - "tests/federated-config.test.cjs", "tests/gate-predicate-evaluator.test.cjs", - "tests/gsd-check-update-worker-platform-gate.test.cjs", - "tests/gsd-mcp-server-bin.test.cjs", "tests/gsd-statusline.test.cjs", - "tests/health-validation.test.cjs", - "tests/locking-bugs-1909-1916-1925-1927.test.cjs", - "tests/perf-316-state-lock-buffer-alloc.test.cjs", - "tests/perf-317-context-monitor-fs.test.cjs", "tests/phase6-capstone-conformance.test.cjs", - "tests/pi-config-dir-env-override.test.cjs", "tests/plan-phase-stall-detection.test.cjs", "tests/plan-review-convergence.test.cjs", "tests/policy-160-route0-resume.test.cjs", diff --git a/tests/check-env.test.cjs b/tests/check-env.test.cjs index 7582cc75e..abee30fa8 100644 --- a/tests/check-env.test.cjs +++ b/tests/check-env.test.cjs @@ -23,6 +23,7 @@ const assert = require('node:assert/strict'); const path = require('node:path'); const fs = require('node:fs'); const { spawnSync } = require('node:child_process'); +const { GENERATOR_SCRIPT_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const SCRIPT = path.resolve(__dirname, '..', 'scripts', 'check-env.cjs'); const FIXTURE_ROOT = path.resolve(__dirname, 'fixtures', 'check-env'); @@ -41,7 +42,7 @@ function runScript(cwd, args = [], envOverrides = {}) { const result = spawnSync(process.execPath, [SCRIPT, ...args], { cwd, encoding: 'utf8', - timeout: 30_000, + timeout: GENERATOR_SCRIPT_TIMEOUT_MS, env: { ...process.env, ...envOverrides }, }); return { diff --git a/tests/config-get-default.test.cjs b/tests/config-get-default.test.cjs index 450f0cb4c..5e5da9c29 100644 --- a/tests/config-get-default.test.cjs +++ b/tests/config-get-default.test.cjs @@ -16,6 +16,14 @@ const path = require('path'); const os = require('os'); const { cleanup, captureFdSync } = require('./helpers.cjs'); +/** + * A `config-get` CLI call with a deliberately pathological 50KB key, + * proving the call fails safely without hanging -- distinct from + * PROBE_TIMEOUT_MS's plain-query class (15000ms) despite the shared file, + * since this site is specifically testing pathological-input tolerance. + */ +const LONG_KEY_CLI_TIMEOUT_MS = 8000; + // In-process invocation, not execFileSync: cmdConfigGet is a pure CJS // function reachable without spawning `node` as a child. The prior // execFileSync(..., { timeout: 5000 }) raced a real subprocess's startup @@ -767,6 +775,7 @@ const { execFileSync } = require('node:child_process'); const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs'); const { ERROR_REASON } = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'io.cjs')); const { cleanup } = require('./helpers.cjs'); +const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); describe('bug-2943: config-get returns schema default for context_window', () => { let tmpDir; @@ -798,7 +807,7 @@ describe('bug-2943: config-get returns schema default for context_window', () => stdout = execFileSync(process.execPath, args, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, }); } catch (err) { exitCode = err.status ?? 1; @@ -1108,7 +1117,7 @@ test('config-get with a very long key (50KB) fails safely without hanging', (t) const projectDir = createTempProject('cli-neg-config-13-'); t.after(() => cleanup(projectDir)); const longKey = 'x'.repeat(50000); - const result = runCli(['config-get', longKey], { cwd: projectDir, timeoutMs: 8000 }); + const result = runCli(['config-get', longKey], { cwd: projectDir, timeoutMs: LONG_KEY_CLI_TIMEOUT_MS }); assert.equal(result.signal, null, 'long input must not trigger the harness timeout'); assert.equal(result.hasStackTrace, false, 'long input must not crash'); assert.notEqual(result.status, 0, 'unknown 50KB key must fail'); diff --git a/tests/federated-config.test.cjs b/tests/federated-config.test.cjs index cdb69949f..218bddca2 100644 --- a/tests/federated-config.test.cjs +++ b/tests/federated-config.test.cjs @@ -67,6 +67,15 @@ const neverCentral = (_key) => false; /** An always-central isCentralKey. */ const alwaysCentral = (_key) => true; +/** + * NOT a subprocess spawn timeout. Fixture DATA -- the test value for a + * fictional `mytool.timeout` config key in a mergeFederatedConfig() + * schema-merge test, proving a user-supplied override for a numeric config + * key is honored. Never passed into a real spawnSync/execFileSync options + * object. + */ +const FIXTURE_CONFIG_TIMEOUT_VALUE = 99; + // ─── 1. Empty configSchema ──────────────────────────────────────────────────── describe('empty configSchema', () => { @@ -279,9 +288,9 @@ describe('valid federated key — user override with correct type', () => { const result = mergeFederatedConfig({ configSchema: { 'mytool.timeout': NUMBER_SLICE }, isCentralKey: neverCentral, - userConfig: { mytool: { timeout: 99 } }, + userConfig: { mytool: { timeout: FIXTURE_CONFIG_TIMEOUT_VALUE } }, }); - assert.strictEqual(result.values['mytool.timeout'], 99); + assert.strictEqual(result.values['mytool.timeout'], FIXTURE_CONFIG_TIMEOUT_VALUE); assert.deepEqual(result.warnings, []); }); diff --git a/tests/gsd-check-update-worker-platform-gate.test.cjs b/tests/gsd-check-update-worker-platform-gate.test.cjs index e1dd50df8..91cdd134f 100644 --- a/tests/gsd-check-update-worker-platform-gate.test.cjs +++ b/tests/gsd-check-update-worker-platform-gate.test.cjs @@ -29,6 +29,35 @@ const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const { scanFencedBlocks } = require('../gsd-core/bin/lib/markdown-sectionizer.cjs'); +const { NPM_VIEW_TIMEOUT_MS } = require('../gsd-core/bin/check-latest-version.cjs'); + +/** + * NOT a margin above NPM_VIEW_TIMEOUT_MS (imported above for disclosure, + * per this batch's epic issue -- this file and check-latest-version.cjs + * once independently guessed the same 15000ms value, causing a Windows + * double-SIGKILL collision fixed in PR #4428). This specific scenario + * (missing compiled runtime library) degrades BEFORE reaching the wrapped + * `npm view` call at all -- see this test's own #3582 comment + * ("checkLatestVersion -> not ok" on the degrade, never invoked) -- so its + * bound has no margin relationship to NPM_VIEW_TIMEOUT_MS to preserve. + * Kept as its own independent, pre-existing value. + */ +const WORKER_DEGRADED_PATH_TIMEOUT_MS = 8000; + +/** + * gsd-update-banner.js only reads a small cache file from disk and prints + * JSON -- no subprocess/network work -- so this leaves generous headroom + * over its sub-second worst case even on a contended CI runner. NOT + * related to NPM_VIEW_TIMEOUT_MS or WORKER_DEGRADED_PATH_TIMEOUT_MS above + * (a different hook script entirely). + */ +const UPDATE_BANNER_HOOK_TIMEOUT_MS = 10_000; + +// Imported for disclosure only (see WORKER_DEGRADED_PATH_TIMEOUT_MS's own +// doc comment above): confirms this file references the collision-prone +// constant from check-latest-version.cjs without deriving any of this +// file's own timeout values from it. +void NPM_VIEW_TIMEOUT_MS; const WORKER_PATH = path.join(__dirname, '..', 'hooks', 'gsd-check-update-worker.js'); const PROJECTION_PATH = path.join( @@ -134,7 +163,7 @@ describe('worker delegates the npm spawn (does not re-open the gate, #498)', () }; const r = runHookSeam(path.join(cold.hooksDir, 'gsd-check-update-worker.js'), [], { env, - timeoutMs: 8000, + timeoutMs: WORKER_DEGRADED_PATH_TIMEOUT_MS, }); assert.equal(r.exitCode, 0, `worker must exit 0 on a build failure; stderr: ${r.stderr}`); assert.ok(fs.existsSync(cacheFile), 'worker must still reach the end and write a cache record'); @@ -863,7 +892,7 @@ describe('gsd-update-banner.js end-to-end', () => { // contended CI runner. const r = seamRunHook(HOOK_PATH, [], { env: { ...process.env, HOME: home, USERPROFILE: home }, - timeoutMs: 10_000, + timeoutMs: UPDATE_BANNER_HOOK_TIMEOUT_MS, }); return { status: r.exitCode, stdout: r.stdout, stderr: r.stderr }; } @@ -1017,7 +1046,7 @@ describe('gsd-update-banner.js: #3582 cold tree — degrades to silent, never cr const r = seamRunHook(path.join(cold.hooksDir, 'gsd-update-banner.js'), [], { env: { ...process.env, HOME: home, USERPROFILE: home }, - timeoutMs: 10_000, + timeoutMs: UPDATE_BANNER_HOOK_TIMEOUT_MS, }); assert.equal(r.exitCode, 0, `hook must exit 0 on a build failure; stderr: ${r.stderr}`); diff --git a/tests/gsd-mcp-server-bin.test.cjs b/tests/gsd-mcp-server-bin.test.cjs index cee622b0c..f82835ead 100644 --- a/tests/gsd-mcp-server-bin.test.cjs +++ b/tests/gsd-mcp-server-bin.test.cjs @@ -11,6 +11,7 @@ const assert = require('node:assert/strict'); const { spawnSync } = require('node:child_process'); const path = require('node:path'); const { PROTOCOL_VERSION } = require('../gsd-core/bin/lib/mcp-server.cjs'); +const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const SHIM = path.join(__dirname, '..', 'bin', 'gsd-mcp-server.js'); @@ -18,7 +19,7 @@ function run(stdin) { return spawnSync(process.execPath, [SHIM], { input: stdin, encoding: 'utf-8', - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, env: { ...process.env, GSD_TEST_MODE: '1' }, }); } diff --git a/tests/health-validation.test.cjs b/tests/health-validation.test.cjs index 7c9fb90f5..c7d9531a8 100644 --- a/tests/health-validation.test.cjs +++ b/tests/health-validation.test.cjs @@ -1492,6 +1492,19 @@ const { buildNotStartedPhaseVariants, } = require('../gsd-core/bin/lib/validate.cjs'); +/** + * NOT a subprocess spawn timeout. This is `node:test`'s own per-test + * `{ timeout }` option (the second positional argument to `test(name, + * options, fn)`), used as a hang BACKSTOP for two ReDoS (#663) adversarial- + * input regression tests -- catches any regression that re-introduces a + * catastrophically-backtracking pattern. Coincides numerically with + * tests/helpers/timeouts.cjs's SCAN_USAGE_ERROR_TIMEOUT_MS and + * MALFORMED_INPUT_HOOK_TIMEOUT_MS but is a completely different mechanism + * (a node:test option, not a subprocess spawn bound) -- disclosed, not + * merged. + */ +const REDOS_BACKSTOP_TEST_TIMEOUT_MS = 5000; + // ─── Part A: behavior preservation ─────────────────────────────────────────── describe('buildRoadmapPhaseVariants — behavior preservation (#663)', () => { @@ -1588,7 +1601,7 @@ describe('buildRoadmapPhaseVariants — ReDoS adversarial fixture (#663)', () => // yields NO match (empty roadmapPhases Set) — the correct behavior when the // terminating colon is absent. The { timeout: 5000 } backstop catches any // regression that re-introduces a slow pattern. - test('malformed heading without colon yields no phase match (adversarial input)', { timeout: 5000 }, () => { + test('malformed heading without colon yields no phase match (adversarial input)', { timeout: REDOS_BACKSTOP_TEST_TIMEOUT_MS }, () => { const pathological = '## Phase a' + '-a'.repeat(32) + ' '; const { roadmapPhases } = buildRoadmapPhaseVariants(pathological); assert.strictEqual(roadmapPhases.size, 0, @@ -1601,7 +1614,7 @@ describe('buildNotStartedPhaseVariants — ReDoS adversarial fixture (#663)', () // A checklist-style line with many "-a" segments and no colon triggers the // same catastrophic backtracking. Assert the correct structured result: // the malformed line yields an empty notStarted Set. - test('malformed unchecked-item without terminator yields no phase match (adversarial input)', { timeout: 5000 }, () => { + test('malformed unchecked-item without terminator yields no phase match (adversarial input)', { timeout: REDOS_BACKSTOP_TEST_TIMEOUT_MS }, () => { // No trailing colon or whitespace: the regex terminator [:\s*] cannot match, // so the engine must backtrack through all '-a' repetitions and conclude no // match. Under the old nested-quantifier pattern this was exponential; diff --git a/tests/locking-bugs-1909-1916-1925-1927.test.cjs b/tests/locking-bugs-1909-1916-1925-1927.test.cjs index a3f981ea5..c8c68fadb 100644 --- a/tests/locking-bugs-1909-1916-1925-1927.test.cjs +++ b/tests/locking-bugs-1909-1916-1925-1927.test.cjs @@ -23,6 +23,14 @@ const { spawn } = require('child_process'); const { runGsdTools, createTempProject, cleanup, waitFor, TOOLS_PATH } = require('./helpers.cjs'); +/** + * NOT a subprocess spawn timeout. This is the file's own `waitFor` busy- + * poll helper's giving-up bound, waiting for two racing subprocesses to + * each write a "ready" sentinel file before the test drops their shared + * synchronization barrier. + */ +const BARRIER_WAIT_TIMEOUT_MS = 10000; + // ───────────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────────── @@ -234,7 +242,7 @@ describe('#1925 TOCTOU: state commands use readModifyWriteStateMd', () => { // ── Orchestrate: wait for both ready-signals, then drop the barrier ─────── try { await waitFor(() => fs.existsSync(readyA) && fs.existsSync(readyB), { - timeoutMs: 10000, + timeoutMs: BARRIER_WAIT_TIMEOUT_MS, stepMs: 10, message: 'Timed out waiting for both subprocesses to reach barrier', }); @@ -373,7 +381,7 @@ describe('#1925 TOCTOU: state commands use readModifyWriteStateMd', () => { // ── Orchestrate: wait for both ready-signals, then drop the barrier ─────── try { await waitFor(() => fs.existsSync(readyA) && fs.existsSync(readyB), { - timeoutMs: 10000, + timeoutMs: BARRIER_WAIT_TIMEOUT_MS, stepMs: 10, message: 'Timed out waiting for both subprocesses to reach barrier', }); @@ -501,7 +509,7 @@ describe('#1927 config.json: setConfigValue must hold planning lock', () => { // ── Wait for both to reach barrier, then release ────────────────────────── try { await waitFor(() => fs.existsSync(readyA) && fs.existsSync(readyB), { - timeoutMs: 10000, + timeoutMs: BARRIER_WAIT_TIMEOUT_MS, stepMs: 10, message: 'Timed out waiting for both config-set subprocesses to reach barrier', }); diff --git a/tests/perf-316-state-lock-buffer-alloc.test.cjs b/tests/perf-316-state-lock-buffer-alloc.test.cjs index 2edae5fc2..2e41ced42 100644 --- a/tests/perf-316-state-lock-buffer-alloc.test.cjs +++ b/tests/perf-316-state-lock-buffer-alloc.test.cjs @@ -38,6 +38,17 @@ const { cleanup } = require('./helpers.cjs'); // Constants // ───────────────────────────────────────────────────────────────────────────── +/** + * NOT a subprocess spawn timeout. This is `node:test`'s own per-test + * `{ timeout }` option (the second positional argument to `test(name, + * options, fn)`), bounding a worker_threads + SharedArrayBuffer + * lock-contention regression test (sabCount retry assertion). Coincides + * numerically with tests/helpers/timeouts.cjs's PROBE_TIMEOUT_MS but is a + * completely different mechanism (a node:test option, not a subprocess + * spawn bound) -- disclosed, not merged. + */ +const LOCK_RETRY_TEST_TIMEOUT_MS = 15000; + const STATE_CJS_PATH = path.join( __dirname, '..', 'gsd-core', 'bin', 'lib', 'state.cjs' ); @@ -193,7 +204,7 @@ describe('perf #316: acquireStateLock hoists sleep buffer — exactly one SAB pe test( 'sabCount === 1 after a call that undergoes >= 1 retry (post-fix assertion)', - { timeout: 15000 }, + { timeout: LOCK_RETRY_TEST_TIMEOUT_MS }, async () => { // ── Worker A: hold the lock until the writer signals contention ───────── // The holder releases the lock only when the writer signals its first diff --git a/tests/perf-317-context-monitor-fs.test.cjs b/tests/perf-317-context-monitor-fs.test.cjs index bc9b50797..1edef079b 100644 --- a/tests/perf-317-context-monitor-fs.test.cjs +++ b/tests/perf-317-context-monitor-fs.test.cjs @@ -27,10 +27,42 @@ const os = require('node:os'); const path = require('node:path'); const { execFileSync } = require('node:child_process'); const { cleanup } = require('./helpers.cjs'); +const { GSD_TOOLS_CLI_MODERATE_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const MONITOR_PATH = path.join(__dirname, '..', 'hooks', 'gsd-context-monitor.js'); const tmpDir = os.tmpdir(); +/** + * A plain single-invocation query-style spawn of hooks/gsd-context-monitor.js + * via the file's MONITOR_PATH constant -- no fan-out, no detached child + * writer triggered. + */ +const CONTEXT_MONITOR_QUERY_TIMEOUT_MS = 5000; + +/** + * The #1974 fold: this hook invocation additionally triggers a DETACHED + * `state record-session` writer internally -- heavier than + * CONTEXT_MONITOR_QUERY_TIMEOUT_MS's plain-query class despite the + * coincidentally-matching value with sites in other files this batch. + */ +const CONTEXT_MONITOR_CRITICAL_RECORD_TIMEOUT_MS = 10000; + +/** + * A tighter bound chosen for one specific quick-probe scenario in this + * file -- distinct tier from the other three context-monitor constants in + * this file, not equalized without bench data. + */ +const CONTEXT_MONITOR_QUICK_PROBE_TIMEOUT_MS = 4000; + +/** + * The #2289/#3709 folds: a hook spawn sometimes carrying an extra + * `--require ` flag ahead of the hook path itself -- + * still one process, no fan-out. Coincides numerically with other files' + * timeout constants in this same batch (a CLI pathological-input query, a + * worker's degraded fast path) but describes neither -- kept local. + */ +const CONTEXT_MONITOR_PRELOAD_HOOK_TIMEOUT_MS = 8000; + /** * Spawn the context-monitor hook with the given options. * @@ -100,7 +132,7 @@ function runMonitorRaw(opts) { stdout = execFileSync(process.execPath, [MONITOR_PATH], { input, encoding: 'utf-8', - timeout: 5000, + timeout: CONTEXT_MONITOR_QUERY_TIMEOUT_MS, }); } catch (e) { exitCode = e.status ?? 1; @@ -220,7 +252,7 @@ describe('perf #317: config.json absent (exercises config-missing → defaults p stdout = execFileSync(process.execPath, [MONITOR_PATH], { input: JSON.stringify({ session_id: sessionId, cwd: testCwd, hook_event_name: 'PostToolUse' }), encoding: 'utf-8', - timeout: 5000, + timeout: CONTEXT_MONITOR_QUERY_TIMEOUT_MS, }); } catch (e) { exitCode = e.status ?? 1; @@ -395,7 +427,7 @@ function runHook(sessionId, remainingPct, cwd) { const result = runHookSeam(HOOK_PATH, [], { input, - timeoutMs: 10000, + timeoutMs: CONTEXT_MONITOR_CRITICAL_RECORD_TIMEOUT_MS, env: { ...process.env, HOME: process.env.HOME }, }); @@ -412,7 +444,7 @@ function runRecordSession(cwd, stoppedAt) { const result = spawnSync( process.execPath, [GSD_TOOLS, 'state', 'record-session', '--stopped-at', stoppedAt, '--cwd', cwd], - { encoding: 'utf-8', timeout: 30000 } + { encoding: 'utf-8', timeout: GSD_TOOLS_CLI_MODERATE_TIMEOUT_MS } ); return { exitCode: result.status, @@ -652,7 +684,7 @@ function runStatuslineHook(remainingPct, totalTokens = 1_000_000, acwEnv = null) execFileSync(process.execPath, [HOOK_PATH], { input: payload, env, - timeout: 4000, + timeout: CONTEXT_MONITOR_QUICK_PROBE_TIMEOUT_MS, }); } catch { /* non-zero exit is fine; we only need the bridge file */ } @@ -685,7 +717,7 @@ function runMonitorHook(remainingPct, usedPct) { stdout = execFileSync(process.execPath, [MONITOR_PATH], { input, encoding: 'utf-8', - timeout: 5000, + timeout: CONTEXT_MONITOR_QUERY_TIMEOUT_MS, }); } catch (e) { stdout = e.stdout || ''; @@ -843,7 +875,7 @@ function runMonitor({ hookEventName, sessionId, remainingPct = 30, usedPct = 70, stdout = execFileSync(process.execPath, [MONITOR_PATH], { input: JSON.stringify(payload), encoding: 'utf-8', - timeout: 5000, + timeout: CONTEXT_MONITOR_QUERY_TIMEOUT_MS, env: { ...process.env, ...env }, }); } catch (e) { @@ -1069,7 +1101,7 @@ function runMonitor(opts) { input: JSON.stringify(payload), env, encoding: 'utf8', - timeout: 8000, + timeout: CONTEXT_MONITOR_PRELOAD_HOOK_TIMEOUT_MS, }); } catch (e) { stdout = e.stdout || ''; @@ -1359,7 +1391,7 @@ describe('#3709 context-monitor: PreCompact resets the warn sentinel', () => { stdout = execFileSync(process.execPath, argv, { input: JSON.stringify({ session_id: id, cwd, hook_event_name: event }), encoding: 'utf8', - timeout: 8000, + timeout: CONTEXT_MONITOR_PRELOAD_HOOK_TIMEOUT_MS, env, }); } catch (e) { stdout = e.stdout || ''; exitCode = e.status ?? 1; } @@ -2302,7 +2334,7 @@ describe('#3709 round 3: DEBOUNCE_CALLS and STALE_SECONDS at their limits', () = stdout = execFileSync(process.execPath, ['--require', NOW_PRELOAD, HOOK], { input: JSON.stringify({ session_id: id, cwd: os.tmpdir(), hook_event_name: 'PostToolUse' }), encoding: 'utf8', - timeout: 8000, + timeout: CONTEXT_MONITOR_PRELOAD_HOOK_TIMEOUT_MS, env: { ...process.env, GSD_TEST_NOW_MS: String(nowMs) }, }); } catch (e) { stdout = e.stdout || ''; exitCode = e.status ?? 1; } diff --git a/tests/pi-config-dir-env-override.test.cjs b/tests/pi-config-dir-env-override.test.cjs index 0a7b0462c..d0d1acbdf 100644 --- a/tests/pi-config-dir-env-override.test.cjs +++ b/tests/pi-config-dir-env-override.test.cjs @@ -41,13 +41,26 @@ const { spawnSync } = require('node:child_process'); const { INSTALL_SCRIPT, installerEnv, BUILD_SCRIPT } = require('./helpers/install-shared.cjs'); const { cleanup, createTempDir } = require('./helpers.cjs'); +const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); + +/** + * Spawns scripts/build-hooks.js (the same script BUILD_TIMEOUT_MS + * documents, at 30000ms) but at four times that constant's bound, used + * here as a prep step before this file's own real installer run below. + * Not equalized to the lighter shared norm without bench data -- the + * identical pattern (and identical value) to epic #4445 batch 9's + * file-local BUILD_HOOKS_UNDER_LOAD_TIMEOUT_MS in a different file + * (tests/emitted-attribution.test.cjs, not importable) -- disclosed as + * coincidence, not merged. + */ +const BUILD_HOOKS_PREP_TIMEOUT_MS = 120_000; // hooks/dist is gitignored and built (DEFECT.HOOKS-DIST-SCOPED-CI): a scoped CI // lane does not run build:hooks first, so a real --pi --global install there // would emit no gsd-hooks/ dir. Build idempotently before spawning, exactly as // the golden/emitted-attribution harnesses do. function ensureHooksBuilt() { - spawnSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe', timeout: 120_000 }); + spawnSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe', timeout: BUILD_HOOKS_PREP_TIMEOUT_MS }); } /** Spawn the real installer for --pi --global against a fresh sandbox HOME, @@ -57,7 +70,7 @@ function runPiGlobalInstall(home, extraEnv = {}) { const result = spawnSync(process.execPath, [INSTALL_SCRIPT, '--pi', '--global'], { cwd: home, encoding: 'utf8', - timeout: 120_000, + timeout: INSTALL_TIMEOUT_MS, env: installerEnv({ HOME: home, USERPROFILE: home, ...extraEnv }), }); assert.strictEqual(result.status, 0,