diff --git a/eslint-rules/no-adhoc-timeout-literal.allowlist.json b/eslint-rules/no-adhoc-timeout-literal.allowlist.json index a233b9861..1a29ae3f4 100644 --- a/eslint-rules/no-adhoc-timeout-literal.allowlist.json +++ b/eslint-rules/no-adhoc-timeout-literal.allowlist.json @@ -7,7 +7,6 @@ "tests/code-review.test.cjs", "tests/dispatcher.test.cjs", "tests/gate-predicate-evaluator.test.cjs", - "tests/gsd-statusline.test.cjs", "tests/phase6-capstone-conformance.test.cjs", "tests/plan-phase-stall-detection.test.cjs", "tests/plan-review-convergence.test.cjs", @@ -17,6 +16,5 @@ "tests/reviewer-manifest-body.test.cjs", "tests/reviewer-trust-disclosure.test.cjs", "tests/run-tests-temp-root.test.cjs", - "tests/shell-command-projection-dispatch.test.cjs", - "tests/teams-status.test.cjs" + "tests/shell-command-projection-dispatch.test.cjs" ] diff --git a/tests/gsd-statusline.test.cjs b/tests/gsd-statusline.test.cjs index 91c5011a9..c604e95e3 100644 --- a/tests/gsd-statusline.test.cjs +++ b/tests/gsd-statusline.test.cjs @@ -24,6 +24,26 @@ const { } = require('../hooks/gsd-statusline.js'); const { cleanup, saveSessionEnv, restoreSessionEnv, clearSessionEnv } = require('./helpers.cjs'); +/** + * A single hooks/gsd-statusline.js spawn, no fan-out -- the "long-lived + * status renderer" class (renders context-window percentage, git + * branch/status, active-teams state), a distinct and heavier operation + * than a trivial CLI query despite this file's own PROBE_TIMEOUT_MS-sized + * sibling class norm elsewhere in the suite. + */ +const STATUSLINE_HOOK_TIMEOUT_MS = 4000; + +/** + * The same hooks/gsd-statusline.js hook as STATUSLINE_HOOK_TIMEOUT_MS, but + * this one test rigs a custom PATH (a git shim directory) and + * CLAUDE_CONFIG_DIR override -- a heavier setup than the plain invocation, + * hence the larger pre-existing bound. Coincides numerically with + * tests/helpers/timeouts.cjs's SCAN_USAGE_ERROR_TIMEOUT_MS and + * MALFORMED_INPUT_HOOK_TIMEOUT_MS (both 5000ms) but describes neither of + * those operations -- kept local. + */ +const STATUSLINE_HOOK_GIT_SHIM_TIMEOUT_MS = 5000; + // ─── parseStateMd ─────────────────────────────────────────────────────────── describe('parseStateMd', () => { @@ -639,7 +659,7 @@ describe('context meter respects CLAUDE_CODE_AUTO_COMPACT_WINDOW (#2219)', () => delete env.CLAUDE_CODE_AUTO_COMPACT_WINDOW; } - const r = runHookSeam(hookPath, [], { input: payload, env, timeoutMs: 4000 }); + const r = runHookSeam(hookPath, [], { input: payload, env, timeoutMs: STATUSLINE_HOOK_TIMEOUT_MS }); const stdout = r.stdout; // Parse normalized used% from the statusline bar output (e.g. "60%") @@ -736,7 +756,7 @@ describe('context meter boundary: acw at/near totalCtx does not pin used at 100% input: payload, env, encoding: 'utf8', - timeout: 4000, + timeout: STATUSLINE_HOOK_TIMEOUT_MS, }); } catch (e) { stdout = e.stdout || ''; @@ -878,7 +898,7 @@ describe('todo-resolution: resolves in_progress task from the newest matching to input: payload, env, encoding: 'utf8', - timeout: 4000, + timeout: STATUSLINE_HOOK_TIMEOUT_MS, }); } catch (e) { stdout = e.stdout || ''; @@ -1656,7 +1676,7 @@ test('config-set statusline.show_context_tokens yes → rejected', () => { }, }, }); - const r = runHookSeam(hookPath, [], { input: payload, timeoutMs: 4000 }); + const r = runHookSeam(hookPath, [], { input: payload, timeoutMs: STATUSLINE_HOOK_TIMEOUT_MS }); // eslint-disable-next-line no-control-regex -- stripping ANSI SGR sequences from captured CLI output return r.stdout.replace(/\x1b\[[0-9;]*m/g, ''); } @@ -2254,7 +2274,7 @@ test('config-set statusline.show_context_tokens yes → rejected', () => { workspace: { current_dir: dir }, session_id: `test-git-${Date.now()}-${Math.random().toString(36).slice(2)}`, }); - const r = runHookSeam(hookPath, [], { input: payload, timeoutMs: 4000 }); + const r = runHookSeam(hookPath, [], { input: payload, timeoutMs: STATUSLINE_HOOK_TIMEOUT_MS }); // eslint-disable-next-line no-control-regex -- stripping ANSI SGR sequences from captured CLI output return r.stdout.replace(/\x1b\[[0-9;]*m/g, ''); } @@ -2514,7 +2534,7 @@ describe('evaluateUpdateCache lineage guard', () => { }); const r = runHookSeam(path.join(cold.hooksDir, 'gsd-statusline.js'), [], { input: payload, - timeoutMs: 4000, + timeoutMs: STATUSLINE_HOOK_TIMEOUT_MS, }); assert.equal(r.exitCode, 0, `must exit 0 on a build failure; stdout: ${r.stdout} stderr: ${r.stderr}`); assert.equal(r.stdout, '', 'must degrade to empty output, not throw a stack trace to stdout'); @@ -3056,7 +3076,7 @@ describe('evaluateUpdateCache lineage guard', () => { const r = runHookSeam(hookPath, [], { input: payload, env: { ...process.env, PATH: `${shimDir}${path.delimiter}${process.env.PATH}`, CLAUDE_CONFIG_DIR: claudeDir }, - timeoutMs: 5000, + timeoutMs: STATUSLINE_HOOK_GIT_SHIM_TIMEOUT_MS, }); assert.equal(r.outcome, OUTCOME.EXITED, `expected clean exit, got outcome=${r.outcome}`); assert.equal(r.exitCode, 0); diff --git a/tests/teams-status.test.cjs b/tests/teams-status.test.cjs index 68f13038e..bedf13694 100644 --- a/tests/teams-status.test.cjs +++ b/tests/teams-status.test.cjs @@ -15,6 +15,20 @@ const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { resolveTeamsStatus } = require('../gsd-core/bin/lib/teams-status.cjs'); +/** + * Reuses the existing PROBE_TIMEOUT_MS constant rather than declaring a + * new one, despite this batch's own issue (#4525, epic #4445) cautioning + * against forcing a "long-lived status renderer" onto PROBE_TIMEOUT_MS + * without checking. Checked directly: gsd-core/bin/lib/teams-status.cjs's + * cmdTeamsStatus (what these 5 sites spawn via `gsd-tools.cjs query + * teams-status`) is a lightweight env-truthiness check plus one fast + * synchronous runtime resolution -- no rendering, no subprocess fan-out -- + * genuinely matching PROBE_TIMEOUT_MS's class, not the heavier + * "long-lived renderer" class the issue's caution targets (that class is + * hooks/gsd-statusline.js, correctly kept off PROBE_TIMEOUT_MS in this + * same batch's sibling file, tests/gsd-statusline.test.cjs). + */ +const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const gsdToolsPath = path.resolve(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs'); @@ -120,7 +134,7 @@ describe('gsd-tools query teams-status — CLI subprocess tests', () => { [gsdToolsPath, 'query', 'teams-status'], { encoding: 'utf8', - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, env: makeEnv({ runtime: 'claude', teamsFlag: '1' }), }, ); @@ -142,7 +156,7 @@ describe('gsd-tools query teams-status — CLI subprocess tests', () => { [gsdToolsPath, 'query', 'teams-status'], { encoding: 'utf8', - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, env: makeEnv({ runtime: 'claude' }), }, ); @@ -158,7 +172,7 @@ describe('gsd-tools query teams-status — CLI subprocess tests', () => { [gsdToolsPath, 'query', 'teams-status', '--active'], { encoding: 'utf8', - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, env: makeEnv({ runtime: 'claude', teamsFlag: '1' }), }, ); @@ -173,7 +187,7 @@ describe('gsd-tools query teams-status — CLI subprocess tests', () => { [gsdToolsPath, 'query', 'teams-status', '--active'], { encoding: 'utf8', - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, env: makeEnv({ runtime: 'claude' }), }, ); @@ -186,7 +200,7 @@ describe('gsd-tools query teams-status — CLI subprocess tests', () => { [gsdToolsPath, 'query', 'teams-status', '--active'], { encoding: 'utf8', - timeout: 15000, + timeout: PROBE_TIMEOUT_MS, env: makeEnv({ runtime: 'codex', teamsFlag: '1' }), }, );