test(#4518): migrate loop/hook-point e2e batch to named timeout constants

Batch 7 of 17 in the ad hoc timeout literal migration (epic #4445).
Replaces every bare numeric timeout/timeoutMs object-literal property in
tests/loop-render-hooks.test.cjs, tests/loop-walk.qa.test.cjs,
tests/loop-hooks-empty-points-e2e.test.cjs, tests/loop-hooks-ship-pre-e2e.test.cjs,
tests/loop-hooks-verify-post-e2e.test.cjs, tests/check-gap-analysis-plan-post-e2e.test.cjs,
tests/check-tdd-review-checkpoint-e2e.test.cjs, tests/execute-wave-post-gate-pipeline-e2e.test.cjs,
tests/plan-pre-hook-e2e.test.cjs, and tests/qa/tdd-walk.cjs with a named
constant, per eslint-rules/no-adhoc-timeout-literal.cjs. Removes the 10
files from the rule's allowlist.

Promotes a new shared class norm to tests/helpers/timeouts.cjs,
LOOP_HOOK_POINT_CLI_TIMEOUT_MS: 7 files independently arrived at the same
value for a single gsd-tools.cjs CLI subcommand invocation with no
confirmed subprocess fan-out. Reuses the existing PROBE_TIMEOUT_MS for
loop-render-hooks.test.cjs's 11 sites (same class, exact value match).
Adds 4 file-local constants for values that share a class with the new
norm or an existing one but diverge in pre-existing value, or that
numerically coincide with an unrelated existing constant without
matching its actual operation.

Isolated Standards-axis review caught that the new shared constant's doc
comment exhaustively enumerated 3 verb families while a 7th genuine site
(an `init new-project` invocation) also correctly belonged to the class;
fixed by rewording the comment to state the class definition (call
shape) first and list all 4 representative verbs, explicitly
illustrative rather than exhaustive. No value changed, no site's
classification changed.

No src/bin file touched, no numeric value changed anywhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sim
2026-09-10 15:50:52 -04:00
parent 138e70d734
commit 6d6e3eea73
12 changed files with 91 additions and 31 deletions

View File

@@ -15,10 +15,8 @@
"tests/capability-writer.test.cjs",
"tests/changeset-new.test.cjs",
"tests/check-env.test.cjs",
"tests/check-gap-analysis-plan-post-e2e.test.cjs",
"tests/check-glossary-refs.test.cjs",
"tests/check-predicate.test.cjs",
"tests/check-tdd-review-checkpoint-e2e.test.cjs",
"tests/cjs-command-router-adapter.test.cjs",
"tests/code-review-pipeline-regression.test.cjs",
"tests/code-review.test.cjs",
@@ -28,7 +26,6 @@
"tests/effort-surface-axis.test.cjs",
"tests/emitted-ack-trailer.test.cjs",
"tests/emitted-attribution.test.cjs",
"tests/execute-wave-post-gate-pipeline-e2e.test.cjs",
"tests/federated-config.test.cjs",
"tests/gate-predicate-evaluator.test.cjs",
"tests/gen-context-index.test.cjs",
@@ -43,11 +40,6 @@
"tests/init.test.cjs",
"tests/lint-docs-command-form.test.cjs",
"tests/locking-bugs-1909-1916-1925-1927.test.cjs",
"tests/loop-hooks-empty-points-e2e.test.cjs",
"tests/loop-hooks-ship-pre-e2e.test.cjs",
"tests/loop-hooks-verify-post-e2e.test.cjs",
"tests/loop-render-hooks.test.cjs",
"tests/loop-walk.qa.test.cjs",
"tests/milestone-lock.test.cjs",
"tests/no-pending-3212-markers.test.cjs",
"tests/pattern.test.cjs",
@@ -57,12 +49,10 @@
"tests/phase6-capstone-conformance.test.cjs",
"tests/pi-config-dir-env-override.test.cjs",
"tests/plan-phase-stall-detection.test.cjs",
"tests/plan-pre-hook-e2e.test.cjs",
"tests/plan-review-convergence.test.cjs",
"tests/policy-160-route0-resume.test.cjs",
"tests/prohibition-enforcement.test.cjs",
"tests/prompt-injection-scan.security.test.cjs",
"tests/qa/tdd-walk.cjs",
"tests/quick-batch.test.cjs",
"tests/read-injection-scanner.property.test.cjs",
"tests/read-injection-scanner.security.test.cjs",

View File

@@ -23,6 +23,7 @@ const path = require('path');
const { spawnSync } = require('child_process');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
@@ -74,7 +75,7 @@ function spawnRenderHooks(point, cwd) {
const result = spawnSync(process.execPath, [GSD_TOOLS, 'loop', 'render-hooks', point, '--raw'], {
cwd,
encoding: 'utf8',
timeout: 60000,
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
env: { ...process.env, GSD_SESSION_KEY: '', CODEX_THREAD_ID: '', CLAUDE_SESSION_ID: '' },
});
return {

View File

@@ -32,6 +32,7 @@ const { execFileSync } = require('node:child_process');
const { cleanup, TEST_ENV_BASE } = require('./helpers.cjs');
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const TOOLS_PATH = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
@@ -147,7 +148,7 @@ function runTools(args, cwd) {
cwd,
encoding: 'utf-8',
env: { ...process.env, ...TEST_ENV_BASE },
timeout: 60000,
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
});
return { success: true, output: stdout.trim(), exitCode: 0, error: '' };
} catch (err) {

View File

@@ -31,6 +31,7 @@ const { spawnSync } = require('node:child_process');
const { cleanup } = require('./helpers.cjs');
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
@@ -72,7 +73,7 @@ function runTool(args, { cwd, env = {} } = {}) {
cwd: cwd || os.tmpdir(),
encoding: 'utf8',
env: childEnv,
timeout: 60000,
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
});
const result = { status: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
if (r.stdout && r.stdout.trim().startsWith('{')) {

View File

@@ -159,6 +159,35 @@ const FIXTURE_HOOK_TIMEOUT_SECONDS = 5;
*/
const STAGED_HOOK_SCRIPT_TIMEOUT_MS = 20000;
/**
* A single `gsd-tools.cjs` CLI subcommand invocation, spawned directly (never
* via an intermediate shell script), used by a loop/hook-point e2e test. The
* command does real in-process work -- capability/config resolution, project
* scaffolding, or (for the `check` verbs that inspect git-tracked state) real
* git plumbing calls -- but as ONE process with no confirmed nested-subprocess
* fan-out, verified per-site by reading each file's own runner function, not
* assumed from "hook" in the filename. Representative verbs seen at this
* norm's 7 sites: `loop render-hooks <point>`, `check <check-id>`,
* `execute <hook-point>`, and `init new-project` (tests/loop-walk.qa.test.cjs's
* concurrency test) -- the norm describes the CALL SHAPE (single direct
* gsd-tools.cjs spawn, no fan-out), not one specific verb family, so this list
* is illustrative, not exhaustive.
*
* Distinct from `HOOK_FANOUT_TIMEOUT_MS` (a bash-hosted script that itself
* shells out to further subprocesses) despite the coincidentally-matching
* value: this norm's sites spawn `gsd-tools.cjs` and nothing beneath it.
* Distinct from `PROBE_TIMEOUT_MS`: the same underlying `loop render-hooks`
* command is ALSO called at `PROBE_TIMEOUT_MS` (15000ms) elsewhere in the
* suite against lighter temp-project fixtures -- the two bounds are kept
* separate rather than equalized, since this migration never raises or
* lowers a value without a fresh bench citation.
*
* Shared across 7 files in batch #4518 of the ad hoc timeout literal
* migration, epic #4445 -- every site independently arrived at this exact
* value -- that is why it lives here rather than as a file-local constant.
*/
const LOOP_HOOK_POINT_CLI_TIMEOUT_MS = 60000;
module.exports = {
PROBE_TIMEOUT_MS,
HOOK_FANOUT_TIMEOUT_MS,
@@ -170,4 +199,5 @@ module.exports = {
QUICK_SPAWN_TIMEOUT_MS,
FIXTURE_HOOK_TIMEOUT_SECONDS,
STAGED_HOOK_SCRIPT_TIMEOUT_MS,
LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
};

View File

@@ -28,6 +28,7 @@ const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { cleanup, installSpawnEnv } = require('./helpers.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
@@ -68,7 +69,7 @@ function spawnGsd(args, cwd) {
return spawnSync(process.execPath, [GSD_TOOLS, ...args], {
cwd: cwd || os.tmpdir(),
encoding: 'utf8',
timeout: 60000,
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
env: installSpawnEnv(),
});
}

View File

@@ -27,6 +27,7 @@ const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { cleanup, installSpawnEnv } = require('./helpers.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
@@ -55,7 +56,7 @@ const realRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
function runTools(args, opts = {}) {
const result = spawnSync(process.execPath, [GSD_TOOLS, ...args], {
encoding: 'utf8',
timeout: 60000,
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
cwd: opts.cwd || process.cwd(),
env: installSpawnEnv(opts.env),
});

View File

@@ -29,6 +29,7 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
// ── Real modules under test ────────────────────────────────────────────────────
const {
@@ -57,7 +58,7 @@ function runCli(args, cwd) {
cwd,
encoding: 'utf8',
env: installSpawnEnv(),
timeout: 60000,
timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS,
});
return result;
}

View File

@@ -963,6 +963,7 @@ describe('renderLoopHooks', () => {
// ─── 9. End-to-end cmdLoopRenderHooks (via gsd-tools subprocess) ─────────────
const { runNode } = require('./helpers/process-seam.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const ROOT = path.resolve(__dirname, '..');
const GSD_TOOLS = path.join(ROOT, 'gsd-core', 'bin', 'gsd-tools.cjs');
@@ -970,7 +971,7 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
test('loop render-hooks plan:pre returns JSON envelope with ui-phase step active', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:pre', '--cwd', tmpProjectDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
const envelope = JSON.parse(result.stdout.trim());
@@ -987,7 +988,7 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
test('loop render-hooks plan:pre with no config.json → ui-phase step active by schema default', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:pre', '--cwd', tmpEmptyProjectDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
const envelope = JSON.parse(result.stdout.trim());
@@ -1011,7 +1012,7 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
'--config-dir',
tmpUiDisabledConfigDir,
],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
const envelope = JSON.parse(result.stdout.trim());
@@ -1027,7 +1028,7 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
test('loop render-hooks plan:pre with ui_phase=false in config.json → ui-phase step absent', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:pre', '--cwd', tmpFalseConfigProjectDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
const envelope = JSON.parse(result.stdout.trim());
@@ -1042,7 +1043,7 @@ describe('cmdLoopRenderHooks end-to-end (via gsd-tools)', () => {
test('loop render-hooks invalid-point exits non-zero', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'plan:mid', '--cwd', tmpProjectDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.notStrictEqual(result.exitCode, 0, 'Expected non-zero exit for invalid point');
assert.match(result.stderr, /plan:mid|Invalid loop point/);
@@ -1085,7 +1086,7 @@ describe('--active-cap flag (loop render-hooks)', () => {
test('--active-cap tdd with tdd_mode=true → stdout trimmed === "true", exit 0', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'tdd', '--cwd', tddOnDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
assert.strictEqual(result.stdout.trim(), 'true', 'Expected stdout "true" when tdd_mode=true');
@@ -1094,7 +1095,7 @@ describe('--active-cap flag (loop render-hooks)', () => {
test('--active-cap tdd with tdd_mode=false → stdout trimmed === "false", exit 0', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'tdd', '--cwd', tddOffDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
assert.strictEqual(result.stdout.trim(), 'false', 'Expected stdout "false" when tdd_mode=false');
@@ -1103,7 +1104,7 @@ describe('--active-cap flag (loop render-hooks)', () => {
test('--active-cap <nonexistent-cap> → stdout trimmed === "false", exit 0', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'no-such-capability-xyz', '--cwd', tddOffDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0 for unknown capId. stderr: ' + (result.stderr || ''));
assert.strictEqual(result.stdout.trim(), 'false', 'Expected stdout "false" for unknown capId');
@@ -1112,7 +1113,7 @@ describe('--active-cap flag (loop render-hooks)', () => {
test('--active-cap with no value → non-zero exit and error message', () => {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', '--cwd', tddOffDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.notStrictEqual(result.exitCode, 0, 'Expected non-zero exit when --active-cap has no value');
assert.match(result.stderr, /active-cap/i, 'Expected error message referencing --active-cap');
@@ -1122,7 +1123,7 @@ describe('--active-cap flag (loop render-hooks)', () => {
// The entire stdout must be just "true" or "false" + newline — no envelope object
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:post', '--active-cap', 'tdd', '--cwd', tddOnDir],
{ cwd: ROOT, timeoutMs: 15000 },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS },
);
assert.strictEqual(result.exitCode, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
// Must be exactly "true" or "false" — not a JSON object/envelope
@@ -1251,7 +1252,7 @@ describe('ADR-1244 D2: load-failed capability gates fail OPEN with a loud warnin
function renderHooks(overlayHome, point, extraArgs = []) {
const result = runNode(
[GSD_TOOLS, 'loop', 'render-hooks', point, '--cwd', overlayHome, ...extraArgs],
{ cwd: ROOT, timeoutMs: 15000, env: { ...process.env, GSD_HOME: overlayHome } },
{ cwd: ROOT, timeoutMs: PROBE_TIMEOUT_MS, env: { ...process.env, GSD_HOME: overlayHome } },
);
assert.strictEqual(result.exitCode, 0, `Expected exit 0 at ${point}. stderr: ` + (result.stderr || ''));
return result;

View File

@@ -21,6 +21,16 @@ const { promisify } = require('node:util');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { getLiveCommandTokens } = require('./helpers/live-command-registry.cjs');
const { LOOP_HOOK_POINT_CLI_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
// Bounds a full run of scripts/qa-smell-ratchet.cjs against the real repo
// tree (a static-analysis scan, not an installer or a CLI subcommand). The
// value numerically coincides with INSTALL_TIMEOUT_MS in
// tests/helpers/timeouts.cjs, but describes a different class of work, so
// it is kept local rather than reusing that constant. No fresh bench data
// justifies a different number, so the pre-existing 120000ms literal is
// preserved exactly under this name.
const QA_SMELL_RATCHET_SCAN_TIMEOUT_MS = 120000;
const { KIND, classify } = require('./qa/result.cjs');
const { ORACLES, runOracles, SEVERITY } = require('./qa/oracles.cjs');
@@ -1617,7 +1627,7 @@ describe('worktree-concurrency (dedicated — trajectory 9 is not expressible as
const { stdout } = await execFileAsync(
process.execPath,
[TOOLS_PATH, '--json-errors', ...argv],
{ cwd: dir, encoding: 'utf-8', env, timeout: 60000 },
{ cwd: dir, encoding: 'utf-8', env, timeout: LOOP_HOOK_POINT_CLI_TIMEOUT_MS },
);
return { stdout: stdout.trim(), startedAtMs, finishedAtMs: Date.now() };
}
@@ -1849,7 +1859,7 @@ describe('qa-smell-ratchet gate (#3597)', () => {
const result = spawnSync(
process.execPath,
['-e', `require(${JSON.stringify(scriptPath)})`],
{ cwd: repoRoot, timeout: 120000, encoding: 'utf-8' },
{ cwd: repoRoot, timeout: QA_SMELL_RATCHET_SCAN_TIMEOUT_MS, encoding: 'utf-8' },
);
assert.strictEqual(
result.status,

View File

@@ -33,6 +33,16 @@ const { cleanup } = require('./helpers.cjs');
const GSD_TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
/**
* Bounds a single gsd-tools.cjs CLI subcommand invocation — the same call
* shape as the shared LOOP_HOOK_POINT_CLI_TIMEOUT_MS constant in
* tests/helpers/timeouts.cjs. This file's pre-existing bound (30000ms) is
* half that shared norm's value (60000ms) — kept as its own file-local
* constant rather than folded into the shared one, since no fresh bench
* data justifies raising this file's bound to match.
*/
const PLAN_PRE_HOOK_CLI_TIMEOUT_MS = 30000;
// ─── Subprocess helper (isolated env, no ambient GSD_ vars) ───────────────────
const CLEAN_ENV = {
@@ -65,7 +75,7 @@ function runTools(args, cwd, envOverrides = {}) {
{
cwd: cwd || process.cwd(),
encoding: 'utf8',
timeout: 30000,
timeout: PLAN_PRE_HOOK_CLI_TIMEOUT_MS,
env: { ...process.env, ...CLEAN_ENV, ...envOverrides },
},
);

View File

@@ -190,6 +190,19 @@ function parseResultSentinel(stdout) {
return lineEndIdx === -1 ? stdout.slice(valueStart) : stdout.slice(valueStart, lineEndIdx);
}
// Bounds `bash -c` execution (below) of a REAL fenced bash block extracted
// from a workflow markdown file's resolution/dispatch step — a bash-hosted
// script that may itself shell out further (e.g. via a `gsd_run` shim), not
// a bare `gsd-tools.cjs` CLI subcommand invocation. Numerically coincides
// with tests/helpers/timeouts.cjs's `BUILD_TIMEOUT_MS` (hooks-bundle build
// via scripts/build-hooks.js, an unrelated operation) and with this batch's
// other file-local `PLAN_PRE_HOOK_CLI_TIMEOUT_MS` (a different file, a
// different command), but matches neither class — kept local, both
// coincidences disclosed rather than silently reusing an unrelated existing
// constant. No fresh bench data justifies a different number, so the
// pre-existing 30000ms literal is preserved exactly under this name.
const TDD_BACKEND_SCRIPT_TIMEOUT_MS = 30000;
/**
* Execute a backend's extracted script for real, via `bash -c`, against
* `cwd`. Never throws: a non-zero exit is captured and returned as a normal
@@ -217,7 +230,7 @@ function executeBackendScript(script, cwd) {
cwd,
env: { ...process.env, ...TEST_ENV_BASE, RUNTIME_DIR: REPO_ROOT, GSD_TEST_MODE: '1' },
encoding: 'utf8',
timeout: 30000,
timeout: TDD_BACKEND_SCRIPT_TIMEOUT_MS,
});
return { success: true, value: parseResultSentinel(stdout), stdout, stderr: '' };
} catch (error) {