From 3e674e4c01f51f80575febb2248e0de49f84eb18 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 31 Aug 2026 04:49:57 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20sweep=20=E2=80=94=20every=20sync=20spawn?= =?UTF-8?q?=20in=20the=20test=20trees=20carries=20a=20timeout=20(436=20sit?= =?UTF-8?q?es,=20157=20files)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spawnSync/execSync/Bun.spawnSync BLOCK the main thread, so bun's in-process per-test timeout can never fire while one waits — a hung child (stdin read, network probe, dead daemon) wedges the whole shard until the runner's external wall-clock SIGKILL. This exact class reached main: free-tests run 33262077256, test/gstack-memory-ingest.test.ts (normally 2.3s) held shard 2 at the 360s wall while its five siblings finished in ~65s. Mechanical sweep in two waves (12 + 4 fan-out agents, every edit verified against its call site): default timeout: 30_000 (matches the free runner's per-test budget), 120_000 for genuinely slow ops (installs, builds, playwright, provider CLIs), helper wrappers fixed ONCE where call sites route through them. Sites that only LOOK like calls (string fixtures, grep needles, comments) were skipped with reasons — the enforcement commit that follows marks them exempt. Co-Authored-By: Claude Fable 5 --- browse/test/build.test.ts | 2 +- browse/test/bun-polyfill.test.ts | 28 +++++----- browse/test/config.test.ts | 16 +++--- browse/test/file-permissions.test.ts | 4 +- browse/test/findport.test.ts | 6 +-- browse/test/gstack-config.test.ts | 1 + browse/test/gstack-update-check.test.ts | 1 + browse/test/temp-dirs.test.ts | 2 +- browse/test/xvfb.test.ts | 2 +- ios-qa/daemon/test/cli-mint.test.ts | 3 +- ios-qa/scripts/gen-accessors.test.ts | 16 +++--- ios-qa/scripts/gen-accessors.ts | 3 +- make-pdf/test/e2e/diagram-gate.test.ts | 1 + test/artifacts-init-migration.test.ts | 2 + test/auq-error-fallback-hook.test.ts | 1 + test/bin-context-windows-slug.test.ts | 42 +++++++-------- test/binding-template-drift.test.ts | 4 +- test/brain-sync.test.ts | 24 +++++---- test/branch-slug-hygiene.test.ts | 10 ++-- test/builder-profile.test.ts | 2 +- test/code-intelligence.test.ts | 8 +-- test/codex-e2e-plan-format.test.ts | 2 +- ...codex-e2e-recommendation-substance.test.ts | 2 +- test/codex-e2e-sol-scope.test.ts | 4 +- test/codex-e2e.test.ts | 2 +- test/codex-generation-model.test.ts | 2 + test/codex-hardening.test.ts | 2 +- test/codex-resume-flag-semantics.test.ts | 2 +- test/codex-web-search-flag.test.ts | 2 +- test/context-bill.test.ts | 2 +- test/design-flag-utils.test.ts | 2 +- test/diagram-render-drift.test.ts | 2 +- test/distill-apply.test.ts | 2 +- test/distill-free-text.test.ts | 8 +-- test/egress-receipt.test.ts | 8 +-- test/empty-find-fallthrough.test.ts | 5 +- test/eval-budgets-policy.test.ts | 2 +- test/eval-list-cli.test.ts | 1 + test/fs-utils.test.ts | 7 +-- test/gbrain-detect-install.test.ts | 1 + test/gbrain-init-rollback.test.ts | 1 + test/gbrain-init-voyage-code-3.test.ts | 4 +- test/gbrain-lib-verify.test.ts | 2 + test/gbrain-local-status.test.ts | 3 ++ test/gbrain-repo-policy-client.test.ts | 2 +- test/gbrain-repo-policy.test.ts | 4 +- test/gbrain-source-worktree-advance.test.ts | 3 +- test/gbrain-sync-skip.test.ts | 3 +- ...ain-sync-voyage-code-3-integration.test.ts | 12 ++--- test/gemini-e2e.test.ts | 2 +- test/gen-skill-docs-import-purity.test.ts | 2 +- test/gen-skill-docs-out-dir.test.ts | 2 +- test/gen-skill-docs.test.ts | 19 ++++--- test/global-discover.test.ts | 3 +- test/gstack-artifacts-init.test.ts | 17 +++--- test/gstack-artifacts-url.test.ts | 2 +- test/gstack-codex-session-import.test.ts | 4 +- test/gstack-config-defaults.test.ts | 1 + test/gstack-config-key-locale.test.ts | 1 + test/gstack-config-redact-keys.test.ts | 1 + test/gstack-decision-bins.test.ts | 6 +-- test/gstack-detach.test.ts | 6 +-- test/gstack-developer-profile.test.ts | 2 + test/gstack-egress-cli.test.ts | 3 ++ test/gstack-gbrain-detect-mcp-mode.test.ts | 1 + test/gstack-gbrain-mcp-verify.test.ts | 3 ++ test/gstack-gbrain-source-wireup.test.ts | 21 +++++--- test/gstack-gbrain-sync.test.ts | 52 +++++++++---------- test/gstack-home-module-scope.test.ts | 2 +- test/gstack-memory-ingest.test.ts | 16 +++--- test/gstack-next-version.test.ts | 32 ++++++------ test/gstack-paths.test.ts | 3 ++ test/gstack-question-log.test.ts | 1 + test/gstack-question-preference.test.ts | 2 + test/gstack-redact-cli.test.ts | 3 +- test/gstack-repo-mode.test.ts | 1 + .../gstack-settings-hook-schema-aware.test.ts | 1 + test/gstack-slug-cwd-walk-up.test.ts | 3 +- test/gstack-slug-sanitize.test.ts | 3 ++ test/gstack-state-root-override.test.ts | 1 + test/gstack-team-init-hook-schema.test.ts | 1 + ...gstack-upgrade-migration-v1_17_0_0.test.ts | 1 + ...gstack-upgrade-migration-v1_40_0_0.test.ts | 1 + test/helpers/auq-sdk-capture.ts | 2 +- test/helpers/capture-parity-baseline.ts | 4 +- test/helpers/codex-session-runner.ts | 2 +- test/helpers/gemini-session-runner.ts | 2 +- test/heredoc-pipe-deadlock.test.ts | 6 ++- test/host-config.test.ts | 4 +- test/ios-qa-regen.test.ts | 10 ++-- test/learnings.test.ts | 2 +- test/memory-cache-injection.test.ts | 1 + test/migrations-v1.27.0.0.test.ts | 1 + test/migrations-v1.65.0.0.test.ts | 1 + test/no-stale-gstack-brain-refs.test.ts | 2 +- test/paid-orphan-tripwire.test.ts | 2 +- test/plan-tune-gates.test.ts | 2 +- test/plan-tune.test.ts | 14 ++--- test/pr-title-rewrite.test.ts | 4 +- test/preamble-first-task-scaffold.test.ts | 2 +- test/question-log-hook.test.ts | 4 +- test/question-preference-hook.test.ts | 3 +- test/readme-throughput.test.ts | 1 + test/redact-audit-log.test.ts | 2 +- test/redact-prepush-hook.test.ts | 17 +++--- test/redact-prepush-rebase-force-push.test.ts | 5 +- test/redact-prepush-scan-range.test.ts | 5 +- test/regression-issue2091-bsd-mktemp.test.ts | 1 + test/regression-pr1169-build-app-sed.test.ts | 3 +- test/review-log.test.ts | 4 +- test/routing-probe.test.ts | 4 +- test/sandbox-doctor-shell.test.ts | 2 +- test/session-update-autostash.test.ts | 2 +- test/setup-bun-cmd-and-pipe-bugs.test.ts | 2 +- test/setup-conductor-worktree.test.ts | 10 ++-- ...tup-plan-tune-hooks-noninteractive.test.ts | 19 +++---- test/ship-version-sync.test.ts | 6 +-- test/skill-e2e-auto-decide-preserved.test.ts | 3 ++ test/skill-e2e-design.test.ts | 2 +- test/skill-e2e-first-task-scaffold.test.ts | 6 +-- test/skill-e2e-ios-device.test.ts | 2 +- test/skill-e2e-ios-swift-build.test.ts | 6 +-- test/skill-e2e-plan-tune-cathedral.test.ts | 16 ++++-- test/skill-e2e-preamble-script-ab.test.ts | 1 + test/skill-e2e-qa-workflow.test.ts | 4 +- test/skill-e2e-review-attribution.test.ts | 2 +- test/skill-e2e-session-intelligence.test.ts | 6 +-- test/skill-e2e-ship-idempotency.test.ts | 4 +- test/skill-e2e-workflow.test.ts | 2 +- test/skill-size-budget.test.ts | 3 +- test/skill-validation.test.ts | 24 +++++---- test/taste-engine.test.ts | 2 +- test/team-mode.test.ts | 16 +++--- test/telemetry-repo-strip.test.ts | 3 +- test/tracker-guard-wiring.test.ts | 2 +- test/uninstall.test.ts | 15 ++++-- test/upgrade-migration-v1.test.ts | 1 + test/user-render-out-dir-install.test.ts | 2 +- test/worktree.test.ts | 20 +++---- 139 files changed, 451 insertions(+), 331 deletions(-) diff --git a/browse/test/build.test.ts b/browse/test/build.test.ts index 050f35764..38e3b1891 100644 --- a/browse/test/build.test.ts +++ b/browse/test/build.test.ts @@ -13,7 +13,7 @@ describe('build: server-node.mjs', () => { // Skip rather than fail so plain `bun test` without a prior build passes. return; } - expect(() => execSync(`node --check ${SERVER_NODE}`, { stdio: 'pipe' })).not.toThrow(); + expect(() => execSync(`node --check ${SERVER_NODE}`, { stdio: 'pipe', timeout: 30_000 })).not.toThrow(); }); test('does not inline @ngrok/ngrok (must be external)', () => { diff --git a/browse/test/bun-polyfill.test.ts b/browse/test/bun-polyfill.test.ts index 21ab985cb..7df28b0d4 100644 --- a/browse/test/bun-polyfill.test.ts +++ b/browse/test/bun-polyfill.test.ts @@ -27,7 +27,7 @@ describe('bun-polyfill', () => { const elapsed = Date.now() - start; console.log(elapsed >= 40 ? 'OK' : 'TOO_FAST'); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('OK'); expect(result.exitCode).toBe(0); }); @@ -38,7 +38,7 @@ describe('bun-polyfill', () => { const r = Bun.spawnSync(['echo', 'hello'], { stdout: 'pipe' }); console.log(r.stdout.toString().trim()); console.log('exit:' + r.exitCode); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const lines = result.stdout.toString().trim().split('\n'); expect(lines[0]).toBe('hello'); expect(lines[1]).toBe('exit:0'); @@ -51,7 +51,7 @@ describe('bun-polyfill', () => { console.log(typeof p.pid === 'number' ? 'HAS_PID' : 'NO_PID'); console.log(typeof p.kill === 'function' ? 'HAS_KILL' : 'NO_KILL'); console.log(typeof p.unref === 'function' ? 'HAS_UNREF' : 'NO_UNREF'); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const lines = result.stdout.toString().trim().split('\n'); expect(lines[0]).toBe('HAS_PID'); expect(lines[1]).toBe('HAS_KILL'); @@ -70,7 +70,7 @@ describe('bun-polyfill', () => { console.log(typeof p.exited === 'object' && typeof p.exited.then === 'function' ? 'IS_PROMISE' : 'NOT_PROMISE'); console.log('exit:' + await p.exited); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const lines = result.stdout.toString().trim().split('\n'); expect(lines[0]).toBe('IS_PROMISE'); expect(lines[1]).toBe('exit:0'); @@ -83,7 +83,7 @@ describe('bun-polyfill', () => { const p = Bun.spawn(['node', '-e', 'process.exit(3)'], { stdio: ['ignore', 'ignore', 'ignore'] }); console.log('exit:' + await p.exited); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('exit:3'); }); @@ -100,7 +100,7 @@ describe('bun-polyfill', () => { const out = await new Response(p.stdout).text(); console.log(out + ':' + code); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('ready:0'); }); @@ -120,7 +120,7 @@ describe('bun-polyfill', () => { ]).catch(() => 'TIMEOUT'); console.log('exit:' + code); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); // Anything other than 'TIMEOUT' (and ideally a non-zero number) means the // lifecycle promise resolved on the spawn error. const out = result.stdout.toString().trim(); @@ -139,7 +139,7 @@ describe('bun-polyfill', () => { setTimeout(() => p.kill('SIGTERM'), 150); console.log('exit:' + await p.exited); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); // SIGTERM = 15 → 128 + 15 = 143. expect(result.stdout.toString().trim()).toBe('exit:143'); }); @@ -166,7 +166,7 @@ describe('bun-polyfill', () => { const out = await new Response(p.stdout).text(); console.log(out.length + ':' + code); })(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('1024:0'); }); @@ -197,7 +197,7 @@ describe('bun-polyfill', () => { const out = await new Response(p.stdout).text(); console.log(out.length + ':' + code); })().catch((e) => { console.log('THREW:' + e.message); }); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('1048576:0'); }, 15000); @@ -217,7 +217,7 @@ describe('bun-polyfill', () => { console.log(typeof server.stop === 'function' ? 'HAS_STOP' : 'NO_STOP'); console.log(typeof server.port === 'number' ? 'HAS_PORT' : 'NO_PORT'); server.stop(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const lines = result.stdout.toString().trim().split('\n'); expect(lines[0]).toBe('HAS_STOP'); expect(lines[1]).toBe('HAS_PORT'); @@ -237,7 +237,7 @@ describe('bun-polyfill', () => { require(${JSON.stringify(polyfillPath)}); Bun.spawn(['node', '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'] }); console.log('windowsHide:' + seen.windowsHide); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('windowsHide:true'); }); @@ -250,7 +250,7 @@ describe('bun-polyfill', () => { require(${JSON.stringify(polyfillPath)}); Bun.spawnSync(['node', '-e', '']); console.log('windowsHide:' + seen.windowsHide); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('windowsHide:true'); }); @@ -263,7 +263,7 @@ describe('bun-polyfill', () => { require(${JSON.stringify(polyfillPath)}); Bun.spawn(['node', '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: false }); console.log('windowsHide:' + seen.windowsHide); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.stdout.toString().trim()).toBe('windowsHide:false'); }); }); diff --git a/browse/test/config.test.ts b/browse/test/config.test.ts index 09aa40d15..c20bad8f2 100644 --- a/browse/test/config.test.ts +++ b/browse/test/config.test.ts @@ -85,7 +85,7 @@ describe('config', () => { // it (the exact bug the fix removed) would skip the guard here. const tmpDir = path.join(os.tmpdir(), `browse-gitignored-repo-test-${Date.now()}`); fs.mkdirSync(tmpDir, { recursive: true }); - Bun.spawnSync(['git', 'init'], { cwd: tmpDir, stdout: 'ignore', stderr: 'ignore' }); + Bun.spawnSync(['git', 'init'], { cwd: tmpDir, stdout: 'ignore', stderr: 'ignore', timeout: 30_000 }); fs.writeFileSync(path.join(tmpDir, '.gitignore'), '.gstack/\n'); const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') }); ensureStateDir(config); @@ -166,22 +166,22 @@ describe('config', () => { fs.mkdirSync(tmpDir, { recursive: true }); // Set up a real git repo - spawnSync('git', ['init', '-q'], { cwd: tmpDir }); - spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir }); - spawnSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir }); + spawnSync('git', ['init', '-q'], { cwd: tmpDir, timeout: 30_000 }); + spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir, timeout: 30_000 }); + spawnSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir, timeout: 30_000 }); // Write a global excludes file that ignores .gstack/ const excludesFile = path.join(tmpDir, 'global-gitignore'); fs.writeFileSync(excludesFile, '.gstack/\n'); - spawnSync('git', ['config', 'core.excludesFile', excludesFile], { cwd: tmpDir }); + spawnSync('git', ['config', 'core.excludesFile', excludesFile], { cwd: tmpDir, timeout: 30_000 }); // .gitignore exists but does NOT contain .gstack/ fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules/\n'); - spawnSync('git', ['add', '.gitignore'], { cwd: tmpDir }); - spawnSync('git', ['commit', '-qm', 'init'], { cwd: tmpDir }); + spawnSync('git', ['add', '.gitignore'], { cwd: tmpDir, timeout: 30_000 }); + spawnSync('git', ['commit', '-qm', 'init'], { cwd: tmpDir, timeout: 30_000 }); // Verify git knows .gstack/ is ignored - const check = spawnSync('git', ['check-ignore', '-q', '.gstack/'], { cwd: tmpDir }); + const check = spawnSync('git', ['check-ignore', '-q', '.gstack/'], { cwd: tmpDir, timeout: 30_000 }); expect(check.status).toBe(0); const config = resolveConfig({ BROWSE_STATE_FILE: path.join(tmpDir, '.gstack', 'browse.json') }); diff --git a/browse/test/file-permissions.test.ts b/browse/test/file-permissions.test.ts index 057d164fd..6a86e7f4d 100644 --- a/browse/test/file-permissions.test.ts +++ b/browse/test/file-permissions.test.ts @@ -87,7 +87,7 @@ describe('restrictDirectoryPermissions', () => { fs.mkdirSync(d); // System chmod, not fs.chmodSync: Bun masks the sticky bit off chmod/ // mkdir modes, so 0o1777 through the fs API lands as 0o777. - Bun.spawnSync(['chmod', '1777', d]); + Bun.spawnSync(['chmod', '1777', d], { timeout: 30_000 }); expect(fs.statSync(d).mode & 0o7777).toBe(0o1777); // fixture took restrictDirectoryPermissions(d); expect(fs.statSync(d).mode & 0o7777).toBe(0o1777); @@ -244,7 +244,7 @@ describe('mkdirSecure', () => { fs.mkdirSync(d); // System chmod: Bun's fs API masks the sticky bit off modes (see the // restrictDirectoryPermissions sticky-dir test). - Bun.spawnSync(['chmod', '1777', d]); + Bun.spawnSync(['chmod', '1777', d], { timeout: 30_000 }); expect(fs.statSync(d).mode & 0o7777).toBe(0o1777); // fixture took mkdirSecure(d); expect(fs.statSync(d).mode & 0o7777).toBe(0o1777); diff --git a/browse/test/findport.test.ts b/browse/test/findport.test.ts index 3255d240c..47c0182d8 100644 --- a/browse/test/findport.test.ts +++ b/browse/test/findport.test.ts @@ -160,7 +160,7 @@ describe('findPort / isPortAvailable', () => { } test(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const output = result.stdout.toString().trim(); // Confirms the polyfill's stop() is fire-and-forget — callers @@ -171,7 +171,7 @@ describe('findPort / isPortAvailable', () => { test('net.createServer approach does not have the race condition', async () => { // Prove the fix: net.createServer with proper async bind/close // releases the port cleanly - const result = Bun.spawnSync(['node', '-e', ` + const result = Bun.spawnSync(['node', '-e', ` // timeout in trailing options const net = require('net'); async function testFix() { @@ -205,7 +205,7 @@ describe('findPort / isPortAvailable', () => { } testFix(); - `], { stdout: 'pipe', stderr: 'pipe' }); + `], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const output = result.stdout.toString().trim(); expect(output).toBe('FIX_WORKS'); diff --git a/browse/test/gstack-config.test.ts b/browse/test/gstack-config.test.ts index 097e25f75..323eefad3 100644 --- a/browse/test/gstack-config.test.ts +++ b/browse/test/gstack-config.test.ts @@ -31,6 +31,7 @@ function run(args: string[] = [], extraEnv: Record = {}) { env, stdout: 'pipe', stderr: 'pipe', + timeout: 30_000, }); return { exitCode: result.exitCode, diff --git a/browse/test/gstack-update-check.test.ts b/browse/test/gstack-update-check.test.ts index 16bf7b851..6b78263f7 100644 --- a/browse/test/gstack-update-check.test.ts +++ b/browse/test/gstack-update-check.test.ts @@ -34,6 +34,7 @@ function run(extraEnv: Record = {}, args: string[] = []) { env, stdout: 'pipe', stderr: 'pipe', + timeout: 30_000, }); return { exitCode: result.exitCode, diff --git a/browse/test/temp-dirs.test.ts b/browse/test/temp-dirs.test.ts index c3ee14ffe..f534a2cb6 100644 --- a/browse/test/temp-dirs.test.ts +++ b/browse/test/temp-dirs.test.ts @@ -107,7 +107,7 @@ describe('untrustable TMPDIR values never widen the allowlist', () => { const r = Bun.spawnSync([ process.execPath, '-e', "import { TEMP_DIRS } from './browse/src/platform'; console.log(JSON.stringify(TEMP_DIRS));", - ], { env: { ...process.env, TMPDIR: tmpdir }, cwd: path.resolve(import.meta.dir, '..', '..') }); + ], { env: { ...process.env, TMPDIR: tmpdir }, cwd: path.resolve(import.meta.dir, '..', '..'), timeout: 30_000 }); return JSON.parse(r.stdout.toString().trim().split('\n').pop()!); }; diff --git a/browse/test/xvfb.test.ts b/browse/test/xvfb.test.ts index dab242a8d..840aa43c5 100644 --- a/browse/test/xvfb.test.ts +++ b/browse/test/xvfb.test.ts @@ -11,7 +11,7 @@ import { const HAS_XVFB = (() => { if (process.platform !== 'linux') return false; - const result = Bun.spawnSync(['which', 'Xvfb'], { stdout: 'pipe', stderr: 'pipe' }); + const result = Bun.spawnSync(['which', 'Xvfb'], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); return result.exitCode === 0; })(); diff --git a/ios-qa/daemon/test/cli-mint.test.ts b/ios-qa/daemon/test/cli-mint.test.ts index f5416bf88..53c07d012 100644 --- a/ios-qa/daemon/test/cli-mint.test.ts +++ b/ios-qa/daemon/test/cli-mint.test.ts @@ -14,7 +14,7 @@ const MINT_BIN = join(ROOT, 'bin', 'gstack-ios-qa-mint'); const DAEMON_BIN = join(ROOT, 'bin', 'gstack-ios-qa-daemon'); function runMint(args: string[]) { - return spawnSync(MINT_BIN, args, { stdio: 'pipe', encoding: 'utf-8' }); + return spawnSync(MINT_BIN, args, { stdio: 'pipe', encoding: 'utf-8', timeout: 30_000 }); } describe('bin/gstack-ios-qa-mint launcher', () => { @@ -112,6 +112,7 @@ describe('bin/gstack-ios-qa-daemon launcher', () => { stdio: 'pipe', encoding: 'utf-8', env: { PATH: '/usr/bin:/bin' }, + timeout: 30_000, }); expect(r.status).not.toBe(0); expect(r.stderr).toContain('bun'); diff --git a/ios-qa/scripts/gen-accessors.test.ts b/ios-qa/scripts/gen-accessors.test.ts index 0c5df37d5..1ed68e653 100644 --- a/ios-qa/scripts/gen-accessors.test.ts +++ b/ios-qa/scripts/gen-accessors.test.ts @@ -612,6 +612,7 @@ class AppState { '--output', outputDir, ], { encoding: 'utf8', + timeout: 30_000, env: { ...process.env, GSTACK_IOS_CACHE_ROOT: join(workDir, 'cache') }, }); expect(result.status).toBe(4); @@ -717,7 +718,7 @@ describe('render', () => { }); test('typechecks beside an internal @Observable app state using a comment marker', () => { - if (spawnSync('swiftc', ['--version'], { encoding: 'utf8' }).status !== 0) return; + if (spawnSync('swiftc', ['--version'], { encoding: 'utf8', timeout: 30_000 }).status !== 0) return; const coreSource = join(workDir, 'DebugBridgeCore.swift'); const coreModule = join(workDir, 'DebugBridgeCore.swiftmodule'); @@ -752,7 +753,7 @@ public final class StateServer { '-module-name', 'DebugBridgeCore', coreSource, '-emit-module-path', coreModule, - ], { encoding: 'utf8' }); + ], { encoding: 'utf8', timeout: 120_000 }); if (emitModule.status !== 0) { throw new Error(`failed to build DebugBridgeCore test stub:\n${emitModule.stderr}`); } @@ -775,7 +776,7 @@ ${render([{ '-D', 'DEBUG', '-I', workDir, appSource, - ], { encoding: 'utf8' }); + ], { encoding: 'utf8', timeout: 120_000 }); if (typecheck.status !== 0) { throw new Error(`generated accessor failed Swift type checking:\n${typecheck.stderr}`); } @@ -783,7 +784,7 @@ ${render([{ test('strict JSON typing and cross-model validate-before-apply restore run correctly', () => { if (process.platform !== 'darwin') return; - if (spawnSync('swiftc', ['--version'], { encoding: 'utf8' }).status !== 0) return; + if (spawnSync('swiftc', ['--version'], { encoding: 'utf8', timeout: 30_000 }).status !== 0) return; const coreSource = join(workDir, 'DebugBridgeCore.swift'); const coreModule = join(workDir, 'DebugBridgeCore.swiftmodule'); @@ -830,7 +831,7 @@ public final class StateServer { '-module-name', 'DebugBridgeCore', coreSource, '-emit-module-path', coreModule, '-o', coreLibrary, - ], { encoding: 'utf8' }); + ], { encoding: 'utf8', timeout: 120_000 }); if (emitCore.status !== 0) throw new Error(`failed to build runtime stub:\n${emitCore.stderr}`); const appSource = join(workDir, 'OptionalRoundTrip.swift'); @@ -915,10 +916,11 @@ struct Runner { const compile = spawnSync('swiftc', [ '-D', 'DEBUG', '-I', workDir, '-L', workDir, '-lDebugBridgeCore', '-parse-as-library', appSource, '-o', executable, - ], { encoding: 'utf8' }); + ], { encoding: 'utf8', timeout: 120_000 }); if (compile.status !== 0) throw new Error(`generated Optional accessor failed compilation:\n${compile.stderr}`); const run = spawnSync(executable, [], { encoding: 'utf8', + timeout: 30_000, env: { ...process.env, DYLD_LIBRARY_PATH: workDir }, }); if (run.status !== 0) throw new Error(`generated Optional accessor failed at runtime:\n${run.stderr}`); @@ -928,7 +930,7 @@ struct Runner { describe('SwiftSyntax generator parity', () => { test('isolates canonical markers and rejects inaccessible fields', () => { if (process.platform !== 'darwin') return; - if (spawnSync('swift', ['--version'], { encoding: 'utf8' }).status !== 0) return; + if (spawnSync('swift', ['--version'], { encoding: 'utf8', timeout: 30_000 }).status !== 0) return; const packageDir = join(import.meta.dir, 'gen-accessors-tool'); const inputDir = join(workDir, 'swift-syntax-input'); diff --git a/ios-qa/scripts/gen-accessors.ts b/ios-qa/scripts/gen-accessors.ts index 738e08364..315f2bde6 100644 --- a/ios-qa/scripts/gen-accessors.ts +++ b/ios-qa/scripts/gen-accessors.ts @@ -739,7 +739,7 @@ export function render(specs: AccessorSpec[], buildId: string, accessorHash: str function detectSwiftVersion(): string { if (process.env.SWIFT_VERSION) return process.env.SWIFT_VERSION; try { - const out = execSync('swift --version', { stdio: ['ignore', 'pipe', 'ignore'] }).toString(); + const out = execSync('swift --version', { stdio: ['ignore', 'pipe', 'ignore'], timeout: 30_000 }).toString(); const m = out.match(/Apple Swift version (\d+\.\d+\.\d+)/); if (m) return m[1]!; } catch { @@ -754,6 +754,7 @@ function detectToolGitRev(): string { return execSync('git rev-parse --short HEAD', { cwd: dirname(new URL(import.meta.url).pathname), stdio: ['ignore', 'pipe', 'ignore'], + timeout: 30_000, }).toString().trim(); } catch { return 'dev'; diff --git a/make-pdf/test/e2e/diagram-gate.test.ts b/make-pdf/test/e2e/diagram-gate.test.ts index a3473592a..37d7ffdf0 100644 --- a/make-pdf/test/e2e/diagram-gate.test.ts +++ b/make-pdf/test/e2e/diagram-gate.test.ts @@ -83,6 +83,7 @@ describe("diagram render gate", () => { env: { ...process.env, BROWSE_BIN }, stdout: "pipe", stderr: "pipe", + timeout: 120_000, }); const stderr = new TextDecoder().decode(run.stderr); if (run.exitCode !== 0) { diff --git a/test/artifacts-init-migration.test.ts b/test/artifacts-init-migration.test.ts index c09affffd..206c7653f 100644 --- a/test/artifacts-init-migration.test.ts +++ b/test/artifacts-init-migration.test.ts @@ -22,6 +22,7 @@ function runMigration(fakeHome: string): { code: number; stdout: string; stderr: env: { ...process.env, HOME: fakeHome }, stdout: 'pipe', stderr: 'pipe', + timeout: 30_000, }); return { code: proc.exitCode ?? -1, @@ -217,6 +218,7 @@ function runMigrationV140(fakeHome: string): { code: number; stdout: string; std env: { ...process.env, HOME: fakeHome }, stdout: 'pipe', stderr: 'pipe', + timeout: 30_000, }); return { code: proc.exitCode ?? -1, diff --git a/test/auq-error-fallback-hook.test.ts b/test/auq-error-fallback-hook.test.ts index 21505c04b..47c4816b4 100644 --- a/test/auq-error-fallback-hook.test.ts +++ b/test/auq-error-fallback-hook.test.ts @@ -82,6 +82,7 @@ function runHook(stdin: object, env: Record): { additionalContex input: JSON.stringify(stdin), encoding: 'utf-8', env: { PATH: process.env.PATH ?? '/usr/bin:/bin', ...env }, + timeout: 30_000, }); const parsed = JSON.parse(res.stdout || '{}'); return parsed.hookSpecificOutput ?? {}; diff --git a/test/bin-context-windows-slug.test.ts b/test/bin-context-windows-slug.test.ts index 4764cbcb1..a845229a5 100644 --- a/test/bin-context-windows-slug.test.ts +++ b/test/bin-context-windows-slug.test.ts @@ -69,8 +69,8 @@ describe("native slug fallback mirrors bin/gstack-slug", () => { ["https://gitlab.com/acme/Widget", "acme-Widget"], ] as const) { const cwd = fs.mkdtempSync(path.join(tmp, "repo-")); - spawnSync("git", ["init", "-q"], { cwd }); - spawnSync("git", ["remote", "add", "origin", url], { cwd }); + spawnSync("git", ["init", "-q"], { cwd, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", url], { cwd, timeout: 30_000 }); expect(slugFromEnvironment(path.join(tmp, "home2"), cwd)).toBe(want); } }); @@ -231,8 +231,8 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { const projectRoot = path.join(tmp, "realgit"); const subdir = path.join(projectRoot, "src", "deep"); fs.mkdirSync(subdir, { recursive: true }); - spawnSync("git", ["init", "-q", projectRoot]); - spawnSync("git", ["-C", projectRoot, "remote", "add", "origin", "https://github.com/foo/bar.git"]); + spawnSync("git", ["init", "-q", projectRoot], { timeout: 30_000 }); + spawnSync("git", ["-C", projectRoot, "remote", "add", "origin", "https://github.com/foo/bar.git"], { timeout: 30_000 }); expectBoth(subdir, "foo-bar"); }); @@ -269,10 +269,10 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { const outer = path.join(tmp, "outer-project"); const inner = path.join(outer, "vendor", "inner-lib"); fs.mkdirSync(inner, { recursive: true }); - spawnSync("git", ["init", "-q", outer]); - spawnSync("git", ["-C", outer, "remote", "add", "origin", "git@github.com:acme/outer.git"]); - spawnSync("git", ["init", "-q", inner]); - spawnSync("git", ["-C", inner, "remote", "add", "origin", "git@github.com:vendor/inner.git"]); + spawnSync("git", ["init", "-q", outer], { timeout: 30_000 }); + spawnSync("git", ["-C", outer, "remote", "add", "origin", "git@github.com:acme/outer.git"], { timeout: 30_000 }); + spawnSync("git", ["init", "-q", inner], { timeout: 30_000 }); + spawnSync("git", ["-C", inner, "remote", "add", "origin", "git@github.com:vendor/inner.git"], { timeout: 30_000 }); expectBoth(inner, "acme-outer"); }); @@ -286,8 +286,8 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { fs.mkdirSync(path.join(strayHome, ".git"), { recursive: true }); // empty — invalid repo const repo = path.join(strayHome, "work", "repo"); fs.mkdirSync(repo, { recursive: true }); - spawnSync("git", ["init", "-q", repo]); - spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/garrytan/gstack"]); + spawnSync("git", ["init", "-q", repo], { timeout: 30_000 }); + spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/garrytan/gstack"], { timeout: 30_000 }); expectBoth(repo, "garrytan-gstack"); expect(slugFromEnvironment(nativeHome(), repo)).not.toBe("strayhome"); }); @@ -299,9 +299,9 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { const outer = path.join(tmp, "outer-plain"); const inner = path.join(outer, "vendor", "inner-lib"); fs.mkdirSync(inner, { recursive: true }); - spawnSync("git", ["init", "-q", outer]); // no origin — marker-only repo - spawnSync("git", ["init", "-q", inner]); - spawnSync("git", ["-C", inner, "remote", "add", "origin", "git@github.com:vendor/inner.git"]); + spawnSync("git", ["init", "-q", outer], { timeout: 30_000 }); // no origin — marker-only repo + spawnSync("git", ["init", "-q", inner], { timeout: 30_000 }); + spawnSync("git", ["-C", inner, "remote", "add", "origin", "git@github.com:vendor/inner.git"], { timeout: 30_000 }); expectBoth(inner, "vendor-inner"); }); @@ -313,8 +313,8 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { fs.mkdirSync(path.join(strayHome, ".git"), { recursive: true }); const repo = path.join(strayHome, "git", "proj"); fs.mkdirSync(repo, { recursive: true }); - spawnSync("git", ["init", "-q", repo]); - spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/garrytan/gstack"]); + spawnSync("git", ["init", "-q", repo], { timeout: 30_000 }); + spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/garrytan/gstack"], { timeout: 30_000 }); const cacheDir = path.join(nativeHome(), "slug-cache"); fs.mkdirSync(cacheDir, { recursive: true }); @@ -335,8 +335,8 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { const inner = path.join(wrapper, "apps", "web"); fs.mkdirSync(inner, { recursive: true }); fs.writeFileSync(path.join(wrapper, "package.json"), '{"name":"wrapper"}\n'); - spawnSync("git", ["init", "-q", inner]); - spawnSync("git", ["-C", inner, "remote", "add", "origin", "https://github.com/acme/web.git"]); + spawnSync("git", ["init", "-q", inner], { timeout: 30_000 }); + spawnSync("git", ["-C", inner, "remote", "add", "origin", "https://github.com/acme/web.git"], { timeout: 30_000 }); const cacheDir = path.join(nativeHome(), "slug-cache"); fs.mkdirSync(cacheDir, { recursive: true }); @@ -361,8 +361,8 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { // fire even though cached == basename(project root). const repo = path.join(tmp, "stickyproj"); fs.mkdirSync(repo, { recursive: true }); - spawnSync("git", ["init", "-q", repo]); - spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/x/y.git"]); + spawnSync("git", ["init", "-q", repo], { timeout: 30_000 }); + spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/x/y.git"], { timeout: 30_000 }); const cacheDir = path.join(nativeHome(), "slug-cache"); fs.mkdirSync(cacheDir, { recursive: true }); @@ -379,8 +379,8 @@ describe("walk-up parity with bin/gstack-slug (outermost project root)", () => { // implementations must reject it and fall through to the basename. const repo = path.join(tmp, "dotty"); fs.mkdirSync(repo, { recursive: true }); - spawnSync("git", ["init", "-q", repo]); - spawnSync("git", ["-C", repo, "remote", "add", "origin", ".."]); + spawnSync("git", ["init", "-q", repo], { timeout: 30_000 }); + spawnSync("git", ["-C", repo, "remote", "add", "origin", ".."], { timeout: 30_000 }); expectBoth(repo, "dotty"); }); diff --git a/test/binding-template-drift.test.ts b/test/binding-template-drift.test.ts index 53f47f6a9..41e3c9d1c 100644 --- a/test/binding-template-drift.test.ts +++ b/test/binding-template-drift.test.ts @@ -102,7 +102,7 @@ describe('content-binding template drift', () => { const abort = spawnSync('bash', ['-c', scriptFor( 'clean body\n', 'body with UNTRUSTED TRACKER CONTENT banner leak\n', - )], { encoding: 'utf-8' }); + )], { encoding: 'utf-8', timeout: 30_000 }); expect(abort.stderr).toContain('ABORT: envelope banner leaked'); expect(abort.stdout).not.toContain('banner tripwire clean'); @@ -110,7 +110,7 @@ describe('content-binding template drift', () => { const clean = spawnSync('bash', ['-c', scriptFor( 'clean body\n', 'also clean body\n', - )], { encoding: 'utf-8' }); + )], { encoding: 'utf-8', timeout: 30_000 }); expect(clean.stdout).toContain('banner tripwire clean'); expect(clean.stderr).not.toContain('ABORT'); } finally { diff --git a/test/brain-sync.test.ts b/test/brain-sync.test.ts index 33eff441a..96e50d01c 100644 --- a/test/brain-sync.test.ts +++ b/test/brain-sync.test.ts @@ -43,12 +43,13 @@ function run(argv: string[], opts: { env?: Record; input?: strin encoding: 'utf-8', input: opts.input, cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout || '', stderr: res.stderr || '', status: res.status ?? -1 }; } function git(args: string[], cwd?: string) { - const res = spawnSync('git', args, { cwd: cwd || tmpHome, encoding: 'utf-8' }); + const res = spawnSync('git', args, { cwd: cwd || tmpHome, encoding: 'utf-8', timeout: 30_000 }); return { stdout: res.stdout || '', stderr: res.stderr || '', status: res.status ?? -1 }; } @@ -77,7 +78,7 @@ function seedSpool(record: string): string { beforeEach(() => { tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-sync-home-')); bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-sync-remote-')); - spawnSync('git', ['init', '--bare', '-q', '-b', 'main', bareRemote]); + spawnSync('git', ['init', '--bare', '-q', '-b', 'main', bareRemote], { timeout: 30_000 }); }); afterEach(() => { @@ -198,6 +199,7 @@ describe('gstack-brain-enqueue', () => { const r = spawnSync(path.join(BIN, 'gstack-brain-enqueue'), [`file-${i}.jsonl`], { env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', + timeout: 30_000, }); resolve(); })); @@ -245,7 +247,7 @@ describe('gstack-jsonl-merge', () => { const lines = fs.readFileSync(ours, 'utf-8').trim().split('\n'); expect(lines.length).toBe(3); // Order is deterministic (sha256 of each line). - const again = spawnSync(path.join(BIN, 'gstack-jsonl-merge'), [base, ours, theirs]); + const again = spawnSync(path.join(BIN, 'gstack-jsonl-merge'), [base, ours, theirs], { timeout: 30_000 }); // (re-running doesn't change the order since same input → same output) }); }); @@ -271,7 +273,7 @@ describe('init + sync + restore round-trip', () => { test('refuses init on different remote', () => { run(['gstack-artifacts-init', '--remote', bareRemote]); const otherRemote = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-other-')); - spawnSync('git', ['init', '--bare', '-q', '-b', 'main', otherRemote]); + spawnSync('git', ['init', '--bare', '-q', '-b', 'main', otherRemote], { timeout: 30_000 }); const r = run(['gstack-artifacts-init', '--remote', otherRemote]); expect(r.status).not.toBe(0); expect(r.stderr).toContain('already a git repo pointing at'); @@ -288,7 +290,7 @@ describe('init + sync + restore round-trip', () => { const r = run(['gstack-brain-sync', '--once']); expect(r.status).toBe(0); // Check the remote got the commit. - const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' }); + const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8', timeout: 30_000 }); expect(log.stdout).toMatch(/sync: 1 file/); }); @@ -311,7 +313,7 @@ describe('init + sync + restore round-trip', () => { const restored = fs.readFileSync(path.join(machineB, 'projects/myproj/learnings.jsonl'), 'utf-8'); expect(restored).toContain('machine A wisdom'); // Merge drivers re-registered on B. - const cfg = spawnSync('git', ['-C', machineB, 'config', '--get', 'merge.jsonl-append.driver'], { encoding: 'utf-8' }); + const cfg = spawnSync('git', ['-C', machineB, 'config', '--get', 'merge.jsonl-append.driver'], { encoding: 'utf-8', timeout: 30_000 }); expect(cfg.stdout).toContain('gstack-jsonl-merge'); fs.rmSync(machineB, { recursive: true, force: true }); }); @@ -399,7 +401,7 @@ describe('gstack-brain-sync egress receipt gate', () => { // No local commit was created. expect(git(['rev-list', '--count', 'HEAD']).stdout.trim()).toBe(commitsBefore); // Nothing reached the remote. - const remoteLog = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' }); + const remoteLog = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8', timeout: 30_000 }); expect(remoteLog.stdout).not.toMatch(/sync: 1 file/); const status = JSON.parse(fs.readFileSync(path.join(tmpHome, '.brain-sync-status.json'), 'utf-8')); expect(status.status).toBe('push_failed'); @@ -411,7 +413,7 @@ describe('gstack-brain-sync egress receipt gate', () => { // Next run (ledger writable again) drains the intact queue and pushes. const retry = run(['gstack-brain-sync', '--once']); expect(retry.status).toBe(0); - const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' }); + const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8', timeout: 30_000 }); expect(log.stdout).toMatch(/sync: 1 file/); }); @@ -583,7 +585,7 @@ describe('#2549 queue integrity', () => { expect(r.status).toBe(0); expect(spoolText()).not.toContain('learnings.jsonl'); // synced, removed expect(spoolText()).toContain('timeline.jsonl'); // held, retained - const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' }); + const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8', timeout: 30_000 }); expect(log.stdout).toMatch(/sync: 1 file/); }); @@ -619,7 +621,7 @@ describe('#2549 queue integrity', () => { fs.rmSync(hook); const retry = run(['gstack-brain-sync', '--once']); expect(retry.status).toBe(0); - const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' }); + const log = spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8', timeout: 30_000 }); expect(log.stdout).toMatch(/sync: 1 file/); expect(git(['rev-list', '--count', 'origin/main..HEAD']).stdout.trim()).toBe('0'); }); @@ -751,7 +753,7 @@ describe('C12 spool queue', () => { run(['gstack-config', 'set', 'artifacts_sync_mode', mode]); } const remoteLog = () => - spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8' }).stdout; + spawnSync('git', ['--git-dir=' + bareRemote, 'log', '--oneline'], { encoding: 'utf-8', timeout: 30_000 }).stdout; test('two rapid enqueues of different paths create two spool files; one drain syncs both', () => { initWithMode('full'); diff --git a/test/branch-slug-hygiene.test.ts b/test/branch-slug-hygiene.test.ts index 58c7cccaf..409c5177b 100644 --- a/test/branch-slug-hygiene.test.ts +++ b/test/branch-slug-hygiene.test.ts @@ -37,7 +37,7 @@ const FILENAME_PREFIX = /\$\{?_BRANCH\}?[A-Za-z0-9._-]*\.(?:jsonl|json|md|txt|lo function renderedSkillFiles(): string[] { const out = execSync( `find "${ROOT}" -name 'SKILL.md' -not -path '*/node_modules/*' -not -path '*/.claude/*' ; find "${ROOT}" -path '*/sections/*.md' -not -path '*/node_modules/*' -not -path '*/.claude/*'`, - { encoding: 'utf-8' }, + { encoding: 'utf-8', timeout: 30_000 }, ); return out.split('\n').filter(Boolean); } @@ -91,18 +91,18 @@ describe('branch slug hygiene (#2550, #1851)', () => { const env = { ...process.env, GSTACK_HOME: home }; execSync( 'git init -q && git -c user.email=t@t -c user.name=t commit -q --allow-empty -m init && git checkout -q -b feat/slug-hygiene', - { cwd: repo, encoding: 'utf-8' }, + { cwd: repo, encoding: 'utf-8', timeout: 30_000 }, ); // Writer: the real gstack-review-log (canonicalizes via gstack-slug). execSync( `"${path.join(ROOT, 'bin', 'gstack-review-log')}" '{"skill":"ship","status":"ok"}'`, - { cwd: repo, env, encoding: 'utf-8' }, + { cwd: repo, env, encoding: 'utf-8', timeout: 30_000 }, ); // The slug-canonical filename must exist; the raw form must not. const slugVars = execSync(`"${path.join(ROOT, 'bin', 'gstack-slug')}"`, { - cwd: repo, env, encoding: 'utf-8', + cwd: repo, env, encoding: 'utf-8', timeout: 30_000, }); const slug = slugVars.match(/^SLUG=(.*)$/m)![1]; const branch = slugVars.match(/^BRANCH=(.*)$/m)![1]; @@ -120,7 +120,7 @@ describe('branch slug hygiene (#2550, #1851)', () => { .find((l) => l.includes('-reviews.jsonl'))!; const script = `_PROJ="${proj}"\nBRANCH="${branch}"\n${probeLine.trim()}`; const out = execSync(`bash -c '${script.replace(/'/g, `'\\''`)}'`, { - cwd: repo, encoding: 'utf-8', + cwd: repo, encoding: 'utf-8', timeout: 30_000, }); expect(out).toContain('REVIEWS: 1 entries'); diff --git a/test/builder-profile.test.ts b/test/builder-profile.test.ts index ba00b8303..ae52c2087 100644 --- a/test/builder-profile.test.ts +++ b/test/builder-profile.test.ts @@ -16,7 +16,7 @@ function runProfile(): Record { encoding: 'utf-8', timeout: 15000, }; - const stdout = execSync(`${BIN}/gstack-builder-profile`, execOpts).trim(); + const stdout = execSync(`${BIN}/gstack-builder-profile`, execOpts).trim(); // timeout via execOpts const result: Record = {}; for (const line of stdout.split('\n')) { const idx = line.indexOf(':'); diff --git a/test/code-intelligence.test.ts b/test/code-intelligence.test.ts index 56dff9cba..010e85f0c 100644 --- a/test/code-intelligence.test.ts +++ b/test/code-intelligence.test.ts @@ -153,9 +153,9 @@ describe("session-start indexing offer (suggest)", () => { home = fs.mkdtempSync(path.join(os.tmpdir(), "ci-home-")); repo = fs.mkdtempSync(path.join(os.tmpdir(), "ci-repo-")); env = { ...process.env, GSTACK_HOME: home }; - Bun.spawnSync(["git", "init", "-q", repo]); + Bun.spawnSync(["git", "init", "-q", repo], { timeout: 30_000 }); for (const name of ["a.ts", "b.ts", "c.ts"]) fs.writeFileSync(path.join(repo, name), "x\n"); - Bun.spawnSync(["git", "-C", repo, "add", "-A"]); + Bun.spawnSync(["git", "-C", repo, "add", "-A"], { timeout: 30_000 }); }); afterEach(() => { fs.rmSync(home, { recursive: true, force: true }); @@ -971,9 +971,9 @@ exit 1 function makeRepoWithFiles(count: number): string { const repo = fs.mkdtempSync(path.join(os.tmpdir(), "ci-cli-suggest-")); - Bun.spawnSync(["git", "init", "-q", repo]); + Bun.spawnSync(["git", "init", "-q", repo], { timeout: 30_000 }); for (let i = 0; i < count; i++) fs.writeFileSync(path.join(repo, `f${i}.ts`), "x\n"); - Bun.spawnSync(["git", "-C", repo, "add", "-A"]); + Bun.spawnSync(["git", "-C", repo, "add", "-A"], { timeout: 30_000 }); return repo; } diff --git a/test/codex-e2e-plan-format.test.ts b/test/codex-e2e-plan-format.test.ts index 3bfce0fdd..4cc591b1c 100644 --- a/test/codex-e2e-plan-format.test.ts +++ b/test/codex-e2e-plan-format.test.ts @@ -43,7 +43,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const CODEX_AVAILABLE = (() => { try { - const result = Bun.spawnSync(['which', 'codex']); + const result = Bun.spawnSync(['which', 'codex'], { timeout: 30_000 }); return result.exitCode === 0; } catch { return false; } })(); diff --git a/test/codex-e2e-recommendation-substance.test.ts b/test/codex-e2e-recommendation-substance.test.ts index f4a6530c0..8d7c9404f 100644 --- a/test/codex-e2e-recommendation-substance.test.ts +++ b/test/codex-e2e-recommendation-substance.test.ts @@ -31,7 +31,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const CODEX_AVAILABLE = (() => { try { - return Bun.spawnSync(['which', 'codex']).exitCode === 0; + return Bun.spawnSync(['which', 'codex'], { timeout: 30_000 }).exitCode === 0; } catch { return false; } diff --git a/test/codex-e2e-sol-scope.test.ts b/test/codex-e2e-sol-scope.test.ts index dd3c52aee..2603cdd6e 100644 --- a/test/codex-e2e-sol-scope.test.ts +++ b/test/codex-e2e-sol-scope.test.ts @@ -21,12 +21,12 @@ import { EvalCollector } from './helpers/eval-store'; import { selectTests, detectBaseBranch, getChangedFiles, GLOBAL_TOUCHFILES } from './helpers/touchfiles'; const ROOT = path.resolve(import.meta.dir, '..'); -const CODEX_AVAILABLE = spawnSync('which', ['codex']).status === 0; +const CODEX_AVAILABLE = spawnSync('which', ['codex'], { timeout: 30_000 }).status === 0; // The run pins the model with --ignore-user-config; older codex CLIs reject // the flag with an argv error indistinguishable from a Sol regression, so // probe support and skip (not fail) on old CLIs. const IGNORE_USER_CONFIG_SUPPORTED = CODEX_AVAILABLE - && (spawnSync('codex', ['exec', '--help'], { encoding: 'utf8' }).stdout ?? '').includes('--ignore-user-config'); + && (spawnSync('codex', ['exec', '--help'], { encoding: 'utf8', timeout: 120_000 }).stdout ?? '').includes('--ignore-user-config'); const evalsEnabled = !!process.env.EVALS; // External-service test — periodic tier only (CLAUDE.md tiering rule 3). The // positive guard shape below is what classifyPaidTestFile greps to exclude diff --git a/test/codex-e2e.test.ts b/test/codex-e2e.test.ts index dc51d322d..2b8fc55fc 100644 --- a/test/codex-e2e.test.ts +++ b/test/codex-e2e.test.ts @@ -32,7 +32,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const CODEX_AVAILABLE = (() => { try { - const result = Bun.spawnSync(['which', 'codex']); + const result = Bun.spawnSync(['which', 'codex'], { timeout: 30_000 }); return result.exitCode === 0; } catch { return false; } })(); diff --git a/test/codex-generation-model.test.ts b/test/codex-generation-model.test.ts index 9f04df296..294b9ec77 100644 --- a/test/codex-generation-model.test.ts +++ b/test/codex-generation-model.test.ts @@ -116,6 +116,7 @@ model = "gpt-5.6-terra" cwd: ROOT, encoding: 'utf8', env: { ...process.env, CODEX_HOME: home }, + timeout: 30_000, }); expect(ok.status).toBe(0); expect(ok.stdout).toBe(`gpt-5.6-sol\t${path.join(home, 'config.toml')}\n`); @@ -123,6 +124,7 @@ model = "gpt-5.6-terra" const bad = spawnSync('bun', ['run', 'scripts/resolve-codex-generation-model.ts', '--explicit', 'llama-local'], { cwd: ROOT, encoding: 'utf8', + timeout: 30_000, }); expect(bad.status).not.toBe(0); expect(bad.stderr).toContain('Unknown model'); diff --git a/test/codex-hardening.test.ts b/test/codex-hardening.test.ts index 27b6a75fd..ee170d88f 100644 --- a/test/codex-hardening.test.ts +++ b/test/codex-hardening.test.ts @@ -297,7 +297,7 @@ describe('gstack-codex-probe: timeout wrapper + namespace hygiene', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-watchdog-')); try { const which = (tool: string) => - spawnSync('bash', ['-c', `command -v ${tool}`]).stdout.toString().trim() || `/bin/${tool}`; + spawnSync('bash', ['-c', `command -v ${tool}`], { timeout: 30_000 }).stdout.toString().trim() || `/bin/${tool}`; fs.symlinkSync(which('bash'), path.join(dir, 'bash')); fs.symlinkSync(which('sleep'), path.join(dir, 'sleep')); const r = runProbe({ diff --git a/test/codex-resume-flag-semantics.test.ts b/test/codex-resume-flag-semantics.test.ts index 9075e0489..8f702ef2b 100644 --- a/test/codex-resume-flag-semantics.test.ts +++ b/test/codex-resume-flag-semantics.test.ts @@ -15,7 +15,7 @@ import { describe, test, expect } from 'bun:test'; import { spawnSync } from 'child_process'; -const codexPath = spawnSync('which', ['codex'], { encoding: 'utf-8' }).stdout.trim(); +const codexPath = spawnSync('which', ['codex'], { encoding: 'utf-8', timeout: 30_000 }).stdout.trim(); const codexAvailable = codexPath.length > 0; describe.skipIf(!codexAvailable)( diff --git a/test/codex-web-search-flag.test.ts b/test/codex-web-search-flag.test.ts index ed7c4f612..217bc5796 100644 --- a/test/codex-web-search-flag.test.ts +++ b/test/codex-web-search-flag.test.ts @@ -23,7 +23,7 @@ function grepRepo(pattern: string, includes: string[]): string[] { const includeArgs = includes.map((i) => `--include='${i}'`).join(' '); const out = execSync( `grep -rln ${includeArgs} -e '${pattern}' "${ROOT}" || true`, - { encoding: 'utf-8' }, + { encoding: 'utf-8', timeout: 30_000 }, ); return out .split('\n') diff --git a/test/context-bill.test.ts b/test/context-bill.test.ts index 5a05db01f..82afda6d9 100644 --- a/test/context-bill.test.ts +++ b/test/context-bill.test.ts @@ -718,7 +718,7 @@ describe("CLI plumbing", () => { }); it("bin/gstack-context-bill runs standalone", () => { - const result = Bun.spawnSync([path.join(ROOT, "bin", "gstack-context-bill"), TREE_A]); + const result = Bun.spawnSync([path.join(ROOT, "bin", "gstack-context-bill"), TREE_A], { timeout: 30_000 }); expect(result.exitCode).toBe(0); expect(result.stdout.toString()).toContain("ALWAYS-ON"); expect(result.stdout.toString()).toContain("EAGER"); diff --git a/test/design-flag-utils.test.ts b/test/design-flag-utils.test.ts index c891bf039..f5882dc67 100644 --- a/test/design-flag-utils.test.ts +++ b/test/design-flag-utils.test.ts @@ -99,7 +99,7 @@ describe("normalizeIntFlag CLI wrapper (exit-1 semantics)", () => { const v = normalizeIntFlag(${rawExpr}, ${specExpr}); console.log("VALUE:" + v); `; - const res = spawnSync("bun", ["-e", script], { encoding: "utf-8", cwd: ROOT }); + const res = spawnSync("bun", ["-e", script], { encoding: "utf-8", cwd: ROOT, timeout: 30_000 }); return { status: res.status ?? -1, stderr: res.stderr ?? "" }; } diff --git a/test/diagram-render-drift.test.ts b/test/diagram-render-drift.test.ts index 231f13a42..48b1574e6 100644 --- a/test/diagram-render-drift.test.ts +++ b/test/diagram-render-drift.test.ts @@ -86,7 +86,7 @@ describe("diagram-render bundle drift", () => { "deep: fresh build reproduces committed dist", async () => { const before = await Bun.file(BUILD_INFO).json(); - const proc = Bun.spawnSync(["bun", "run", "scripts/build.ts"], { cwd: ROOT }); + const proc = Bun.spawnSync(["bun", "run", "scripts/build.ts"], { cwd: ROOT, timeout: 120_000 }); expect(proc.exitCode).toBe(0); const after = await Bun.file(BUILD_INFO).json(); expect(after.sha256).toBe(before.sha256); diff --git a/test/distill-apply.test.ts b/test/distill-apply.test.ts index e46781c21..3012776f8 100644 --- a/test/distill-apply.test.ts +++ b/test/distill-apply.test.ts @@ -60,7 +60,7 @@ function run(args: string[]): { stdout: string; stderr: string; status: number } env.GSTACK_STATE_ROOT = stateRoot; env.GSTACK_QUESTION_LOG_NO_DERIVE = '1'; delete env.GSTACK_HOME; - const res = spawnSync(BIN, args, { env, encoding: 'utf-8', cwd: fixtureCwd }); + const res = spawnSync(BIN, args, { env, encoding: 'utf-8', cwd: fixtureCwd, timeout: 30_000 }); return { stdout: res.stdout ?? '', stderr: res.stderr ?? '', diff --git a/test/distill-free-text.test.ts b/test/distill-free-text.test.ts index a79490831..b2d543cf8 100644 --- a/test/distill-free-text.test.ts +++ b/test/distill-free-text.test.ts @@ -47,6 +47,7 @@ function run(args: string[]): { stdout: string; stderr: string; status: number } env: makeEnv(), encoding: 'utf-8', cwd: fixtureCwd, + timeout: 30_000, }); return { stdout: res.stdout ?? '', @@ -75,6 +76,7 @@ function writeAuqOtherEvent(text: string): void { env: makeEnv(), cwd: fixtureCwd, encoding: 'utf-8', + timeout: 30_000, }, ); } @@ -150,7 +152,7 @@ describe('no-event paths', () => { tool_use_id: 'tu-x', }), ], - { env: makeEnv(), cwd: fixtureCwd, encoding: 'utf-8' }, + { env: makeEnv(), cwd: fixtureCwd, encoding: 'utf-8', timeout: 30_000 }, ); const r = run([]); expect(r.status).toBe(0); @@ -169,7 +171,7 @@ describe('--dry-run', () => { // Strip ANTHROPIC_API_KEY to prove no API call happens. const env = makeEnv(); delete env.ANTHROPIC_API_KEY; - const res = spawnSync(BIN, ['--dry-run'], { env, cwd: fixtureCwd, encoding: 'utf-8' }); + const res = spawnSync(BIN, ['--dry-run'], { env, cwd: fixtureCwd, encoding: 'utf-8', timeout: 30_000 }); expect(res.status).toBe(0); expect(res.stdout).toContain('DISTILL PROMPT'); expect(res.stdout).toContain('always include tests'); @@ -185,7 +187,7 @@ describe('API auth', () => { writeAuqOtherEvent('Some free text response that needs distilling'); const env = makeEnv(); delete env.ANTHROPIC_API_KEY; - const res = spawnSync(BIN, [], { env, cwd: fixtureCwd, encoding: 'utf-8' }); + const res = spawnSync(BIN, [], { env, cwd: fixtureCwd, encoding: 'utf-8', timeout: 30_000 }); expect(res.status).not.toBe(0); expect(res.stderr).toMatch(/ANTHROPIC_API_KEY/); expect(res.stderr).toMatch(/separate billing/); diff --git a/test/egress-receipt.test.ts b/test/egress-receipt.test.ts index 4bbc16d5f..07a9e90b6 100644 --- a/test/egress-receipt.test.ts +++ b/test/egress-receipt.test.ts @@ -222,12 +222,12 @@ describe('gstack-egress-receipt shell bridge', () => { fs.writeFileSync(payload, '[{"v":1}]'); const write = spawnSync(bin, ['write', '--sink', 'telemetry-sync', '--host', '127.0.0.1:8399', '--class', 'telemetry-events', '--payload-file', payload, '--consent', 'telemetry=community'], - { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home } }); + { encoding: 'utf-8', timeout: 30_000, env: { ...process.env, GSTACK_HOME: home } }); expect(write.status).toBe(0); const id = write.stdout.trim(); expect(id).toMatch(/^[0-9a-f]{64}$/); const outcome = spawnSync(bin, ['outcome', id, '204'], - { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home } }); + { encoding: 'utf-8', timeout: 30_000, env: { ...process.env, GSTACK_HOME: home } }); expect(outcome.status).toBe(0); const receipts = listReceipts(home); expect(receipts.length).toBe(1); @@ -239,7 +239,7 @@ describe('gstack-egress-receipt shell bridge', () => { test('--no-payload records sha256:null (git-class: a subprocess owns the bytes)', () => { const write = spawnSync(bin, ['write', '--sink', 'brain-sync', '--host', 'github.com', '--class', 'git-push', '--no-payload', '--consent', 'artifacts_sync_mode=auto'], - { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home } }); + { encoding: 'utf-8', timeout: 30_000, env: { ...process.env, GSTACK_HOME: home } }); expect(write.status).toBe(0); const receipts = listReceipts(home); expect(receipts.length).toBe(1); @@ -251,7 +251,7 @@ describe('gstack-egress-receipt shell bridge', () => { if (!canRevokeWrites()) return; // chmod is advisory here (win32, root, DAC-override containers) fs.mkdirSync(path.join(home, 'security'), { recursive: true, mode: 0o500 }); const write = spawnSync(bin, ['write', '--sink', 's', '--host', 'h', '--class', 'c', '--no-payload'], - { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home } }); + { encoding: 'utf-8', timeout: 30_000, env: { ...process.env, GSTACK_HOME: home } }); expect(write.status).toBe(3); expect(write.stderr).toContain('EGRESS_RECEIPT_FAILED'); fs.chmodSync(path.join(home, 'security'), 0o700); diff --git a/test/empty-find-fallthrough.test.ts b/test/empty-find-fallthrough.test.ts index 10782ae9d..634c644f0 100644 --- a/test/empty-find-fallthrough.test.ts +++ b/test/empty-find-fallthrough.test.ts @@ -35,7 +35,7 @@ describe('empty find must not fall through to cwd (#2483)', () => { test('no resolver emits a bare `xargs ls -t` (must be `xargs -r ls -t`)', () => { const out = execSync( `grep -rn "xargs ls -t" "${path.join(ROOT, 'scripts')}" "${path.join(ROOT, 'bin')}" || true`, - { encoding: 'utf-8' }, + { encoding: 'utf-8', timeout: 30_000 }, ); expect(out.trim()).toBe(''); }); @@ -67,6 +67,7 @@ describe('empty find must not fall through to cwd (#2483)', () => { const out = execSync(`bash -c '${script.replace(/'/g, `'\\''`)}'`, { cwd, encoding: 'utf-8', + timeout: 30_000, }); expect(out).toContain('LATEST_CP=[]'); expect(out).not.toContain('DECOY.md'); @@ -79,7 +80,7 @@ describe('empty find must not fall through to cwd (#2483)', () => { test('no generated SKILL.md carries the unguarded form', () => { const out = execSync( `grep -rln "xargs ls -t" --include=SKILL.md "${ROOT}" || true`, - { encoding: 'utf-8' }, + { encoding: 'utf-8', timeout: 30_000 }, ); // node_modules and vendored trees are not generated output; nothing in // the repo's generated skills may carry the unguarded form. diff --git a/test/eval-budgets-policy.test.ts b/test/eval-budgets-policy.test.ts index 3d39c0ed1..6e7bff921 100644 --- a/test/eval-budgets-policy.test.ts +++ b/test/eval-budgets-policy.test.ts @@ -41,7 +41,7 @@ describe('eval budget tiers', () => { }); test('no paid-test timeout literal exceeds the ceiling tier', () => { - const out = spawnSync('git', ['ls-files', 'test/*.test.ts'], { cwd: ROOT, encoding: 'utf-8' }); + const out = spawnSync('git', ['ls-files', 'test/*.test.ts'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); const files = out.stdout.split('\n').filter((f) => f && isPaidTestFile(f)); expect(files.length).toBeGreaterThan(50); // scan-rot guard diff --git a/test/eval-list-cli.test.ts b/test/eval-list-cli.test.ts index 536e36617..cffc68e09 100644 --- a/test/eval-list-cli.test.ts +++ b/test/eval-list-cli.test.ts @@ -65,6 +65,7 @@ function runEvalList(...args: string[]): { stdout: string; stderr: string; statu GSTACK_HOME: path.join(tmpHome, '.gstack'), }, encoding: 'utf-8', + timeout: 30_000, }); return { stdout: result.stdout ?? '', diff --git a/test/fs-utils.test.ts b/test/fs-utils.test.ts index 6959b8a57..f15c45199 100644 --- a/test/fs-utils.test.ts +++ b/test/fs-utils.test.ts @@ -74,11 +74,11 @@ describe("swept mkdirp sites under bun-on-Windows EEXIST semantics (#2635)", () fs.mkdirSync(work, { recursive: true }); const payload = '{"decision":"eexist probe","rationale":"r","scope":"repo","source":"user"}'; const env = { ...process.env, HOME: base }; - const first = spawnSync("bun", [DECISION_LOG, payload], { cwd: work, encoding: "utf8", env }); + const first = spawnSync("bun", [DECISION_LOG, payload], { cwd: work, encoding: "utf8", env, timeout: 30_000 }); expect(first.status).toBe(0); const second = spawnSync( "bun", ["--preload", EEXIST_PRELOAD, DECISION_LOG, payload], - { cwd: work, encoding: "utf8", env }, + { cwd: work, encoding: "utf8", env, timeout: 30_000 }, ); expect(second.status).toBe(0); expect(second.stderr ?? "").not.toContain("EEXIST"); @@ -93,7 +93,7 @@ describe("install-prepush-hook under bun-on-Windows EEXIST semantics (#2635)", ( const base = tmpdir(); try { const repo = path.join(base, "repo"); - spawnSync("git", ["init", "-q", repo]); + spawnSync("git", ["init", "-q", repo], { timeout: 30_000 }); const hookDir = path.join(repo, ".git", "hooks"); fs.mkdirSync(hookDir, { recursive: true }); const hookPath = path.join(hookDir, "pre-push"); @@ -105,6 +105,7 @@ describe("install-prepush-hook under bun-on-Windows EEXIST semantics (#2635)", ( const r = spawnSync("bun", ["--preload", EEXIST_PRELOAD, REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", + timeout: 30_000, }); expect(r.status).toBe(0); expect(r.stderr ?? "").not.toContain("EEXIST"); diff --git a/test/gbrain-detect-install.test.ts b/test/gbrain-detect-install.test.ts index 725eb9bdc..9109ec304 100644 --- a/test/gbrain-detect-install.test.ts +++ b/test/gbrain-detect-install.test.ts @@ -51,6 +51,7 @@ function run(bin: string, args: string[], opts: RunOpts = {}) { env, cwd: opts.cwd, encoding: 'utf-8', + timeout: 30_000, }); return { stdout: (res.stdout || '').trim(), diff --git a/test/gbrain-init-rollback.test.ts b/test/gbrain-init-rollback.test.ts index 39777e03a..747cac8a9 100644 --- a/test/gbrain-init-rollback.test.ts +++ b/test/gbrain-init-rollback.test.ts @@ -122,6 +122,7 @@ echo "ok" HOME: env.home, PATH: `${env.bindir}:/usr/bin:/bin`, }, + timeout: 30_000, }); return { exitCode: result.status ?? 1, diff --git a/test/gbrain-init-voyage-code-3.test.ts b/test/gbrain-init-voyage-code-3.test.ts index 7365d8e1c..be73e26b3 100644 --- a/test/gbrain-init-voyage-code-3.test.ts +++ b/test/gbrain-init-voyage-code-3.test.ts @@ -121,6 +121,7 @@ gbrain init --pglite --json "$@" const result = spawnSync(shell, ["-c", script], { encoding: "utf-8", env: baseEnv, + timeout: 30_000, }); if (result.status !== 0) { throw new Error(`init script exited ${result.status}: ${result.stderr}`); @@ -133,7 +134,7 @@ function lastArgc(env: FakeEnv): number { return parseInt(lines[lines.length - 1], 10); } -const HAVE_ZSH = spawnSync("zsh", ["-c", "true"]).status === 0; +const HAVE_ZSH = spawnSync("zsh", ["-c", "true"], { timeout: 30_000 }).status === 0; describe("voyage-code-3 default for gstack-driven PGLite init", () => { it("passes voyage-code-3 flags when VOYAGE_API_KEY is set", () => { @@ -193,6 +194,7 @@ gbrain init --pglite --json $GBRAIN_EMBED_FLAGS const result = spawnSync("zsh", ["-c", brokenShape], { encoding: "utf-8", env: { ...process.env, HOME: env.home, PATH: `${env.bindir}:/usr/bin:/bin` }, + timeout: 30_000, }); expect(result.status).toBe(0); expect(lastArgc(env)).toBe(4); // init, --pglite, --json, "" diff --git a/test/gbrain-lib-verify.test.ts b/test/gbrain-lib-verify.test.ts index 64c88e8f3..2a29e5af7 100644 --- a/test/gbrain-lib-verify.test.ts +++ b/test/gbrain-lib-verify.test.ts @@ -28,6 +28,7 @@ function runVerify(arg: string, stdin?: string) { const res = spawnSync(VERIFY, arg === '' ? [] : [arg], { input: stdin, encoding: 'utf-8', + timeout: 30_000, }); return { stdout: (res.stdout || '').trim(), @@ -43,6 +44,7 @@ function runLibSnippet(snippet: string, stdin: string = '') { const res = spawnSync('bash', ['-c', script], { input: stdin, encoding: 'utf-8', + timeout: 30_000, }); return { stdout: (res.stdout || '').trim(), diff --git a/test/gbrain-local-status.test.ts b/test/gbrain-local-status.test.ts index 85bebd2a0..a81059f85 100644 --- a/test/gbrain-local-status.test.ts +++ b/test/gbrain-local-status.test.ts @@ -538,6 +538,7 @@ describe("lib/gbrain-local-status — thin-client (#2051)", () => { GSTACK_HOME: env.gstackHome, GSTACK_DETECT_NO_CACHE: "1", }, + timeout: 30_000, }); expect(r.status).toBe(0); }); @@ -554,6 +555,7 @@ describe("lib/gbrain-local-status — thin-client (#2051)", () => { GSTACK_HOME: env.gstackHome, GSTACK_DETECT_NO_CACHE: "1", }, + timeout: 30_000, }); expect(r.status).toBe(1); }); @@ -790,6 +792,7 @@ describe("lib/gbrain-local-status — bearer-token thin-client (#2520)", () => { GSTACK_HOME: env.gstackHome, GSTACK_DETECT_NO_CACHE: "1", }, + timeout: 30_000, }); expect(r.status).toBe(0); }); diff --git a/test/gbrain-repo-policy-client.test.ts b/test/gbrain-repo-policy-client.test.ts index fd87bf227..de130e29c 100644 --- a/test/gbrain-repo-policy-client.test.ts +++ b/test/gbrain-repo-policy-client.test.ts @@ -32,7 +32,7 @@ function env(): NodeJS.ProcessEnv { } function run(args: string[], input?: string) { - const res = spawnSync(BIN, args, { env: env(), encoding: "utf-8", input }); + const res = spawnSync(BIN, args, { env: env(), encoding: "utf-8", input, timeout: 30_000 }); return { stdout: res.stdout || "", stderr: res.stderr || "", diff --git a/test/gbrain-repo-policy.test.ts b/test/gbrain-repo-policy.test.ts index 05c25f974..db6b58c2d 100644 --- a/test/gbrain-repo-policy.test.ts +++ b/test/gbrain-repo-policy.test.ts @@ -29,6 +29,7 @@ function run(args: string[], opts: { env?: Record } = {}) { const res = spawnSync(BIN, args, { env: { ...process.env, GSTACK_HOME: tmpHome, ...(opts.env || {}) }, encoding: 'utf-8', + timeout: 30_000, }); return { stdout: (res.stdout || '').trim(), @@ -263,6 +264,7 @@ describe('get without arg (auto-detect from current dir)', () => { env: { ...process.env, GSTACK_HOME: tmpHome }, cwd: cwdTmp, encoding: 'utf-8', + timeout: 30_000, }); expect((res.stdout || '').trim()).toBe('unset'); } finally { @@ -288,7 +290,7 @@ describe('gstack-gbrain-sync code stage honors the repo policy (#2140 sync path) function makeRepo(): void { repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gbrain-policy-repo-')); const git = (...args: string[]) => - spawnSync('git', args, { cwd: repoDir, encoding: 'utf-8' }); + spawnSync('git', args, { cwd: repoDir, encoding: 'utf-8', timeout: 30_000 }); git('init', '-q', '.'); git('remote', 'add', 'origin', REPO_URL); fs.writeFileSync(path.join(repoDir, 'README.md'), 'fixture\n'); diff --git a/test/gbrain-source-worktree-advance.test.ts b/test/gbrain-source-worktree-advance.test.ts index 3763bcf57..eaeaa4a57 100644 --- a/test/gbrain-source-worktree-advance.test.ts +++ b/test/gbrain-source-worktree-advance.test.ts @@ -23,12 +23,13 @@ function run(argv: string[], env: Record = {}) { env: { ...process.env, HOME: tmpHome, GSTACK_HOME: tmpHome, ...env }, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout || '', stderr: res.stderr || '', status: res.status ?? -1 }; } function git(args: string[], cwd: string) { - const res = spawnSync('git', args, { cwd, encoding: 'utf-8' }); + const res = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 30_000 }); return { stdout: (res.stdout || '').trim(), status: res.status ?? -1 }; } diff --git a/test/gbrain-sync-skip.test.ts b/test/gbrain-sync-skip.test.ts index 902256b3b..dabf15a7d 100644 --- a/test/gbrain-sync-skip.test.ts +++ b/test/gbrain-sync-skip.test.ts @@ -112,9 +112,10 @@ function runOrchestrator( ): { stdout: string; stderr: string; exitCode: number } { // Initialize a git repo in the sandbox so repoRoot() finds it (otherwise // code stage skips with "not in git repo" before our check ever fires). - spawnSync("git", ["init", "-q", env.home], { encoding: "utf-8" }); + spawnSync("git", ["init", "-q", env.home], { encoding: "utf-8", timeout: 30_000 }); spawnSync("git", ["-C", env.home, "commit", "--allow-empty", "-m", "init", "-q"], { encoding: "utf-8", + timeout: 30_000, env: { ...process.env, GIT_AUTHOR_NAME: "T", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "T", GIT_COMMITTER_EMAIL: "t@t" }, }); diff --git a/test/gbrain-sync-voyage-code-3-integration.test.ts b/test/gbrain-sync-voyage-code-3-integration.test.ts index 268e5ec5b..f2064032b 100644 --- a/test/gbrain-sync-voyage-code-3-integration.test.ts +++ b/test/gbrain-sync-voyage-code-3-integration.test.ts @@ -44,7 +44,7 @@ import { tmpdir } from "os"; import { join } from "path"; import { spawnSync } from "child_process"; -const gbrainPath = spawnSync("which", ["gbrain"], { encoding: "utf-8" }).stdout.trim(); +const gbrainPath = spawnSync("which", ["gbrain"], { encoding: "utf-8", timeout: 30_000 }).stdout.trim(); const gbrainAvailable = gbrainPath.length > 0; const voyageKey = process.env.VOYAGE_API_KEY?.trim() ?? ""; const voyageKeyPresent = voyageKey.length > 0; @@ -114,14 +114,14 @@ The queue module is a simple FIFO job queue. ); // Make it a git repo because gbrain's code-sync strategy expects one. - const gitInit = spawnSync("git", ["init", "-q"], { cwd: fixtureDir, encoding: "utf-8" }); + const gitInit = spawnSync("git", ["init", "-q"], { cwd: fixtureDir, encoding: "utf-8", timeout: 30_000 }); if (gitInit.status !== 0) { throw new Error(`git init failed: ${gitInit.stderr}`); } - spawnSync("git", ["config", "user.email", "test@example.invalid"], { cwd: fixtureDir }); - spawnSync("git", ["config", "user.name", "test"], { cwd: fixtureDir }); - spawnSync("git", ["add", "."], { cwd: fixtureDir }); - spawnSync("git", ["commit", "-q", "-m", "fixture"], { cwd: fixtureDir }); + spawnSync("git", ["config", "user.email", "test@example.invalid"], { cwd: fixtureDir, timeout: 30_000 }); + spawnSync("git", ["config", "user.name", "test"], { cwd: fixtureDir, timeout: 30_000 }); + spawnSync("git", ["add", "."], { cwd: fixtureDir, timeout: 30_000 }); + spawnSync("git", ["commit", "-q", "-m", "fixture"], { cwd: fixtureDir, timeout: 30_000 }); return { root, diff --git a/test/gemini-e2e.test.ts b/test/gemini-e2e.test.ts index eb3c49b00..cdd9a9fa5 100644 --- a/test/gemini-e2e.test.ts +++ b/test/gemini-e2e.test.ts @@ -29,7 +29,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const GEMINI_AVAILABLE = (() => { try { - const result = Bun.spawnSync(['which', 'gemini']); + const result = Bun.spawnSync(['which', 'gemini'], { timeout: 30_000 }); return result.exitCode === 0; } catch { return false; } })(); diff --git a/test/gen-skill-docs-import-purity.test.ts b/test/gen-skill-docs-import-purity.test.ts index e109b73ba..f3f104798 100644 --- a/test/gen-skill-docs-import-purity.test.ts +++ b/test/gen-skill-docs-import-purity.test.ts @@ -39,7 +39,7 @@ describe('gen-skill-docs import purity', () => { } console.log('IMPORT_PURE'); `; - const out = Bun.spawnSync(['bun', '-e', probe], { cwd: ROOT }); + const out = Bun.spawnSync(['bun', '-e', probe], { cwd: ROOT, timeout: 120_000 }); const stdout = out.stdout.toString(); const stderr = out.stderr.toString(); expect(stderr, stderr).not.toContain('import mutated'); diff --git a/test/gen-skill-docs-out-dir.test.ts b/test/gen-skill-docs-out-dir.test.ts index 5cbff0fb2..f957d89be 100644 --- a/test/gen-skill-docs-out-dir.test.ts +++ b/test/gen-skill-docs-out-dir.test.ts @@ -21,7 +21,7 @@ describe('gen-skill-docs --out-dir (B2 render isolation)', () => { } function porcelain(): string { - const r = spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf-8' }); + const r = spawnSync('git', ['status', '--porcelain'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); return r.status === 0 ? r.stdout : ''; } diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index 961fa2a5e..dc7c66cae 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -139,7 +139,7 @@ const EXTERNAL_OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-gen-docs-out- { const render = Bun.spawnSync( ['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--out-dir', EXTERNAL_OUT], - { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }, + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000 }, ); if (render.exitCode !== 0) { throw new Error( @@ -315,6 +315,7 @@ describe('gen-skill-docs', () => { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + timeout: 120_000, }); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); @@ -1965,7 +1966,7 @@ describe('Codex generation (--host codex)', () => { '/tmp/gstack-claude-error-XXXXXX', '/tmp/gstack-claude-diff-XXXXXX', ]) { - const result = spawnSync('mktemp', [template], { encoding: 'utf-8' }); + const result = spawnSync('mktemp', [template], { encoding: 'utf-8', timeout: 30_000 }); expect(result.status).toBe(0); const created = result.stdout.trim(); expect(created.startsWith(template.replace('XXXXXX', ''))).toBe(true); @@ -1990,6 +1991,7 @@ describe('Codex generation (--host codex)', () => { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + timeout: 120_000, }); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); @@ -2005,11 +2007,13 @@ describe('Codex generation (--host codex)', () => { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + timeout: 120_000, }); const agentsResult = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'agents', '--dry-run', '--out-dir', EXTERNAL_OUT], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + timeout: 120_000, }); expect(codexResult.exitCode).toBe(0); expect(agentsResult.exitCode).toBe(0); @@ -2224,6 +2228,7 @@ describe('Codex generation (--host codex)', () => { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + timeout: 120_000, }); expect(override.exitCode).toBe(0); const content = fs.readFileSync(path.join(overrideOut, '.agents', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8'); @@ -2342,10 +2347,10 @@ describe('Factory generation (--host factory)', () => { test('--host droid alias works', () => { const factoryResult = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'factory', '--dry-run', '--out-dir', EXTERNAL_OUT], { - cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000, }); const droidResult = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'droid', '--dry-run', '--out-dir', EXTERNAL_OUT], { - cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000, }); expect(factoryResult.exitCode).toBe(0); expect(droidResult.exitCode).toBe(0); @@ -2354,7 +2359,7 @@ describe('Factory generation (--host factory)', () => { test('--host factory --dry-run freshness', () => { const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'factory', '--dry-run', '--out-dir', EXTERNAL_OUT], { - cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000, }); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); @@ -2432,7 +2437,7 @@ describe('Parameterized host smoke tests', () => { test('--dry-run freshness check passes', () => { const result = Bun.spawnSync( ['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', hostConfig.name, '--dry-run', '--out-dir', EXTERNAL_OUT], - { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' } + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000 } ); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); @@ -2457,7 +2462,7 @@ describe('--host all', () => { // claude host plus every external host regenerate deterministically. test('--host all generates for all registered hosts', () => { const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all', '--dry-run', '--out-dir', EXTERNAL_OUT], { - cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000, }); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); diff --git a/test/global-discover.test.ts b/test/global-discover.test.ts index f433da8c4..48b09e72e 100644 --- a/test/global-discover.test.ts +++ b/test/global-discover.test.ts @@ -180,10 +180,11 @@ describe("gstack-global-discover", () => { // Create a git repo as the session target const repoDir = join(tmpDir, "fake-repo"); mkdirSync(repoDir); - spawnSync("git", ["init"], { cwd: repoDir, stdio: "pipe" }); + spawnSync("git", ["init"], { cwd: repoDir, stdio: "pipe", timeout: 30_000 }); spawnSync("git", ["commit", "--allow-empty", "-m", "init"], { cwd: repoDir, stdio: "pipe", + timeout: 30_000, }); // Write a session with a 20KB first line (simulates Codex v0.117+) diff --git a/test/gstack-artifacts-init.test.ts b/test/gstack-artifacts-init.test.ts index a125562c3..b1dd3c84b 100644 --- a/test/gstack-artifacts-init.test.ts +++ b/test/gstack-artifacts-init.test.ts @@ -119,7 +119,7 @@ exit 0 * test focused on artifacts-init's branching logic, not git plumbing. */ function makeFakeGit() { - const realGit = spawnSync('which', ['git'], { encoding: 'utf-8' }).stdout.trim(); + const realGit = spawnSync('which', ['git'], { encoding: 'utf-8', timeout: 30_000 }).stdout.trim(); const script = `#!/bin/bash # Walk argv past leading -C and similar flags to find the real subcommand. args=("$@") @@ -157,6 +157,7 @@ function run(argv: string[], opts: { env?: Record; input?: strin encoding: 'utf-8', input: opts.input, cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout || '', @@ -176,7 +177,7 @@ beforeEach(() => { fakeBinDir = fs.mkdtempSync(path.join(os.tmpdir(), 'artifacts-fake-bin-')); ghCallLog = path.join(fakeBinDir, 'gh-calls.log'); glabCallLog = path.join(fakeBinDir, 'glab-calls.log'); - spawnSync('git', ['init', '--bare', '-q', '-b', 'main', bareRemote]); + spawnSync('git', ['init', '--bare', '-q', '-b', 'main', bareRemote], { timeout: 30_000 }); makeFakeGit(); }); @@ -277,7 +278,7 @@ describe('gstack-artifacts-init canonical URL storage (codex Finding #10)', () = makeFakeGh({ webUrl: 'https://github.com/testuser/gstack-artifacts-testuser' }); const r = run(['--host', 'github']); expect(r.status).toBe(0); - const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8', timeout: 30_000 }); expect(remote.stdout.trim()).toBe('https://github.com/testuser/gstack-artifacts-testuser'); }); @@ -288,7 +289,7 @@ describe('gstack-artifacts-init canonical URL storage (codex Finding #10)', () = }); const r = run(['--host', 'github']); expect(r.status).toBe(0); - const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8', timeout: 30_000 }); expect(remote.stdout.trim()).toBe('git@github.com:testuser/gstack-artifacts-testuser.git'); }); @@ -296,7 +297,7 @@ describe('gstack-artifacts-init canonical URL storage (codex Finding #10)', () = makeFakeGh({ gitProtocol: 'unset' }); const r = run(['--host', 'github']); expect(r.status).toBe(0); - const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8', timeout: 30_000 }); expect(remote.stdout.trim()).toBe('https://github.com/testuser/gstack-artifacts-testuser'); }); @@ -304,7 +305,7 @@ describe('gstack-artifacts-init canonical URL storage (codex Finding #10)', () = makeFakeGlab({ gitProtocol: 'ssh' }); const r = run(['--host', 'gitlab']); expect(r.status).toBe(0); - const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8', timeout: 30_000 }); expect(remote.stdout.trim()).toBe('git@gitlab.com:testuser/gstack-artifacts-testuser.git'); }); }); @@ -359,7 +360,7 @@ describe('gstack-artifacts-init idempotency', () => { makeFakeGh({ gitProtocol: 'ssh' }); const r = run(['--remote', 'https://github.com/testuser/gstack-artifacts-testuser']); expect(r.status).toBe(0); - const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8', timeout: 30_000 }); expect(remote.stdout.trim()).toBe('https://github.com/testuser/gstack-artifacts-testuser'); }); @@ -372,7 +373,7 @@ describe('gstack-artifacts-init idempotency', () => { 'ssh', ]); expect(r.status).toBe(0); - const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8' }); + const remote = spawnSync('git', ['-C', tmpHome, 'remote', 'get-url', 'origin'], { encoding: 'utf-8', timeout: 30_000 }); expect(remote.stdout.trim()).toBe('git@github.com:testuser/gstack-artifacts-testuser.git'); }); diff --git a/test/gstack-artifacts-url.test.ts b/test/gstack-artifacts-url.test.ts index 133ed7fc8..76f3e2cc0 100644 --- a/test/gstack-artifacts-url.test.ts +++ b/test/gstack-artifacts-url.test.ts @@ -13,7 +13,7 @@ const ROOT = path.resolve(import.meta.dir, '..'); const URL_BIN = path.join(ROOT, 'bin', 'gstack-artifacts-url'); function run(args: string[]): { code: number; stdout: string; stderr: string } { - const r = spawnSync(URL_BIN, args, { encoding: 'utf-8' }); + const r = spawnSync(URL_BIN, args, { encoding: 'utf-8', timeout: 30_000 }); return { code: r.status ?? -1, stdout: (r.stdout || '').trim(), diff --git a/test/gstack-codex-session-import.test.ts b/test/gstack-codex-session-import.test.ts index 7cd32e949..be48c75ae 100644 --- a/test/gstack-codex-session-import.test.ts +++ b/test/gstack-codex-session-import.test.ts @@ -68,7 +68,7 @@ function runImport(sessionPath: string): { stdout: string; stderr: string; statu env.GSTACK_STATE_ROOT = stateRoot; env.GSTACK_QUESTION_LOG_NO_DERIVE = '1'; delete env.GSTACK_HOME; - const res = spawnSync(BIN, [sessionPath], { env, encoding: 'utf-8', cwd: ROOT }); + const res = spawnSync(BIN, [sessionPath], { env, encoding: 'utf-8', cwd: ROOT, timeout: 30_000 }); return { stdout: res.stdout ?? '', stderr: res.stderr ?? '', @@ -196,7 +196,7 @@ describe('default mode (no args → latest)', () => { } env.GSTACK_STATE_ROOT = stateRoot; env.CODEX_SESSIONS_ROOT = emptyDir; - const res = spawnSync(BIN, [], { env, encoding: 'utf-8', cwd: ROOT }); + const res = spawnSync(BIN, [], { env, encoding: 'utf-8', cwd: ROOT, timeout: 30_000 }); expect(res.status).toBe(0); expect(res.stdout).toMatch(/NO_SESSIONS/); } finally { diff --git a/test/gstack-config-defaults.test.ts b/test/gstack-config-defaults.test.ts index cb8febadd..781ee64b9 100644 --- a/test/gstack-config-defaults.test.ts +++ b/test/gstack-config-defaults.test.ts @@ -45,6 +45,7 @@ const STATE = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-config-test-')); function get(key: string): { out: string; code: number } { const r = spawnSync('bash', [CONFIG_BIN, 'get', key], { encoding: 'utf-8', + timeout: 30_000, env: { ...process.env, GSTACK_STATE_ROOT: STATE }, }); return { out: r.stdout ?? '', code: r.status ?? -1 }; diff --git a/test/gstack-config-key-locale.test.ts b/test/gstack-config-key-locale.test.ts index 94d60cd6b..599a0c168 100644 --- a/test/gstack-config-key-locale.test.ts +++ b/test/gstack-config-key-locale.test.ts @@ -28,6 +28,7 @@ function run(args: string[]) { // live tree (observed in the free-tests CI job). Relink behavior itself is // covered in isolation by test/relink.test.ts's mock install. env: { ...process.env, GSTACK_STATE_ROOT: stateRoot, GSTACK_SETUP_RUNNING: "1" }, + timeout: 30_000, }); return { diff --git a/test/gstack-config-redact-keys.test.ts b/test/gstack-config-redact-keys.test.ts index 9290d478d..9a73571a5 100644 --- a/test/gstack-config-redact-keys.test.ts +++ b/test/gstack-config-redact-keys.test.ts @@ -16,6 +16,7 @@ function cfg(args: string[]): { code: number; out: string; err: string } { const r = spawnSync(CONFIG, args, { encoding: "utf8", env: { ...process.env, GSTACK_HOME: home }, + timeout: 30_000, }); return { code: r.status ?? 0, out: r.stdout ?? "", err: r.stderr ?? "" }; } diff --git a/test/gstack-decision-bins.test.ts b/test/gstack-decision-bins.test.ts index a99209039..82c28ac23 100644 --- a/test/gstack-decision-bins.test.ts +++ b/test/gstack-decision-bins.test.ts @@ -20,18 +20,18 @@ function opts(): ExecSyncOptionsWithStringEncoding { } function log(arg: string, expectFail = false): { out: string; code: number } { try { - return { out: execSync(`${LOG} '${arg.replace(/'/g, "'\\''")}'`, opts()).trim(), code: 0 }; + return { out: execSync(`${LOG} '${arg.replace(/'/g, "'\\''")}'`, opts()).trim(), code: 0 }; // timeout via opts() } catch (e: any) { if (expectFail) return { out: (e.stderr?.toString() || "").trim(), code: e.status || 1 }; throw e; } } function logFlag(flag: string): string { - return execSync(`${LOG} ${flag}`, opts()).trim(); + return execSync(`${LOG} ${flag}`, opts()).trim(); // timeout via opts() } function search(args = ""): string { try { - return execSync(`${SEARCH} ${args}`, opts()).trim(); + return execSync(`${SEARCH} ${args}`, opts()).trim(); // timeout via opts() } catch { return ""; } diff --git a/test/gstack-detach.test.ts b/test/gstack-detach.test.ts index 926bf253a..f6f26ca13 100644 --- a/test/gstack-detach.test.ts +++ b/test/gstack-detach.test.ts @@ -15,13 +15,13 @@ const ROOT = path.resolve(import.meta.dir, '..'); const DETACH = path.join(ROOT, 'bin', 'gstack-detach'); function ownPgid(): string { - return (spawnSync('ps', ['-o', 'pgid=', '-p', String(process.pid)], { encoding: 'utf-8' }).stdout || '').trim(); + return (spawnSync('ps', ['-o', 'pgid=', '-p', String(process.pid)], { encoding: 'utf-8', timeout: 30_000 }).stdout || '').trim(); } function waitFor(pred: () => boolean, ms: number): boolean { const end = Date.now() + ms; while (Date.now() < end) { if (pred()) return true; - spawnSync('sleep', ['0.2']); + spawnSync('sleep', ['0.2'], { timeout: 30_000 }); } return pred(); } @@ -90,7 +90,7 @@ describe('gstack-detach', () => { }, 20000); test('rejects missing command (exit 2)', () => { - const r = spawnSync(DETACH, ['--label', 'x'], { encoding: 'utf-8' }); + const r = spawnSync(DETACH, ['--label', 'x'], { encoding: 'utf-8', timeout: 30_000 }); expect(r.status).toBe(2); }); }); diff --git a/test/gstack-developer-profile.test.ts b/test/gstack-developer-profile.test.ts index 507706509..1af3eef18 100644 --- a/test/gstack-developer-profile.test.ts +++ b/test/gstack-developer-profile.test.ts @@ -36,6 +36,7 @@ function runDev(...args: string[]): { stdout: string; stderr: string; status: nu env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout ?? '', @@ -49,6 +50,7 @@ function logQuestion(payload: Record): number { env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return res.status ?? -1; } diff --git a/test/gstack-egress-cli.test.ts b/test/gstack-egress-cli.test.ts index 6d9f19f8d..7b0398375 100644 --- a/test/gstack-egress-cli.test.ts +++ b/test/gstack-egress-cli.test.ts @@ -36,6 +36,7 @@ function run(args: string[]) { const result = spawnSync(BIN, args, { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home }, + timeout: 30_000, }); return { code: result.status ?? -1, stdout: result.stdout || '', stderr: result.stderr || '' }; } @@ -122,11 +123,13 @@ describe('gstack-egress grants', () => { const config = spawnSync(path.join(ROOT, 'bin', 'gstack-config'), ['set', 'telemetry', 'community'], { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home }, + timeout: 30_000, }); expect(config.status).toBe(0); spawnSync(path.join(ROOT, 'bin', 'gstack-config'), ['set', 'artifacts_sync_mode', 'full'], { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: home }, + timeout: 30_000, }); const r = run(['grants', '--json']); expect(r.code).toBe(0); diff --git a/test/gstack-gbrain-detect-mcp-mode.test.ts b/test/gstack-gbrain-detect-mcp-mode.test.ts index c4793e6fc..e1cfba204 100644 --- a/test/gstack-gbrain-detect-mcp-mode.test.ts +++ b/test/gstack-gbrain-detect-mcp-mode.test.ts @@ -66,6 +66,7 @@ function runDetect(extraEnv: Record = {}): { code: number; json: ...extraEnv, }, encoding: 'utf-8', + timeout: 30_000, }); let json: any = null; try { diff --git a/test/gstack-gbrain-mcp-verify.test.ts b/test/gstack-gbrain-mcp-verify.test.ts index 4461d4dd5..57f036ebe 100644 --- a/test/gstack-gbrain-mcp-verify.test.ts +++ b/test/gstack-gbrain-mcp-verify.test.ts @@ -94,6 +94,7 @@ function runVerify(token: string, url: string): { code: number; stdout: string; GSTACK_HOME: tmpDir, }, encoding: 'utf-8', + timeout: 30_000, }); return { code: result.status ?? -1, @@ -249,6 +250,7 @@ describe('gstack-gbrain-mcp-verify', () => { const r = spawnSync(VERIFY_BIN, ['https://example.com/mcp'], { env: { ...process.env, PATH: `${fakeBinDir}:${process.env.PATH}`, GBRAIN_MCP_TOKEN: '' }, encoding: 'utf-8', + timeout: 30_000, }); expect(r.status).toBe(2); expect(r.stderr).toContain('GBRAIN_MCP_TOKEN'); @@ -259,6 +261,7 @@ describe('gstack-gbrain-mcp-verify', () => { const r = spawnSync(VERIFY_BIN, [], { env: { ...process.env, PATH: `${fakeBinDir}:${process.env.PATH}`, GBRAIN_MCP_TOKEN: 'x' }, encoding: 'utf-8', + timeout: 30_000, }); expect(r.status).toBe(2); }); diff --git a/test/gstack-gbrain-source-wireup.test.ts b/test/gstack-gbrain-source-wireup.test.ts index 71e2d8b17..4b7858c5f 100644 --- a/test/gstack-gbrain-source-wireup.test.ts +++ b/test/gstack-gbrain-source-wireup.test.ts @@ -145,6 +145,7 @@ function run( env, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); } @@ -163,13 +164,13 @@ function gbrainCalls(): string[] { function setupGstackRepo(remoteUrl: string) { // Real git repo at gstackHome with at least one commit + an origin remote. fs.mkdirSync(gstackHome, { recursive: true }); - spawnSync('git', ['-C', gstackHome, 'init', '-q', '-b', 'main'], { stdio: 'pipe' }); - spawnSync('git', ['-C', gstackHome, 'config', 'user.email', 'test@example.com'], { stdio: 'pipe' }); - spawnSync('git', ['-C', gstackHome, 'config', 'user.name', 'test'], { stdio: 'pipe' }); + spawnSync('git', ['-C', gstackHome, 'init', '-q', '-b', 'main'], { stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['-C', gstackHome, 'config', 'user.email', 'test@example.com'], { stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['-C', gstackHome, 'config', 'user.name', 'test'], { stdio: 'pipe', timeout: 30_000 }); fs.writeFileSync(path.join(gstackHome, '.brain-allowlist'), '# allowlist\n'); - spawnSync('git', ['-C', gstackHome, 'add', '.'], { stdio: 'pipe' }); - spawnSync('git', ['-C', gstackHome, 'commit', '-q', '-m', 'init'], { stdio: 'pipe' }); - spawnSync('git', ['-C', gstackHome, 'remote', 'add', 'origin', remoteUrl], { stdio: 'pipe' }); + spawnSync('git', ['-C', gstackHome, 'add', '.'], { stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['-C', gstackHome, 'commit', '-q', '-m', 'init'], { stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['-C', gstackHome, 'remote', 'add', 'origin', remoteUrl], { stdio: 'pipe', timeout: 30_000 }); } beforeEach(() => { @@ -316,6 +317,7 @@ describe('gstack-gbrain-source-wireup — wireup mode', () => { const check = spawnSync('bash', ['-c', `command -v gbrain && gbrain --version`], { env: { PATH: `${hostLikeDir}:${process.env.PATH || '/usr/bin:/bin'}` }, encoding: 'utf-8', + timeout: 30_000, }); expect(check.status).toBe(0); expect(check.stdout).toContain('gbrain 0.18.2'); @@ -526,13 +528,15 @@ describe('gstack-gbrain-source-wireup — defensive paths', () => { run([], { env: { GSTACK_BRAIN_NO_SYNC: '1' } }); // Make a new commit on parent so worktree HEAD is "behind" fs.writeFileSync(path.join(gstackHome, 'newfile.md'), 'new'); - spawnSync('git', ['-C', gstackHome, 'add', '.'], { stdio: 'pipe' }); - spawnSync('git', ['-C', gstackHome, 'commit', '-q', '-m', 'second commit'], { stdio: 'pipe' }); + spawnSync('git', ['-C', gstackHome, 'add', '.'], { stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['-C', gstackHome, 'commit', '-q', '-m', 'second commit'], { stdio: 'pipe', timeout: 30_000 }); const parentHeadAfter = spawnSync('git', ['-C', gstackHome, 'rev-parse', 'HEAD'], { encoding: 'utf-8', + timeout: 30_000, }).stdout.trim(); const worktreeHeadBefore = spawnSync('git', ['-C', worktreeDir, 'rev-parse', 'HEAD'], { encoding: 'utf-8', + timeout: 30_000, }).stdout.trim(); expect(parentHeadAfter).not.toBe(worktreeHeadBefore); // sanity: parent advanced // --no-pull should leave worktree HEAD where it was @@ -540,6 +544,7 @@ describe('gstack-gbrain-source-wireup — defensive paths', () => { expect(r.status).toBe(0); const worktreeHeadAfter = spawnSync('git', ['-C', worktreeDir, 'rev-parse', 'HEAD'], { encoding: 'utf-8', + timeout: 30_000, }).stdout.trim(); expect(worktreeHeadAfter).toBe(worktreeHeadBefore); expect(worktreeHeadAfter).not.toBe(parentHeadAfter); diff --git a/test/gstack-gbrain-sync.test.ts b/test/gstack-gbrain-sync.test.ts index 9948da51c..7f16772bd 100644 --- a/test/gstack-gbrain-sync.test.ts +++ b/test/gstack-gbrain-sync.test.ts @@ -137,7 +137,7 @@ describe("gstack-gbrain-sync CLI", () => { const repo = mkdtempSync(join(tmpdir(), "gstack-pinned-source-repo-")); const commandLog = join(home, "gbrain-commands.log"); mkdirSync(gstackHome, { recursive: true }); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); writeFileSync(join(repo, ".gbrain-source"), "client-acme-app\n"); writeFileSync(join(bindir, "gbrain"), `#!/bin/sh printf '%s\\n' "$*" >> "$GSTACK_TEST_GBRAIN_LOG" @@ -179,7 +179,7 @@ exit 99 mkdirSync(gstackHome, { recursive: true }); mkdirSync(join(home, ".gbrain"), { recursive: true }); writeFileSync(join(home, ".gbrain", "config.json"), JSON.stringify({ engine: "pglite", database_url: "pglite:///test" })); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); writeFileSync(join(repo, ".gbrain-source"), "client-acme-app\n"); symlinkSync(repo, link, "dir"); writeFileSync(join(bindir, "gbrain"), `#!/bin/sh @@ -223,7 +223,7 @@ esac const bindir = mkdtempSync(join(tmpdir(), "gstack-pinned-dream-bin-")); const repo = mkdtempSync(join(tmpdir(), "gstack-pinned-dream-repo-")); mkdirSync(gstackHome, { recursive: true }); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); writeFileSync(join(repo, ".gbrain-source"), "client-acme-app\n"); writeFileSync(join(bindir, "gbrain"), "#!/bin/sh\nexit 99\n"); chmodSync(join(bindir, "gbrain"), 0o755); @@ -247,7 +247,7 @@ esac const gstackHome = join(home, ".gstack"); const repo = mkdtempSync(join(tmpdir(), "gstack-unreadable-pin-repo-")); mkdirSync(gstackHome, { recursive: true }); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); mkdirSync(join(repo, ".gbrain-source")); const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { @@ -282,8 +282,8 @@ esac const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-source-id-repo-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", remote], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", remote], { cwd: repo, timeout: 30_000 }); const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { encoding: "utf-8", @@ -312,7 +312,7 @@ esac const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-no-origin-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); // No `git remote add origin` — this is the no-remote case. const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { @@ -347,7 +347,7 @@ esac const parent = mkdtempSync(join(tmpdir(), "gstack-empty-base-")); const repo = join(parent, "___"); mkdirSync(repo); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); // No `origin` remote — forces the basename-fallback path. const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { @@ -381,8 +381,8 @@ esac const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-host-collide-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", "https://github.com/example/multihost.git"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/example/multihost.git"], { cwd: repo, timeout: 30_000 }); // Dry-run still gates the code stage on `command -v gbrain`. Drop a no-op // shim on PATH so the stage runs (we only assert the preview line, never @@ -569,8 +569,8 @@ esac const repoA = mkdtempSync(join(tmpdir(), "gstack-worktree-a-")); const repoB = mkdtempSync(join(tmpdir(), "gstack-worktree-b-")); for (const repo of [repoA, repoB]) { - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", remote], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", remote], { cwd: repo, timeout: 30_000 }); } const idOf = (cwd: string): string => { @@ -606,8 +606,8 @@ esac const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-worktree-stable-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", remote], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", remote], { cwd: repo, timeout: 30_000 }); const idOf = (): string => { const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { @@ -635,8 +635,8 @@ esac const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-legacy-cleanup-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", "https://github.com/garrytan/gstack.git"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/garrytan/gstack.git"], { cwd: repo, timeout: 30_000 }); const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { encoding: "utf-8", @@ -671,8 +671,8 @@ esac const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-attach-preview-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", "https://github.com/garrytan/gstack.git"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/garrytan/gstack.git"], { cwd: repo, timeout: 30_000 }); const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { encoding: "utf-8", @@ -727,8 +727,8 @@ describe("derivePathOnlyHashLegacyId", () => { // legacy id regardless of $GSTACK_HOSTNAME, because the pre-#1468 hash // didn't include hostname. const repo = mkdtempSync(join(tmpdir(), "gstack-legacy-id-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", "https://github.com/example/legacy-test.git"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/example/legacy-test.git"], { cwd: repo, timeout: 30_000 }); const cwd = process.cwd(); try { @@ -754,8 +754,8 @@ describe("derivePathOnlyHashLegacyId", () => { // host-fold id must differ for any non-empty hostname, so the migration // can detect + clean up the orphan. const repo = mkdtempSync(join(tmpdir(), "gstack-legacy-id-distinct-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", "https://github.com/example/distinct.git"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/example/distinct.git"], { cwd: repo, timeout: 30_000 }); const cwd = process.cwd(); try { @@ -890,10 +890,10 @@ describe("constrainSourceId truncation (hyphen-boundary cut)", () => { const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-hyphen-cut-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); // Remote chosen to be long enough that constrainSourceId truncates and // the boundary lands inside the word `skill`. - spawnSync("git", ["remote", "add", "origin", "https://github.com/drummerms-av-sow-wiz/skill-270c0001.git"], { cwd: repo }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/drummerms-av-sow-wiz/skill-270c0001.git"], { cwd: repo, timeout: 30_000 }); const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { encoding: "utf-8", @@ -924,8 +924,8 @@ describe("constrainSourceId truncation (hyphen-boundary cut)", () => { const gstackHome = join(home, ".gstack"); mkdirSync(gstackHome, { recursive: true }); const repo = mkdtempSync(join(tmpdir(), "gstack-https-period-")); - spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo }); - spawnSync("git", ["remote", "add", "origin", "https://github.com/foo/bar.git"], { cwd: repo }); + spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 }); + spawnSync("git", ["remote", "add", "origin", "https://github.com/foo/bar.git"], { cwd: repo, timeout: 30_000 }); const r = spawnSync("bun", [SCRIPT, "--dry-run", "--code-only", "--quiet"], { encoding: "utf-8", diff --git a/test/gstack-home-module-scope.test.ts b/test/gstack-home-module-scope.test.ts index 842e13247..c28c8d220 100644 --- a/test/gstack-home-module-scope.test.ts +++ b/test/gstack-home-module-scope.test.ts @@ -26,7 +26,7 @@ const ROOT = path.resolve(__dirname, '..'); function trackedTestFiles(): string[] { const out = spawnSync('git', ['ls-files', '*.test.ts'], { - cwd: ROOT, encoding: 'utf-8', + cwd: ROOT, encoding: 'utf-8', timeout: 30_000, }); if (out.status !== 0) throw new Error(`git ls-files failed: ${out.stderr}`); return out.stdout.split('\n').filter(Boolean); diff --git a/test/gstack-memory-ingest.test.ts b/test/gstack-memory-ingest.test.ts index 56523a0b6..a0220e279 100644 --- a/test/gstack-memory-ingest.test.ts +++ b/test/gstack-memory-ingest.test.ts @@ -500,6 +500,7 @@ describe("gstack-memory-ingest writer (gbrain v0.20+ batch `import` interface)", const POLICY = join(import.meta.dir, "..", "bin", "gstack-gbrain-repo-policy"); const seeded = spawnSync("bash", [POLICY, "set", "_unattributed", "deny"], { encoding: "utf-8", + timeout: 30_000, env: { ...process.env, HOME: home, GSTACK_HOME: gstackHome }, }); expect(seeded.status).toBe(0); @@ -622,6 +623,7 @@ esac expect(existsSync(stagingCopy)).toBe(true); const findMd = spawnSync("find", [stagingCopy, "-name", "*.md", "-type", "f"], { encoding: "utf-8", + timeout: 30_000, }); const mdPaths = (findMd.stdout || "").trim().split("\n").filter(Boolean); expect(mdPaths.length).toBeGreaterThan(0); @@ -685,6 +687,7 @@ esac // walk to find a .md and read its head.) const findMd = spawnSync("find", [stagingCopy, "-name", "*.md", "-type", "f"], { encoding: "utf-8", + timeout: 30_000, }); const mdPaths = (findMd.stdout || "").trim().split("\n").filter(Boolean); expect(mdPaths.length).toBeGreaterThan(0); @@ -913,8 +916,8 @@ describe("#2394: probe applies the same attribution gate as prepare", () => { function makeAttributableCwd(home: string): string { const repo = join(home, "work", "attributable-repo"); mkdirSync(repo, { recursive: true }); - spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8" }); - spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8" }); + spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8", timeout: 30_000 }); + spawnSync("git", ["-C", repo, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8", timeout: 30_000 }); return repo; } @@ -993,8 +996,8 @@ describe("#2394: probe applies the same attribution gate as prepare", () => { mkdirSync(gstackHome, { recursive: true }); const attributableCwd = join(home, "work", "attributable-repo"); mkdirSync(attributableCwd, { recursive: true }); - spawnSync("git", ["-C", attributableCwd, "init", "-q"], { encoding: "utf-8" }); - spawnSync("git", ["-C", attributableCwd, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8" }); + spawnSync("git", ["-C", attributableCwd, "init", "-q"], { encoding: "utf-8", timeout: 30_000 }); + spawnSync("git", ["-C", attributableCwd, "remote", "add", "origin", "https://github.com/foo/bar.git"], { encoding: "utf-8", timeout: 30_000 }); const ts = new Date().toISOString(); const cwdLine = `{"type":"user","message":{"role":"user","content":"hello"},"timestamp":"${ts}","cwd":"${attributableCwd.replace(/\\/g, "\\\\")}"}\n`; @@ -1077,8 +1080,8 @@ describe("#2392: transcript ingest honors per-remote trust policy", () => { function makeRepoWithRemote(home: string, name: string, remoteUrl: string): string { const repo = join(home, "work", name); mkdirSync(repo, { recursive: true }); - spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8" }); - spawnSync("git", ["-C", repo, "remote", "add", "origin", remoteUrl], { encoding: "utf-8" }); + spawnSync("git", ["-C", repo, "init", "-q"], { encoding: "utf-8", timeout: 30_000 }); + spawnSync("git", ["-C", repo, "remote", "add", "origin", remoteUrl], { encoding: "utf-8", timeout: 30_000 }); return repo; } @@ -1095,6 +1098,7 @@ describe("#2392: transcript ingest honors per-remote trust policy", () => { function setPolicy(gstackHome: string, url: string, tier: string): void { const r = spawnSync(POLICY_BIN, ["set", url, tier], { encoding: "utf-8", + timeout: 30_000, env: { ...process.env, GSTACK_HOME: gstackHome }, }); expect(r.status).toBe(0); diff --git a/test/gstack-next-version.test.ts b/test/gstack-next-version.test.ts index 319047287..9099fd626 100644 --- a/test/gstack-next-version.test.ts +++ b/test/gstack-next-version.test.ts @@ -326,7 +326,7 @@ describe("default-base detection (no --base)", () => { function runWithoutBase(cwd: string): { exitCode: number; parsed: any } { const proc = Bun.spawnSync( ["bun", "run", SCRIPT, "--bump", "patch", "--workspace-root", "null"], - { cwd }, + { cwd, timeout: 30_000 }, ); const out = new TextDecoder().decode(proc.stdout); return { exitCode: proc.exitCode, parsed: JSON.parse(out) }; @@ -403,11 +403,11 @@ describe("offline output contract (what /ship branches on, #2545)", () => { // host:"unknown" on CI) while keeping ls-remote/fetch fully local. const bare = join(root, "github.com", "origin.git"); mkdirSync(bare, { recursive: true }); - Bun.spawnSync(["git", "init", "-q", "--bare", "-b", "main", bare]); + Bun.spawnSync(["git", "init", "-q", "--bare", "-b", "main", bare], { timeout: 30_000 }); const work = join(root, "work"); mkdirSync(work); const git = (...args: string[]) => - Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd: work }); + Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd: work, timeout: 30_000 }); git("init", "-q", "-b", "main"); writeFileSync(join(work, "VERSION"), "1.0.0.0\n"); git("add", "-A"); @@ -427,7 +427,7 @@ describe("offline output contract (what /ship branches on, #2545)", () => { const proc = Bun.spawnSync( ["bun", "run", NEXTVER, "--base", "main", "--bump", "patch", "--current-version", "1.0.0.0", "--workspace-root", "null"], - { cwd: work, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` } }, + { cwd: work, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` }, timeout: 30_000 }, ); rmSync(stubDir, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true }); @@ -454,7 +454,7 @@ describe("offline output contract (what /ship branches on, #2545)", () => { const proc = Bun.spawnSync( ["bun", "run", NEXTVER, "--base", "main", "--bump", "patch", "--current-version", "1.0.0.0", "--workspace-root", "null"], - { cwd: work, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` } }, + { cwd: work, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` }, timeout: 30_000 }, ); rmSync(stubDir, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true }); @@ -475,7 +475,7 @@ describe("fetchGitClaimed (offline allocation — the anti-duplicate fallback, # // (plus three earlier pairs found in the same audit). Git knows what the API // was asked for, so offline now degrades the QUEUE VIEW, not the ALLOCATION. function git(cwd: string, ...args: string[]) { - return Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd }); + return Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd, timeout: 30_000 }); } function fixture(): string { @@ -600,7 +600,7 @@ describe("fetchGitClaimed — non-mutating live remote query (ls-remote first)", // remote's LIVE branch list with zero local mutation — a path/file remote // answers it offline, which is exactly what these fixtures use. function git(cwd: string, ...args: string[]) { - return Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd }); + return Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd, timeout: 30_000 }); } // Local origin with: main (0.1.66.0), sibling (0.1.67.0, live claim), and @@ -718,7 +718,7 @@ describe("fetchGitClaimed — unfetched live claims (G2: ls-remote advertises SH // VERSION reads fail. The old `continue` silently dropped that LIVE claim — // the exact duplicate-allocation this fallback exists to prevent. function git(cwd: string, ...args: string[]) { - return Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd }); + return Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd, timeout: 30_000 }); } function cloneFixture(): { root: string; origin: string; clone: string } { @@ -908,14 +908,14 @@ describe("width pinned on failed base read (3-digit repos)", () => { // the base read fails too, which is the path under test. writeFileSync(join(stubDir, "gh"), "#!/bin/sh\nexit 1\n", { mode: 0o755 }); writeFileSync(join(stubDir, "glab"), "#!/bin/sh\nexit 1\n", { mode: 0o755 }); - Bun.spawnSync(["git", "init", "-q", "-b", "main"], { cwd: dir }); + Bun.spawnSync(["git", "init", "-q", "-b", "main"], { cwd: dir, timeout: 30_000 }); writeFileSync(join(dir, "VERSION"), "0.99.2\n"); - Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: dir }); - Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "init"], { cwd: dir }); + Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", "add", "-A"], { cwd: dir, timeout: 30_000 }); + Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "init"], { cwd: dir, timeout: 30_000 }); const proc = Bun.spawnSync( ["bun", "run", SCRIPT, "--base", "main", "--bump", "patch", "--workspace-root", "null"], - { cwd: dir, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` } }, + { cwd: dir, env: { ...process.env, PATH: `${stubDir}:${process.env.PATH}` }, timeout: 30_000 }, ); const out = JSON.parse(new TextDecoder().decode(proc.stdout)); // Zero base at the repo's OWN width — never "0.0.0.0" in a 3-digit repo. @@ -946,7 +946,7 @@ describe("integration (smoke)", () => { "1.6.3.0", "--workspace-root", "null", // skip sibling scan in CI - ]); + ], { timeout: 30_000 }); const out = new TextDecoder().decode(proc.stdout); const parsed = JSON.parse(out); expect(parsed).toHaveProperty("version"); @@ -976,7 +976,7 @@ describe("integration (smoke)", () => { "null", "--version-path", "Tinas Second Brain/health-tracker/VERSION", - ]); + ], { timeout: 30_000 }); const out = new TextDecoder().decode(proc.stdout); const parsed = JSON.parse(out); expect(parsed).toHaveProperty("version_path", "Tinas Second Brain/health-tracker/VERSION"); @@ -1003,7 +1003,7 @@ describe("fetchGitClaimed — laundered ls-remote (exit 0, empty output) is neve chmodSync(join(stubDir, "git"), 0o755); const git = (cwd: string, ...args: string[]) => - Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd }); + Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd, timeout: 30_000 }); const cwd = process.cwd(); const oldPath = process.env.PATH; @@ -1052,7 +1052,7 @@ describe("fetchGitClaimed — laundered ls-remote (exit 0, empty output) is neve chmodSync(join(stubDir, "git"), 0o755); const git = (cwd: string, ...args: string[]) => - Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd }); + Bun.spawnSync(["git", "-c", "user.email=t@t", "-c", "user.name=t", ...args], { cwd, timeout: 30_000 }); const cwd = process.cwd(); const oldPath = process.env.PATH; diff --git a/test/gstack-paths.test.ts b/test/gstack-paths.test.ts index 628f59919..50c1c7ed1 100644 --- a/test/gstack-paths.test.ts +++ b/test/gstack-paths.test.ts @@ -19,6 +19,7 @@ function run(env: Record): Record { const result = spawnSync('bash', [BIN], { env: { PATH: process.env.PATH, USERPROFILE: '', ...env } as Record, encoding: 'utf-8', + timeout: 30_000, }); if (result.status !== 0) { throw new Error(`gstack-paths failed (status ${result.status}): ${result.stderr}`); @@ -117,6 +118,7 @@ describe('gstack-paths', () => { { env: { PATH: process.env.PATH, USERPROFILE: '', ...env } as Record, encoding: 'utf-8', + timeout: 30_000, }, ); if (result.status !== 0) { @@ -159,6 +161,7 @@ describe('gstack-paths', () => { const result = spawnSync('bash', [BIN], { env: { PATH: process.env.PATH, USERPROFILE: '', HOME: '/tmp/h' } as Record, encoding: 'utf-8', + timeout: 30_000, }); const lines = result.stdout.split('\n').filter(Boolean); for (const line of lines) { diff --git a/test/gstack-question-log.test.ts b/test/gstack-question-log.test.ts index c99edf3d2..2be6772bc 100644 --- a/test/gstack-question-log.test.ts +++ b/test/gstack-question-log.test.ts @@ -26,6 +26,7 @@ function run(payload: string): { stdout: string; stderr: string; status: number env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout ?? '', diff --git a/test/gstack-question-preference.test.ts b/test/gstack-question-preference.test.ts index 9b4f3c4a3..1b1e03063 100644 --- a/test/gstack-question-preference.test.ts +++ b/test/gstack-question-preference.test.ts @@ -31,6 +31,7 @@ function run(...args: string[]): { stdout: string; stderr: string; status: numbe env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout ?? '', @@ -45,6 +46,7 @@ function runWithStdin(input: string, ...args: string[]): { stdout: string; stder encoding: 'utf-8', cwd: ROOT, input, + timeout: 30_000, }); return { stdout: res.stdout ?? '', diff --git a/test/gstack-redact-cli.test.ts b/test/gstack-redact-cli.test.ts index b41294b64..129125574 100644 --- a/test/gstack-redact-cli.test.ts +++ b/test/gstack-redact-cli.test.ts @@ -22,6 +22,7 @@ function run( ): { code: number; stdout: string; stderr: string } { const proc = Bun.spawnSync(["bun", BIN, ...args], { stdin: Buffer.from(stdin), + timeout: 30_000, }); return { code: proc.exitCode, @@ -88,7 +89,7 @@ describe("gstack-redact --from-file", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "redact-file-")); const f = path.join(dir, "spec.md"); fs.writeFileSync(f, "leaked ghp_" + "a".repeat(36)); - const proc = Bun.spawnSync(["bun", BIN, "--from-file", f, "--json"]); + const proc = Bun.spawnSync(["bun", BIN, "--from-file", f, "--json"], { timeout: 30_000 }); const parsed = JSON.parse(proc.stdout.toString()); expect(parsed.findings[0].id).toBe("github.pat"); fs.rmSync(dir, { recursive: true, force: true }); diff --git a/test/gstack-repo-mode.test.ts b/test/gstack-repo-mode.test.ts index 62088a217..47b382684 100644 --- a/test/gstack-repo-mode.test.ts +++ b/test/gstack-repo-mode.test.ts @@ -25,6 +25,7 @@ function run(command: string, args: string[], cwd: string, home: string): Comman cwd, encoding: 'utf8', env: { ...process.env, HOME: home, GSTACK_HOME: path.join(home, '.gstack') }, + timeout: 30_000, }); return { stdout: result.stdout ?? '', diff --git a/test/gstack-settings-hook-schema-aware.test.ts b/test/gstack-settings-hook-schema-aware.test.ts index a6dbb252a..7a0ce7b83 100644 --- a/test/gstack-settings-hook-schema-aware.test.ts +++ b/test/gstack-settings-hook-schema-aware.test.ts @@ -919,6 +919,7 @@ describe('prune-stale', () => { const live = mkCanon(tmpDir, 'live-worktree'); execSync(`'${path.join(ROOT, 'bin', 'gstack-config')}' set plan_tune_hooks no`, { env: { ...process.env, GSTACK_STATE_ROOT: tmpDir }, + timeout: 30_000, }); fs.writeFileSync(settingsFile, JSON.stringify({ hooks: { diff --git a/test/gstack-slug-cwd-walk-up.test.ts b/test/gstack-slug-cwd-walk-up.test.ts index b129b8e79..4280c2107 100644 --- a/test/gstack-slug-cwd-walk-up.test.ts +++ b/test/gstack-slug-cwd-walk-up.test.ts @@ -172,12 +172,13 @@ describe('gstack-slug — outermost project-root resolution', () => { // succeeds. (The script's step 2 reads the remote when there's no cache.) const gitInit = spawnSync('git', ['init', '-q', '-b', 'main', projectRoot], { encoding: 'utf8', + timeout: 30_000, }); expect(gitInit.status).toBe(0); const gitRemote = spawnSync( 'git', ['-C', projectRoot, 'remote', 'add', 'origin', 'https://github.com/foo/bar.git'], - { encoding: 'utf8' }, + { encoding: 'utf8', timeout: 30_000 }, ); expect(gitRemote.status).toBe(0); diff --git a/test/gstack-slug-sanitize.test.ts b/test/gstack-slug-sanitize.test.ts index ab0eb0b7f..bc3aee3d1 100644 --- a/test/gstack-slug-sanitize.test.ts +++ b/test/gstack-slug-sanitize.test.ts @@ -26,6 +26,7 @@ function runSlug(cwd: string, home: string) { return spawnSync([SLUG_BIN], { cwd, env: { ...process.env, HOME: home }, + timeout: 30_000, }); } @@ -77,6 +78,7 @@ describe('slug cache hygiene', () => { const r = spawnSync(['bash', SLUG_BIN], { cwd: os.tmpdir(), env: { ...process.env, GSTACK_HOME: home, GSTACK_PROJECT_SLUG: 'override-slug' }, + timeout: 30_000, }); expect(r.stdout.toString()).toContain('SLUG=override-slug'); expect(fs.existsSync(path.join(home, 'slug-cache'))).toBe(false); @@ -95,6 +97,7 @@ describe('slug cache hygiene', () => { const r = spawnSync(['bash', SLUG_BIN], { cwd: os.tmpdir(), env: { ...ambient, GSTACK_HOME: home }, + timeout: 30_000, }); expect(r.exitCode).toBe(0); const entries = fs.readdirSync(path.join(home, 'slug-cache')); diff --git a/test/gstack-state-root-override.test.ts b/test/gstack-state-root-override.test.ts index cc2e672d6..75a130220 100644 --- a/test/gstack-state-root-override.test.ts +++ b/test/gstack-state-root-override.test.ts @@ -54,6 +54,7 @@ function runBin( env: cleaned, encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout ?? '', diff --git a/test/gstack-team-init-hook-schema.test.ts b/test/gstack-team-init-hook-schema.test.ts index 9ce2f0cfc..f5ff08a4f 100644 --- a/test/gstack-team-init-hook-schema.test.ts +++ b/test/gstack-team-init-hook-schema.test.ts @@ -42,6 +42,7 @@ describe('gstack-team-init required: PreToolUse hook schema (#2413)', () => { const stdout = execSync(`bash "${hookPath}"`, { env: { ...process.env, HOME: home }, encoding: 'utf-8', + timeout: 30_000, }); return { status: 0, stdout, stderr: '' }; } catch (err) { diff --git a/test/gstack-upgrade-migration-v1_17_0_0.test.ts b/test/gstack-upgrade-migration-v1_17_0_0.test.ts index e1d20a95d..c5ffefe65 100644 --- a/test/gstack-upgrade-migration-v1_17_0_0.test.ts +++ b/test/gstack-upgrade-migration-v1_17_0_0.test.ts @@ -64,6 +64,7 @@ function run(opts: { env?: Record } = {}) { env, encoding: 'utf-8', cwd: tmpHome, + timeout: 30_000, }); } diff --git a/test/gstack-upgrade-migration-v1_40_0_0.test.ts b/test/gstack-upgrade-migration-v1_40_0_0.test.ts index f76815d83..b09fd44f2 100644 --- a/test/gstack-upgrade-migration-v1_40_0_0.test.ts +++ b/test/gstack-upgrade-migration-v1_40_0_0.test.ts @@ -136,6 +136,7 @@ function run(opts: { path?: string } = {}) { env, encoding: "utf-8", cwd: tmpHome, + timeout: 30_000, }); } diff --git a/test/helpers/auq-sdk-capture.ts b/test/helpers/auq-sdk-capture.ts index 9bb08acf7..87191bfe8 100644 --- a/test/helpers/auq-sdk-capture.ts +++ b/test/helpers/auq-sdk-capture.ts @@ -287,7 +287,7 @@ export function verboseSkill(gitRef = 'ab66193e^'): string { } function execGit(args: string[]): string { - const r = spawnSync('git', args, { cwd: ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 }); + const r = spawnSync('git', args, { cwd: ROOT, encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024, timeout: 30_000 }); if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`); return r.stdout; } diff --git a/test/helpers/capture-parity-baseline.ts b/test/helpers/capture-parity-baseline.ts index 9971e266b..7bb794a80 100644 --- a/test/helpers/capture-parity-baseline.ts +++ b/test/helpers/capture-parity-baseline.ts @@ -141,8 +141,8 @@ function discoverEvalCoverage(repoRoot: string, skills: string[]): { function getGitInfo(repoRoot: string): { commit: string; branch: string } { try { - const commit = execSync('git rev-parse --short HEAD', { cwd: repoRoot, encoding: 'utf-8' }).trim(); - const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoRoot, encoding: 'utf-8' }).trim(); + const commit = execSync('git rev-parse --short HEAD', { cwd: repoRoot, encoding: 'utf-8', timeout: 30_000 }).trim(); + const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repoRoot, encoding: 'utf-8', timeout: 30_000 }).trim(); return { commit, branch }; } catch { return { commit: 'unknown', branch: 'unknown' }; diff --git a/test/helpers/codex-session-runner.ts b/test/helpers/codex-session-runner.ts index e6ddf5837..4246c6c01 100644 --- a/test/helpers/codex-session-runner.ts +++ b/test/helpers/codex-session-runner.ts @@ -183,7 +183,7 @@ export async function runCodexSkill(opts: { const name = skillName || path.basename(skillDir) || 'gstack'; // Check if codex binary exists - const whichResult = Bun.spawnSync(['which', 'codex']); + const whichResult = Bun.spawnSync(['which', 'codex'], { timeout: 30_000 }); if (whichResult.exitCode !== 0) { return { output: 'SKIP: codex binary not found', diff --git a/test/helpers/gemini-session-runner.ts b/test/helpers/gemini-session-runner.ts index f9d9c478b..e42f53ade 100644 --- a/test/helpers/gemini-session-runner.ts +++ b/test/helpers/gemini-session-runner.ts @@ -111,7 +111,7 @@ export async function runGeminiSkill(opts: { const startTime = Date.now(); // Check if gemini binary exists - const whichResult = Bun.spawnSync(['which', 'gemini']); + const whichResult = Bun.spawnSync(['which', 'gemini'], { timeout: 30_000 }); if (whichResult.exitCode !== 0) { return { output: 'SKIP: gemini binary not found', diff --git a/test/heredoc-pipe-deadlock.test.ts b/test/heredoc-pipe-deadlock.test.ts index 0518fc366..47df8d81c 100644 --- a/test/heredoc-pipe-deadlock.test.ts +++ b/test/heredoc-pipe-deadlock.test.ts @@ -31,7 +31,7 @@ const MAX_BODY = 64 * 1024; const GUARD_RE = /^\s*(?::\s*"\$\{)?BASH_COMPAT(?:[:=]|\}")/m; function trackedShellScripts(): string[] { - const out = execSync('git ls-files', { cwd: ROOT, encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024 }); + const out = execSync('git ls-files', { cwd: ROOT, encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024, timeout: 30_000 }); return out .split('\n') .map((s) => s.trim()) @@ -88,6 +88,7 @@ describe('heredoc pipe-deadlock guard', () => { test('the guard actually moves the body off the pipe', () => { const bash = spawnSync('bash', ['-c', 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"'], { encoding: 'utf-8', + timeout: 30_000, }); const version = (bash.stdout ?? '').trim(); const [maj, min] = version.split('.').map((n) => parseInt(n, 10)); @@ -102,6 +103,7 @@ describe('heredoc pipe-deadlock guard', () => { // probe would answer OTHER for an unobservable fd. Skip rather than fail. const devStdin = spawnSync('bash', ['-c', '[ -e /dev/stdin ] && echo yes || echo no'], { encoding: 'utf-8', + timeout: 30_000, }); if ((devStdin.stdout ?? '').trim() !== 'yes') return; @@ -114,7 +116,7 @@ $body EOF `; const run = (guard: string) => - (spawnSync('bash', ['-c', probe(guard)], { encoding: 'utf-8' }).stdout ?? '').trim(); + (spawnSync('bash', ['-c', probe(guard)], { encoding: 'utf-8', timeout: 30_000 }).stdout ?? '').trim(); expect(run('')).toBe('PIPE'); expect(run('BASH_COMPAT=50')).toBe('TEMPFILE'); diff --git a/test/host-config.test.ts b/test/host-config.test.ts index cad209557..7cc31ccc8 100644 --- a/test/host-config.test.ts +++ b/test/host-config.test.ts @@ -330,7 +330,7 @@ describe('host-config-export.ts CLI', () => { function run(...args: string[]): { stdout: string; stderr: string; exitCode: number } { const result = Bun.spawnSync(['bun', 'run', EXPORT_SCRIPT, ...args], { - cwd: ROOT, stdout: 'pipe', stderr: 'pipe', + cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000, }); return { stdout: result.stdout.toString().trim(), @@ -445,7 +445,7 @@ describe('golden-file regression', () => { for (const host of ['codex', 'factory']) { const result = Bun.spawnSync( ['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', host, '--out-dir', GOLDEN_OUT], - { cwd: ROOT }, + { cwd: ROOT, timeout: 120_000 }, ); if (result.exitCode !== 0) { throw new Error( diff --git a/test/ios-qa-regen.test.ts b/test/ios-qa-regen.test.ts index 0378869dd..2aa39884c 100644 --- a/test/ios-qa-regen.test.ts +++ b/test/ios-qa-regen.test.ts @@ -105,7 +105,7 @@ describe('gstack-ios-qa-regen', () => { const workDir = mkdtempSync(join(tmpdir(), 'ios-qa-regen-')); workDirs.push(workDir); const { launcher } = copyIntoFakeInstall(workDir); - const result = spawnSync('bash', [launcher, '--app-source', workDir], { encoding: 'utf8' }); + const result = spawnSync('bash', [launcher, '--app-source', workDir], { encoding: 'utf8', timeout: 30_000 }); expect(result.status).toBe(2); expect(result.stderr).toContain('both --app-source and --bridge-dir are required'); @@ -134,6 +134,7 @@ describe('gstack-ios-qa-regen', () => { ], { encoding: 'utf8', env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH ?? ''}` }, + timeout: 30_000, }); expect(result.status).toBe(17); @@ -184,7 +185,7 @@ final class AppState { GEN_ACCESSORS_REV: 'regen-test', }; const args = [launcher, '--app-source', appSource, '--bridge-dir', bridgeDir]; - const first = spawnSync('bash', args, { encoding: 'utf8', env }); + const first = spawnSync('bash', args, { encoding: 'utf8', env, timeout: 30_000 }); expect(first.status).toBe(0); expect(first.stderr).toBe(''); @@ -232,10 +233,11 @@ final class AppState { expect(installedContents).not.toContain('FORBIDDEN-STATE-SENTINEL'); expect(installedContents).not.toContain('OBSOLETE-HARNESS-SENTINEL'); - const swiftAvailable = spawnSync('swift', ['--version'], { encoding: 'utf8' }).status === 0; + const swiftAvailable = spawnSync('swift', ['--version'], { encoding: 'utf8', timeout: 30_000 }).status === 0; if (swiftAvailable) { const dump = spawnSync('swift', ['package', 'dump-package', '--package-path', bridgeDir], { encoding: 'utf8', + timeout: 30_000, }); expect(dump.status).toBe(0); const manifest = JSON.parse(dump.stdout) as { targets: Array<{ name: string }> }; @@ -248,7 +250,7 @@ final class AppState { const firstHash = treeHash(bridgeDir, generatedDir); const firstAccessorHash = accessor.match(/accessorHash: "([a-f0-9]+)"/)?.[1]; - const second = spawnSync('bash', args, { encoding: 'utf8', env }); + const second = spawnSync('bash', args, { encoding: 'utf8', env, timeout: 30_000 }); expect(second.status).toBe(0); expect(second.stderr).toBe(''); expect(second.stdout).toContain('gen-accessors: cache hit'); diff --git a/test/learnings.test.ts b/test/learnings.test.ts index 603fe07f9..e43fe7c04 100644 --- a/test/learnings.test.ts +++ b/test/learnings.test.ts @@ -37,7 +37,7 @@ function runSearch(args: string = ''): string { timeout: 15000, }; try { - return execSync(`${BIN}/gstack-learnings-search ${args}`, execOpts).trim(); + return execSync(`${BIN}/gstack-learnings-search ${args}`, execOpts).trim(); // timeout via execOpts } catch { return ''; } diff --git a/test/memory-cache-injection.test.ts b/test/memory-cache-injection.test.ts index 991a4c019..163eb7286 100644 --- a/test/memory-cache-injection.test.ts +++ b/test/memory-cache-injection.test.ts @@ -53,6 +53,7 @@ function runHook(stdin: object): { stdout: string; stderr: string; status: numbe input: JSON.stringify({ ...stdin, cwd: fixtureCwd }), encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); let parsed: any = null; try { parsed = JSON.parse(res.stdout || '{}'); } catch {} diff --git a/test/migrations-v1.27.0.0.test.ts b/test/migrations-v1.27.0.0.test.ts index 0c62a8c22..12c5f24b1 100644 --- a/test/migrations-v1.27.0.0.test.ts +++ b/test/migrations-v1.27.0.0.test.ts @@ -81,6 +81,7 @@ function run(extraEnv: Record = {}, input = ''): { code: number; encoding: 'utf-8', input, cwd: tmpHome, + timeout: 30_000, }); return { code: r.status ?? -1, stdout: r.stdout || '', stderr: r.stderr || '' }; } diff --git a/test/migrations-v1.65.0.0.test.ts b/test/migrations-v1.65.0.0.test.ts index 554120513..c29cb2366 100644 --- a/test/migrations-v1.65.0.0.test.ts +++ b/test/migrations-v1.65.0.0.test.ts @@ -97,6 +97,7 @@ function run(extraEnv: Record = {}): { }, encoding: 'utf-8', cwd: tmpHome, + timeout: 30_000, }); return { code: r.status ?? -1, stdout: r.stdout || '', stderr: r.stderr || '' }; } diff --git a/test/no-stale-gstack-brain-refs.test.ts b/test/no-stale-gstack-brain-refs.test.ts index 171eb73f7..b2d1da89d 100644 --- a/test/no-stale-gstack-brain-refs.test.ts +++ b/test/no-stale-gstack-brain-refs.test.ts @@ -99,7 +99,7 @@ const SCAN_PATHS = [ function grepRefs(pattern: string): string[] { const args = ['-rn', '--', pattern, ...SCAN_PATHS.map((p) => path.join(ROOT, p))]; - const r = spawnSync('grep', args, { encoding: 'utf-8' }); + const r = spawnSync('grep', args, { encoding: 'utf-8', timeout: 30_000 }); // grep exits 1 when no matches — that's fine for our purposes. const lines = (r.stdout || '').split('\n').filter((l) => l.trim().length > 0); return lines diff --git a/test/paid-orphan-tripwire.test.ts b/test/paid-orphan-tripwire.test.ts index 15e76604a..d34813366 100644 --- a/test/paid-orphan-tripwire.test.ts +++ b/test/paid-orphan-tripwire.test.ts @@ -47,7 +47,7 @@ const GATE_PATTERNS = [ ]; function trackedTestFiles(): string[] { - const out = spawnSync('git', ['ls-files', '*.test.ts'], { cwd: ROOT, encoding: 'utf-8' }); + const out = spawnSync('git', ['ls-files', '*.test.ts'], { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); if (out.status !== 0) throw new Error(`git ls-files failed: ${out.stderr}`); return out.stdout.split('\n').filter(Boolean); } diff --git a/test/plan-tune-gates.test.ts b/test/plan-tune-gates.test.ts index faedf1554..93d842ade 100644 --- a/test/plan-tune-gates.test.ts +++ b/test/plan-tune-gates.test.ts @@ -55,7 +55,7 @@ function runBin( } env.GSTACK_STATE_ROOT = stateRoot; delete env.GSTACK_HOME; - const res = spawnSync(bin, args, { env, encoding: 'utf-8', cwd: ROOT }); + const res = spawnSync(bin, args, { env, encoding: 'utf-8', cwd: ROOT, timeout: 30_000 }); return { stdout: res.stdout ?? '', stderr: res.stderr ?? '', diff --git a/test/plan-tune.test.ts b/test/plan-tune.test.ts index 9bb0230aa..4eb9483ba 100644 --- a/test/plan-tune.test.ts +++ b/test/plan-tune.test.ts @@ -568,15 +568,15 @@ describe('end-to-end pipeline (binaries working together)', () => { ts: `2026-04-0${i + 1}T10:00:00Z`, }), ], - { env, cwd: ROOT, encoding: 'utf-8' }, + { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }, ); expect(r.status).toBe(0); } - const derive = spawnSync(devBin, ['--derive'], { env, cwd: ROOT, encoding: 'utf-8' }); + const derive = spawnSync(devBin, ['--derive'], { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); expect(derive.status).toBe(0); - const profileOut = spawnSync(devBin, ['--profile'], { env, cwd: ROOT, encoding: 'utf-8' }); + const profileOut = spawnSync(devBin, ['--profile'], { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); const p = JSON.parse(profileOut.stdout); expect(p.inferred.sample_size).toBe(5); expect(p.inferred.values.scope_appetite).toBeGreaterThan(0.5); @@ -598,13 +598,13 @@ describe('end-to-end pipeline (binaries working together)', () => { '--write', JSON.stringify({ question_id: 'fake-id', preference: 'never-ask', source: 'inline-tool-output' }), ], - { env, cwd: ROOT, encoding: 'utf-8' }, + { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }, ); expect(r.status).toBe(2); expect(r.stderr).toContain('poisoning'); // Verify no preference was written - const read = spawnSync(prefBin, ['--read'], { env, cwd: ROOT, encoding: 'utf-8' }); + const read = spawnSync(prefBin, ['--read'], { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); const prefs = JSON.parse(read.stdout); expect(prefs['fake-id']).toBeUndefined(); } finally { @@ -633,11 +633,11 @@ describe('end-to-end pipeline (binaries working together)', () => { ); // Migrate - const m = spawnSync(devBin, ['--migrate'], { env, cwd: ROOT, encoding: 'utf-8' }); + const m = spawnSync(devBin, ['--migrate'], { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); expect(m.status).toBe(0); // Legacy shim should still return the same KEY: VALUE shape - const shimOut = spawnSync(shimBin, [], { env, cwd: ROOT, encoding: 'utf-8' }); + const shimOut = spawnSync(shimBin, [], { env, cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }); expect(shimOut.status).toBe(0); expect(shimOut.stdout).toContain('SESSION_COUNT: 3'); expect(shimOut.stdout).toContain('TIER: welcome_back'); diff --git a/test/pr-title-rewrite.test.ts b/test/pr-title-rewrite.test.ts index cfdef402e..d97f4c921 100644 --- a/test/pr-title-rewrite.test.ts +++ b/test/pr-title-rewrite.test.ts @@ -5,7 +5,7 @@ import * as path from 'path'; const HELPER = path.join(import.meta.dir, '..', 'bin', 'gstack-pr-title-rewrite.sh'); function rewrite(version: string, title: string): { stdout: string; status: number; stderr: string } { - const r = spawnSync(HELPER, [version, title], { encoding: 'utf-8' }); + const r = spawnSync(HELPER, [version, title], { encoding: 'utf-8', timeout: 30_000 }); return { stdout: (r.stdout ?? '').trimEnd(), status: r.status ?? -1, stderr: r.stderr ?? '' }; } @@ -54,7 +54,7 @@ describe('gstack-pr-title-rewrite', () => { }); test('errors on missing args', () => { - const r = spawnSync(HELPER, ['1.2.3.4'], { encoding: 'utf-8' }); + const r = spawnSync(HELPER, ['1.2.3.4'], { encoding: 'utf-8', timeout: 30_000 }); expect(r.status).not.toBe(0); }); diff --git a/test/preamble-first-task-scaffold.test.ts b/test/preamble-first-task-scaffold.test.ts index ac4ffab46..fd641bd73 100644 --- a/test/preamble-first-task-scaffold.test.ts +++ b/test/preamble-first-task-scaffold.test.ts @@ -28,7 +28,7 @@ function detect(cwd: string): string { return execFileSync(DETECT, [], { cwd, encoding: 'utf-8', env: GIT_ENV }).trim(); } function git(cwd: string, args: string) { - execSync(`git ${args}`, { cwd, env: GIT_ENV, stdio: 'ignore' }); + execSync(`git ${args}`, { cwd, env: GIT_ENV, stdio: 'ignore', timeout: 30_000 }); } let tmp: string; diff --git a/test/question-log-hook.test.ts b/test/question-log-hook.test.ts index faa0cfc8b..27b6497b9 100644 --- a/test/question-log-hook.test.ts +++ b/test/question-log-hook.test.ts @@ -48,6 +48,7 @@ function runHook(stdin: object): { stdout: string; stderr: string; status: numbe input: JSON.stringify(stdin), encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); return { stdout: res.stdout ?? '', @@ -345,7 +346,7 @@ describe('PostToolUse hook (crash safety)', () => { } env.GSTACK_STATE_ROOT = stateRoot; env.GSTACK_QUESTION_LOG_NO_DERIVE = '1'; - const res = spawnSync(HOOK, [], { env, input: '', encoding: 'utf-8' }); + const res = spawnSync(HOOK, [], { env, input: '', encoding: 'utf-8', timeout: 30_000 }); expect(res.status).toBe(0); }); @@ -360,6 +361,7 @@ describe('PostToolUse hook (crash safety)', () => { env, input: 'not json', encoding: 'utf-8', + timeout: 30_000, }); expect(res.status).toBe(0); // Error logged to hook-errors.log diff --git a/test/question-preference-hook.test.ts b/test/question-preference-hook.test.ts index d96843f4e..098a0e5cc 100644 --- a/test/question-preference-hook.test.ts +++ b/test/question-preference-hook.test.ts @@ -89,6 +89,7 @@ function runHook(stdin: object, cwd?: string, extraEnv?: Record) input: JSON.stringify({ ...stdin, cwd: cwd || fixtureCwd }), encoding: 'utf-8', cwd: ROOT, + timeout: 30_000, }); let parsed: any = null; try { parsed = JSON.parse(res.stdout || '{}'); } catch {} @@ -181,7 +182,7 @@ describe('passes through (no enforcement)', () => { if (v !== undefined) env[k] = v; } env.GSTACK_STATE_ROOT = stateRoot; - const res = spawnSync(HOOK, [], { env, input: '', encoding: 'utf-8' }); + const res = spawnSync(HOOK, [], { env, input: '', encoding: 'utf-8', timeout: 30_000 }); expect(res.status).toBe(0); expect(res.stdout).toBe(''); }); diff --git a/test/readme-throughput.test.ts b/test/readme-throughput.test.ts index 252dfb836..eba52c157 100644 --- a/test/readme-throughput.test.ts +++ b/test/readme-throughput.test.ts @@ -39,6 +39,7 @@ function runScript(cwd: string): { stdout: string; stderr: string; status: numbe encoding: 'utf-8', cwd, env: { ...process.env }, + timeout: 30_000, }); return { stdout: (res.stdout ?? '').trim(), diff --git a/test/redact-audit-log.test.ts b/test/redact-audit-log.test.ts index ce833954c..115fc07fe 100644 --- a/test/redact-audit-log.test.ts +++ b/test/redact-audit-log.test.ts @@ -91,7 +91,7 @@ describe("CLI", () => { const r = spawnSync( "bun", [LIB, JSON.stringify({ repo_visibility: "public", outcome: "flagged", categories_flagged: ["pii"] }), bodyFile], - { env: { ...process.env, GSTACK_HOME: home }, encoding: "utf8" }, + { env: { ...process.env, GSTACK_HOME: home }, encoding: "utf8", timeout: 30_000 }, ); expect(r.status).toBe(0); const line = JSON.parse(fs.readFileSync(logPath(), "utf8").trim()); diff --git a/test/redact-prepush-hook.test.ts b/test/redact-prepush-hook.test.ts index 16f0f6d4b..3c583a3a9 100644 --- a/test/redact-prepush-hook.test.ts +++ b/test/redact-prepush-hook.test.ts @@ -20,7 +20,7 @@ const REDACT = path.resolve(import.meta.dir, "..", "bin", "gstack-redact"); let repo: string; function git(args: string[], cwd = repo): string { - const r = spawnSync("git", args, { cwd, encoding: "utf8" }); + const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000 }); return r.stdout?.trim() ?? ""; } @@ -40,6 +40,7 @@ function runHook( input: Buffer.from(stdinLines), encoding: "utf8", env: { ...process.env, ...env }, + timeout: 30_000, }); return { code: r.status ?? 0, stderr: r.stderr ?? "" }; } @@ -322,7 +323,7 @@ describe("install / chaining", () => { const existing = path.join(hookDir, "pre-push"); fs.writeFileSync(existing, "#!/usr/bin/env bash\necho mine\n", { mode: 0o755 }); - const r = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8" }); + const r = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", timeout: 30_000 }); expect(r.status).toBe(0); const installed = fs.readFileSync(existing, "utf8"); expect(installed).toContain("gstack-redact pre-push (managed)"); @@ -336,7 +337,7 @@ describe("install / chaining", () => { test("chained pre-push.local receives the final ref line (trailing newline preserved)", () => { const hookDir = path.join(repo, ".git", "hooks"); fs.mkdirSync(hookDir, { recursive: true }); - spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo }); + spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000 }); const seen = path.join(repo, "seen.txt"); fs.writeFileSync( @@ -352,6 +353,7 @@ describe("install / chaining", () => { input: Buffer.from(line), encoding: "utf8", env: { ...process.env, GSTACK_REDACT_PREPUSH: "skip" }, + timeout: 30_000, }); expect(r.status).toBe(0); expect(fs.existsSync(seen)).toBe(true); @@ -363,7 +365,7 @@ describe("install / chaining", () => { test("a blocking pre-push.local still short-circuits the push", () => { const hookDir = path.join(repo, ".git", "hooks"); fs.mkdirSync(hookDir, { recursive: true }); - spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo }); + spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000 }); fs.writeFileSync( path.join(hookDir, "pre-push.local"), "#!/usr/bin/env bash\nwhile read -r _a _b _c _d || [ -n \"${_a:-}\" ]; do exit 1; done\nexit 0\n", @@ -374,6 +376,7 @@ describe("install / chaining", () => { input: Buffer.from(`refs/heads/main ${"b".repeat(40)} refs/heads/main ${ZERO}\n`), encoding: "utf8", env: { ...process.env, GSTACK_REDACT_PREPUSH: "skip" }, + timeout: 30_000, }); expect(r.status).toBe(1); }); @@ -384,8 +387,8 @@ describe("install / chaining", () => { fs.writeFileSync(path.join(hookDir, "pre-push"), "#!/usr/bin/env bash\necho mine\n", { mode: 0o755, }); - spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo }); - spawnSync("bun", [REDACT, "uninstall-prepush-hook"], { cwd: repo }); + spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, timeout: 30_000 }); + spawnSync("bun", [REDACT, "uninstall-prepush-hook"], { cwd: repo, timeout: 30_000 }); const restored = fs.readFileSync(path.join(hookDir, "pre-push"), "utf8"); expect(restored).toContain("echo mine"); expect(restored).not.toContain("managed"); @@ -404,7 +407,7 @@ describe("base resolution when the default branch is neither main nor master", ( // blocks the push having scanned NOTHING — the "scans more, never less" // fallback inverting into "scans nothing". const bare = fs.mkdtempSync(path.join(os.tmpdir(), "prepush-remote-")); - spawnSync("git", ["init", "-q", "--bare", "-b", "trunk", bare]); + spawnSync("git", ["init", "-q", "--bare", "-b", "trunk", bare], { timeout: 30_000 }); git(["branch", "-M", "trunk"]); const old = commit("legacy.txt", FAKE_AWS_KEY + "\n", "secret already on the remote"); diff --git a/test/redact-prepush-rebase-force-push.test.ts b/test/redact-prepush-rebase-force-push.test.ts index d1852b641..b5979ae3f 100644 --- a/test/redact-prepush-rebase-force-push.test.ts +++ b/test/redact-prepush-rebase-force-push.test.ts @@ -38,7 +38,7 @@ let repo: string; let remote: string; function git(args: string[], cwd = repo): string { - const r = spawnSync("git", args, { cwd, encoding: "utf8" }); + const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000 }); if (r.status !== 0) throw new Error(`git ${args.join(" ")}\n${r.stderr}`); return r.stdout?.trim() ?? ""; } @@ -56,6 +56,7 @@ function runHook(stdinLines: string): { code: number; stderr: string } { cwd: repo, input: Buffer.from(stdinLines), encoding: "utf8", + timeout: 30_000, env: { ...process.env }, }); return { code: r.status ?? 0, stderr: r.stderr ?? "" }; @@ -115,7 +116,7 @@ describe("rebased force-push does not re-scan upstream commits (#2573)", () => { // The rebased tip exists locally and is NOT an ancestor of HEAD — the // exact condition #2573 identified as the untested third branch. expect(git(["cat-file", "-t", preRebaseTip])).toBe("commit"); - const isAncestor = spawnSync("git", ["merge-base", "--is-ancestor", preRebaseTip, "HEAD"], { cwd: repo }); + const isAncestor = spawnSync("git", ["merge-base", "--is-ancestor", preRebaseTip, "HEAD"], { cwd: repo, timeout: 30_000 }); expect(isAncestor.status).not.toBe(0); // What the OLD range would scan: upstream's published fixture included. const oldDiff = git(["diff", "--unified=0", `${preRebaseTip}..HEAD`]); diff --git a/test/redact-prepush-scan-range.test.ts b/test/redact-prepush-scan-range.test.ts index f44df28c4..2f2d31ec2 100644 --- a/test/redact-prepush-scan-range.test.ts +++ b/test/redact-prepush-scan-range.test.ts @@ -23,7 +23,7 @@ import { dirname, join } from "path"; let dir: string; const run = (args: string[], cwd = dir): string => { - const r = spawnSync("git", args, { cwd, encoding: "utf8" }); + const r = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000 }); if (r.status !== 0) throw new Error(`git ${args.join(" ")}\n${r.stderr}`); return r.stdout ?? ""; }; @@ -146,7 +146,7 @@ describe("narrowing the range does not narrow coverage", () => { run(["push", "-q", "origin", "main"]); run(["fetch", "-q", "origin"]); run(["checkout", "-q", "feature"]); - spawnSync("git", ["merge", "--no-edit", "main"], { cwd: dir, encoding: "utf8" }); // conflicts + spawnSync("git", ["merge", "--no-edit", "main"], { cwd: dir, encoding: "utf8", timeout: 30_000 }); // conflicts writeFileSync(join(dir, "conflict.txt"), `resolved ${FAKE_AWS_RESOLV}\n`); run(["add", "conflict.txt"]); run(["commit", "-q", "--no-edit"]); @@ -178,6 +178,7 @@ describe("S1: exclusion scoped to the push-target remote", () => { input: Buffer.from(stdinLines), encoding: "utf8", env: { ...process.env }, + timeout: 30_000, }); return { code: r.status ?? 0, stderr: r.stderr ?? "" }; } diff --git a/test/regression-issue2091-bsd-mktemp.test.ts b/test/regression-issue2091-bsd-mktemp.test.ts index 0bb65a638..d25e2e93f 100644 --- a/test/regression-issue2091-bsd-mktemp.test.ts +++ b/test/regression-issue2091-bsd-mktemp.test.ts @@ -138,6 +138,7 @@ function tmpRoot(env: Record): string { const result = spawnSync('bash', [PATHS_BIN], { env: { PATH: process.env.PATH, USERPROFILE: '', ...env } as Record, encoding: 'utf-8', + timeout: 30_000, }); if (result.status !== 0) { throw new Error(`gstack-paths failed (status ${result.status}): ${result.stderr}`); diff --git a/test/regression-pr1169-build-app-sed.test.ts b/test/regression-pr1169-build-app-sed.test.ts index 8d2596112..e03e5e65e 100644 --- a/test/regression-pr1169-build-app-sed.test.ts +++ b/test/regression-pr1169-build-app-sed.test.ts @@ -55,7 +55,7 @@ describe("PR #1169 bug #2: build-app.sh sed escape for $APP_NAME", () => { "_", appName, ], - { encoding: "utf-8" } + { encoding: "utf-8", timeout: 30_000 } ); expect(result.status).toBe(0); @@ -149,6 +149,7 @@ describe("PR #1169 bug #3: build-app.sh DMG_TMP mktemp failure guard", () => { { encoding: "utf-8", env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}` }, + timeout: 30_000, } ); diff --git a/test/review-log.test.ts b/test/review-log.test.ts index a3fc099a8..32c7aa510 100644 --- a/test/review-log.test.ts +++ b/test/review-log.test.ts @@ -19,7 +19,7 @@ function run(input: string, opts: { expectFail?: boolean } = {}): { stdout: stri timeout: 10000, }; try { - const stdout = execSync(`${BIN}/gstack-review-log '${input.replace(/'/g, "'\\''")}'`, execOpts).trim(); + const stdout = execSync(`${BIN}/gstack-review-log '${input.replace(/'/g, "'\\''")}'`, execOpts).trim(); // timeout via execOpts return { stdout, exitCode: 0 }; } catch (e: any) { if (opts.expectFail) { @@ -119,7 +119,7 @@ describe('gstack-review-log', () => { encoding: 'utf-8', timeout: 10000, }; - execSync(`${BIN}/gstack-review-log '{"skill":"review","status":"clean"}'`, execOpts); + execSync(`${BIN}/gstack-review-log '{"skill":"review","status":"clean"}'`, execOpts); // timeout via execOpts // A record landed somewhere under projects/ without a wtree stamp. const found: string[] = []; const walk = (d: string) => { diff --git a/test/routing-probe.test.ts b/test/routing-probe.test.ts index 3d9fb463c..6fb15f131 100644 --- a/test/routing-probe.test.ts +++ b/test/routing-probe.test.ts @@ -78,7 +78,7 @@ describe('routing probe checks AGENTS.md too (#2500)', () => { ); const out = execSync( `bash -c '${probe.replace(/'/g, `'\\''`)}\necho "HAS_ROUTING: $_HAS_ROUTING"'`, - { cwd: dir, encoding: 'utf-8' }, + { cwd: dir, encoding: 'utf-8', timeout: 30_000 }, ); expect(out).toContain('HAS_ROUTING: yes'); } finally { @@ -92,7 +92,7 @@ describe('routing probe checks AGENTS.md too (#2500)', () => { try { const out = execSync( `bash -c '${probe.replace(/'/g, `'\\''`)}\necho "HAS_ROUTING: $_HAS_ROUTING"'`, - { cwd: dir, encoding: 'utf-8' }, + { cwd: dir, encoding: 'utf-8', timeout: 30_000 }, ); expect(out).toContain('HAS_ROUTING: no'); } finally { diff --git a/test/sandbox-doctor-shell.test.ts b/test/sandbox-doctor-shell.test.ts index 54685f43a..5c084afce 100644 --- a/test/sandbox-doctor-shell.test.ts +++ b/test/sandbox-doctor-shell.test.ts @@ -17,7 +17,7 @@ const SCRIPT = path.join(import.meta.dir, '..', 'scripts', 'sandbox-doctor.sh'); describe('sandbox-doctor.sh', () => { test('parses as POSIX sh, fails fast, and guards every mutation for idempotency', () => { // Syntax: `sh -n` parses without executing. - const parse = spawnSync('sh', ['-n', SCRIPT], { encoding: 'utf8' }); + const parse = spawnSync('sh', ['-n', SCRIPT], { encoding: 'utf8', timeout: 30_000 }); expect(parse.status, parse.stderr).toBe(0); const src = fs.readFileSync(SCRIPT, 'utf-8'); diff --git a/test/session-update-autostash.test.ts b/test/session-update-autostash.test.ts index dc7f89b54..341ddc118 100644 --- a/test/session-update-autostash.test.ts +++ b/test/session-update-autostash.test.ts @@ -205,7 +205,7 @@ describe('gstack-session-update lock identity + TTL (#2613)', () => { test('a dead pid is reclaimed and the run proceeds', async () => { const { base, install, state } = makeFixture(); try { - const dead = spawnSync('true', { encoding: 'utf8' }); // reaped by the time spawnSync returns + const dead = spawnSync('true', { encoding: 'utf8', timeout: 30_000 }); // reaped by the time spawnSync returns const lockDir = path.join(state, '.setup-lock'); fs.mkdirSync(lockDir, { recursive: true }); fs.writeFileSync(path.join(lockDir, 'pid'), String(dead.pid)); diff --git a/test/setup-bun-cmd-and-pipe-bugs.test.ts b/test/setup-bun-cmd-and-pipe-bugs.test.ts index 098597d55..21a73f69b 100644 --- a/test/setup-bun-cmd-and-pipe-bugs.test.ts +++ b/test/setup-bun-cmd-and-pipe-bugs.test.ts @@ -9,7 +9,7 @@ const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8'); // Run a bash snippet, return {stdout, stderr, status}. function runBash(script: string): { stdout: string; stderr: string; status: number } { - const r = spawnSync('bash', ['-c', script], { encoding: 'utf-8' }); + const r = spawnSync('bash', ['-c', script], { encoding: 'utf-8', timeout: 30_000 }); return { stdout: r.stdout || '', stderr: r.stderr || '', status: r.status ?? -1 }; } diff --git a/test/setup-conductor-worktree.test.ts b/test/setup-conductor-worktree.test.ts index 29609ac8f..2ff48eefa 100644 --- a/test/setup-conductor-worktree.test.ts +++ b/test/setup-conductor-worktree.test.ts @@ -43,7 +43,7 @@ describe('setup: Conductor worktree guard', () => { fs.mkdirSync(source); fs.mkdirSync(dest); // The buggy invocation: target dest is an existing real dir. - const result = spawnSync('ln', ['-snf', source, dest], { encoding: 'utf-8' }); + const result = spawnSync('ln', ['-snf', source, dest], { encoding: 'utf-8', timeout: 30_000 }); expect(result.status).toBe(0); // Child symlink leaked inside dest. const leaked = path.join(dest, path.basename(source)); @@ -85,7 +85,7 @@ describe('setup: Conductor worktree guard', () => { echo "LINKED" fi `; - const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8' }); + const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8', timeout: 30_000 }); expect(result.status).toBe(0); expect(result.stdout.trim()).toBe('SKIP'); // No child symlink leaked. @@ -120,7 +120,7 @@ describe('setup: Conductor worktree guard', () => { echo "LINKED" fi `; - const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8' }); + const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8', timeout: 30_000 }); expect(result.status).toBe(0); expect(result.stdout.trim()).toBe('LINKED'); expect(fs.lstatSync(dest).isSymbolicLink()).toBe(true); @@ -159,7 +159,7 @@ describe('setup: Conductor worktree guard', () => { echo "LINKED" fi `; - const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8' }); + const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8', timeout: 30_000 }); expect(result.status).toBe(0); expect(result.stdout.trim()).toBe('LINKED'); expect(fs.readlinkSync(dest)).toBe(source); @@ -191,7 +191,7 @@ describe('setup: Conductor worktree guard', () => { fi echo "skip=$_SKIP_CLAUDE_REGISTER" `; - const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8' }); + const result = spawnSync('bash', ['-c', script], { encoding: 'utf-8', timeout: 30_000 }); expect(result.status).toBe(0); expect(result.stdout.trim()).toBe('skip=0'); } finally { diff --git a/test/setup-plan-tune-hooks-noninteractive.test.ts b/test/setup-plan-tune-hooks-noninteractive.test.ts index cb4871a41..a9da5d26a 100644 --- a/test/setup-plan-tune-hooks-noninteractive.test.ts +++ b/test/setup-plan-tune-hooks-noninteractive.test.ts @@ -119,7 +119,7 @@ describe('gstack-config: has subcommand (key-presence provenance)', () => { function has(key: string): number { try { - execSync(`${GSTACK_CONFIG} has '${key}'`, { encoding: 'utf-8', env: env2 }); + execSync(`${GSTACK_CONFIG} has '${key}'`, { encoding: 'utf-8', env: env2, timeout: 30_000 }); return 0; } catch (e: any) { return e.status ?? 1; @@ -128,12 +128,12 @@ describe('gstack-config: has subcommand (key-presence provenance)', () => { test('absent key exits nonzero even though get returns the default', () => { expect(has('plan_tune_hooks')).not.toBe(0); - const got = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env: env2 }).trim(); + const got = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env: env2, timeout: 30_000 }).trim(); expect(got).toBe('prompt'); // default — indistinguishable from a saved value via get }); test('present key exits 0 through the same STATE_DIR resolution as get', () => { - execSync(`${GSTACK_CONFIG} set plan_tune_hooks no`, { encoding: 'utf-8', env: env2 }); + execSync(`${GSTACK_CONFIG} set plan_tune_hooks no`, { encoding: 'utf-8', env: env2, timeout: 30_000 }); expect(has('plan_tune_hooks')).toBe(0); // GSTACK_STATE_ROOT was the writer — a hardcoded ~/.gstack grep would miss it. }); @@ -164,29 +164,30 @@ describe('gstack-config: plan_tune_hooks key', () => { const out = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env, + timeout: 30_000, }).trim(); expect(out).toBe('prompt'); }); test('appears in defaults and list output', () => { - const defaults = execSync(`${GSTACK_CONFIG} defaults`, { encoding: 'utf-8', env }); + const defaults = execSync(`${GSTACK_CONFIG} defaults`, { encoding: 'utf-8', env, timeout: 30_000 }); expect(defaults).toContain('plan_tune_hooks'); - const list = execSync(`${GSTACK_CONFIG} list`, { encoding: 'utf-8', env }); + const list = execSync(`${GSTACK_CONFIG} list`, { encoding: 'utf-8', env, timeout: 30_000 }); expect(list).toContain('plan_tune_hooks'); }); test('accepts valid values (round-trips yes/no/prompt)', () => { for (const v of ['yes', 'no', 'prompt']) { - execSync(`${GSTACK_CONFIG} set plan_tune_hooks ${v}`, { encoding: 'utf-8', env }); - const got = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env }).trim(); + execSync(`${GSTACK_CONFIG} set plan_tune_hooks ${v}`, { encoding: 'utf-8', env, timeout: 30_000 }); + const got = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env, timeout: 30_000 }).trim(); expect(got).toBe(v); } }); test('rejects out-of-domain values (warns + falls back to prompt)', () => { - const res = execSync(`${GSTACK_CONFIG} set plan_tune_hooks maybe 2>&1`, { encoding: 'utf-8', env }); + const res = execSync(`${GSTACK_CONFIG} set plan_tune_hooks maybe 2>&1`, { encoding: 'utf-8', env, timeout: 30_000 }); expect(res.toLowerCase()).toContain('not recognized'); - const got = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env }).trim(); + const got = execSync(`${GSTACK_CONFIG} get plan_tune_hooks`, { encoding: 'utf-8', env, timeout: 30_000 }).trim(); expect(got).toBe('prompt'); }); }); diff --git a/test/ship-version-sync.test.ts b/test/ship-version-sync.test.ts index c657795c5..6504fa665 100644 --- a/test/ship-version-sync.test.ts +++ b/test/ship-version-sync.test.ts @@ -69,7 +69,7 @@ else fi fi`; try { - const stdout = execSync(script, { shell: "/bin/bash", encoding: "utf8" }); + const stdout = execSync(script, { shell: "/bin/bash", encoding: "utf8", timeout: 30_000 }); return { stdout: stdout.trim(), code: 0 }; } catch (e: any) { return { stdout: (e.stdout || "").toString().trim(), code: e.status ?? 1 }; @@ -88,7 +88,7 @@ if [ -f package.json ]; then node -e 'const fs=require("fs"),p=require("./package.json");p.version=process.argv[1];fs.writeFileSync("package.json",JSON.stringify(p,null,2)+"\\n")' "$NEW_VERSION" fi`; try { - execSync(script, { shell: "/bin/bash", stdio: "pipe" }); + execSync(script, { shell: "/bin/bash", stdio: "pipe", timeout: 30_000 }); return { code: 0 }; } catch (e: any) { return { code: e.status ?? 1 }; @@ -104,7 +104,7 @@ if ! printf '%s' "$REPAIR_VERSION" | grep -qE '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9] fi node -e 'const fs=require("fs"),p=require("./package.json");p.version=process.argv[1];fs.writeFileSync("package.json",JSON.stringify(p,null,2)+"\\n")' "$REPAIR_VERSION"`; try { - execSync(script, { shell: "/bin/bash", stdio: "pipe" }); + execSync(script, { shell: "/bin/bash", stdio: "pipe", timeout: 30_000 }); return { code: 0 }; } catch (e: any) { return { code: e.status ?? 1 }; diff --git a/test/skill-e2e-auto-decide-preserved.test.ts b/test/skill-e2e-auto-decide-preserved.test.ts index 2feca71cf..3482f2fe1 100644 --- a/test/skill-e2e-auto-decide-preserved.test.ts +++ b/test/skill-e2e-auto-decide-preserved.test.ts @@ -59,6 +59,7 @@ describeE2E('AUTO_DECIDE opt-in preserved under Conductor flags (periodic)', () const setRes = spawnSync(configBin, ['set', 'question_tuning', 'true'], { env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', + timeout: 30_000, }); if (setRes.status !== 0) { throw new Error(`gstack-config set failed: ${setRes.stderr || setRes.stdout}`); @@ -73,6 +74,7 @@ describeE2E('AUTO_DECIDE opt-in preserved under Conductor flags (periodic)', () cwd: ROOT, env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', + timeout: 30_000, }); // gstack-slug emits `eval`-able shell exports like `SLUG=garrytan-gstack`. const slug = (slugRes.stdout.match(/SLUG=([^\s;]+)/)?.[1] ?? 'unknown').replace(/['"]/g, ''); @@ -90,6 +92,7 @@ describeE2E('AUTO_DECIDE opt-in preserved under Conductor flags (periodic)', () { env: { ...process.env, GSTACK_HOME: tmpHome }, encoding: 'utf-8', + timeout: 30_000, }, ); if (writeRes.status !== 0) { diff --git a/test/skill-e2e-design.test.ts b/test/skill-e2e-design.test.ts index 9c97fe665..c0be419ab 100644 --- a/test/skill-e2e-design.test.ts +++ b/test/skill-e2e-design.test.ts @@ -615,7 +615,7 @@ Review the site at ${serverUrl}. Use --quick mode. Skip any AskUserQuestion call // Check if any design fix commits were made const gitLog = spawnSync('git', ['log', '--oneline'], { - cwd: qaDesignDir, stdio: 'pipe', + cwd: qaDesignDir, stdio: 'pipe', timeout: 30_000, }); const commits = gitLog.stdout.toString().trim().split('\n'); const designFixCommits = commits.filter((c: string) => c.includes('style(design)')); diff --git a/test/skill-e2e-first-task-scaffold.test.ts b/test/skill-e2e-first-task-scaffold.test.ts index 619eba1ce..ccb4df40d 100644 --- a/test/skill-e2e-first-task-scaffold.test.ts +++ b/test/skill-e2e-first-task-scaffold.test.ts @@ -78,10 +78,10 @@ describeIfSelected('first-run scaffold detection (E2E)', ['first-task-scaffold'] // greenfield bucket: git repo, zero commits. const greenDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fts-green-')); try { - execSync('git init -q -b main', { cwd: nodeDir, env: GIT_ENV }); + execSync('git init -q -b main', { cwd: nodeDir, env: GIT_ENV, timeout: 30_000 }); fs.writeFileSync(path.join(nodeDir, 'package.json'), '{"name":"x"}'); - execSync('git add -A && git commit -qm init', { cwd: nodeDir, env: GIT_ENV }); - execSync('git init -q -b main', { cwd: greenDir, env: GIT_ENV }); + execSync('git add -A && git commit -qm init', { cwd: nodeDir, env: GIT_ENV, timeout: 30_000 }); + execSync('git init -q -b main', { cwd: greenDir, env: GIT_ENV, timeout: 30_000 }); const nodeOut = await detectVia(nodeDir, 'first-task-scaffold'); expect(nodeOut).toContain('code_node'); diff --git a/test/skill-e2e-ios-device.test.ts b/test/skill-e2e-ios-device.test.ts index 678d80be7..96bd55d2c 100644 --- a/test/skill-e2e-ios-device.test.ts +++ b/test/skill-e2e-ios-device.test.ts @@ -280,7 +280,7 @@ describeIfDevice('ios device path', () => { test('fixture iOS SDK and UIKit compile guards are available', () => { // This is an environment + source-guard preflight. The explicit deployment // test below performs the real signed iOS xcodebuild before installation. - const sdkPath = spawnSync('xcrun', ['--sdk', 'iphoneos', '--show-sdk-path'], { stdio: 'pipe' }); + const sdkPath = spawnSync('xcrun', ['--sdk', 'iphoneos', '--show-sdk-path'], { stdio: 'pipe', timeout: 30_000 }); if (sdkPath.status !== 0) { console.error('iOS SDK not found. Install via Xcode.'); } diff --git a/test/skill-e2e-ios-swift-build.test.ts b/test/skill-e2e-ios-swift-build.test.ts index 8fd126e15..529c6348a 100644 --- a/test/skill-e2e-ios-swift-build.test.ts +++ b/test/skill-e2e-ios-swift-build.test.ts @@ -299,7 +299,7 @@ describe('iOS tap harness regressions', () => { }); function hasSwift(): boolean { - const r = spawnSync('swift', ['--version'], { stdio: 'pipe' }); + const r = spawnSync('swift', ['--version'], { stdio: 'pipe', timeout: 30_000 }); return r.status === 0; } @@ -370,14 +370,14 @@ describeIfSwift('swift build invariants', () => { '-path', '*/release/*', '-name', '*.o', '-path', '*DebugBridge*', - ], { stdio: 'pipe' }); + ], { stdio: 'pipe', timeout: 30_000 }); const files = (oFiles.stdout?.toString() ?? '').trim().split('\n').filter(Boolean); expect(files.length).toBeGreaterThan(0); let foundForbidden = 0; const forbidden = ['StateServer', 'handleRequest', 'sessionAcquire', 'authRotate', 'snapshotGet']; for (const f of files) { - const nm = spawnSync('nm', ['-j', f], { stdio: 'pipe' }); + const nm = spawnSync('nm', ['-j', f], { stdio: 'pipe', timeout: 30_000 }); const syms = nm.stdout?.toString() ?? ''; for (const tok of forbidden) { if (syms.includes(tok)) { diff --git a/test/skill-e2e-plan-tune-cathedral.test.ts b/test/skill-e2e-plan-tune-cathedral.test.ts index 85a455036..bd5d8fc3c 100644 --- a/test/skill-e2e-plan-tune-cathedral.test.ts +++ b/test/skill-e2e-plan-tune-cathedral.test.ts @@ -45,12 +45,12 @@ function scaffoldFixture(prefix: string): { workDir: string; stateRoot: string; fs.mkdirSync(stateRoot, { recursive: true }); // git init so gstack-slug resolves a deterministic slug. - spawnSync('git', ['init', '-b', 'main'], { cwd: workDir, stdio: 'pipe' }); - spawnSync('git', ['config', 'user.email', 't@t.com'], { cwd: workDir, stdio: 'pipe' }); - spawnSync('git', ['config', 'user.name', 'T'], { cwd: workDir, stdio: 'pipe' }); + spawnSync('git', ['init', '-b', 'main'], { cwd: workDir, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['config', 'user.email', 't@t.com'], { cwd: workDir, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['config', 'user.name', 'T'], { cwd: workDir, stdio: 'pipe', timeout: 30_000 }); fs.writeFileSync(path.join(workDir, 'README.md'), '# cathedral fixture\n'); - spawnSync('git', ['add', '.'], { cwd: workDir, stdio: 'pipe' }); - spawnSync('git', ['commit', '-m', 'init'], { cwd: workDir, stdio: 'pipe' }); + spawnSync('git', ['add', '.'], { cwd: workDir, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['commit', '-m', 'init'], { cwd: workDir, stdio: 'pipe', timeout: 30_000 }); // Copy bins. const binDir = path.join(workDir, 'bin'); @@ -146,6 +146,7 @@ describeIfSelected('PlanTune cathedral E2E: hook capture', ['plan-tune-hook-capt }, input: JSON.stringify(payload), encoding: 'utf-8', + timeout: 30_000, }); expect(res.status).toBe(0); const logPath = path.join(fixture.stateRoot, 'projects', fixture.slug, 'question-log.jsonl'); @@ -209,6 +210,7 @@ describeIfSelected('PlanTune cathedral E2E: enforcement', ['plan-tune-enforcemen }, input: JSON.stringify(payload), encoding: 'utf-8', + timeout: 30_000, }); expect(res.status).toBe(0); const parsed = JSON.parse(res.stdout || '{}'); @@ -293,6 +295,7 @@ describeIfSelected('PlanTune cathedral E2E: annotation', ['plan-tune-annotation' }, input: JSON.stringify(payload), encoding: 'utf-8', + timeout: 30_000, }); expect(res.status).toBe(0); const parsed = JSON.parse(res.stdout || '{}'); @@ -352,6 +355,7 @@ describeIfSelected('PlanTune cathedral E2E: codex import', ['plan-tune-codex-imp }, encoding: 'utf-8', cwd: fixture.workDir, + timeout: 30_000, }); expect(res.status).toBe(0); expect(res.stdout).toContain('IMPORTED: 1'); @@ -412,6 +416,7 @@ describeIfSelected('PlanTune cathedral E2E: dream cycle', ['plan-tune-dream-cycl env: { ...process.env, GSTACK_STATE_ROOT: fixture.stateRoot }, encoding: 'utf-8', cwd: fixture.workDir, + timeout: 30_000, }); expect(applyRes.status).toBe(0); @@ -453,6 +458,7 @@ describeIfSelected('PlanTune cathedral E2E: dream cycle', ['plan-tune-dream-cycl }, input: JSON.stringify(payload), encoding: 'utf-8', + timeout: 30_000, }); expect(hookRes.status).toBe(0); const parsed = JSON.parse(hookRes.stdout || '{}'); diff --git a/test/skill-e2e-preamble-script-ab.test.ts b/test/skill-e2e-preamble-script-ab.test.ts index 2a2603c8a..e3dcd06c5 100644 --- a/test/skill-e2e-preamble-script-ab.test.ts +++ b/test/skill-e2e-preamble-script-ab.test.ts @@ -45,6 +45,7 @@ function inlineSkill(): string { cwd: ROOT, encoding: 'utf-8', maxBuffer: 8 * 1024 * 1024, + timeout: 30_000, }); } diff --git a/test/skill-e2e-qa-workflow.test.ts b/test/skill-e2e-qa-workflow.test.ts index d2c028d46..a07315d7f 100644 --- a/test/skill-e2e-qa-workflow.test.ts +++ b/test/skill-e2e-qa-workflow.test.ts @@ -151,7 +151,7 @@ Write your report to ${qaOnlyDir}/qa-reports/qa-only-report.md`, // Verify git working tree is still clean (no source modifications) const gitStatus = spawnSync('git', ['status', '--porcelain'], { - cwd: qaOnlyDir, stdio: 'pipe', + cwd: qaOnlyDir, stdio: 'pipe', timeout: 30_000, }); const statusLines = gitStatus.stdout.toString().trim().split('\n').filter( (l: string) => l.trim() && !l.includes('.prompt-tmp') && !l.includes('.gstack/') && !l.includes('qa-reports/'), @@ -263,7 +263,7 @@ This is a test+fix loop: find bugs, fix them in the source code, commit each fix // Verify at least one fix commit was made beyond the initial commit const gitLog = spawnSync('git', ['log', '--oneline'], { - cwd: qaFixDir, stdio: 'pipe', + cwd: qaFixDir, stdio: 'pipe', timeout: 30_000, }); const commits = gitLog.stdout.toString().trim().split('\n'); console.log(`/qa fix loop: ${commits.length} commits total (1 initial + ${commits.length - 1} fixes)`); diff --git a/test/skill-e2e-review-attribution.test.ts b/test/skill-e2e-review-attribution.test.ts index 6a60f1d1f..5098eb889 100644 --- a/test/skill-e2e-review-attribution.test.ts +++ b/test/skill-e2e-review-attribution.test.ts @@ -184,7 +184,7 @@ describeIfSelected('Review Dashboard Via Attribution', ['review-dashboard-via'], run('git', ['commit', '-m', 'feat: update']); // Get HEAD commit for review entries - const headResult = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: dashDir, stdio: 'pipe' }); + const headResult = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: dashDir, stdio: 'pipe', timeout: 30_000 }); const commit = headResult.stdout.toString().trim(); // Pre-populate review log with autoplan-sourced entries diff --git a/test/skill-e2e-session-intelligence.test.ts b/test/skill-e2e-session-intelligence.test.ts index ae8bb50f8..90fdbe149 100644 --- a/test/skill-e2e-session-intelligence.test.ts +++ b/test/skill-e2e-session-intelligence.test.ts @@ -78,14 +78,14 @@ describeIfSelected('Session Intelligence E2E', [ spawnSync(logBin, [JSON.stringify({ skill: 'review', event: 'started', branch: 'main', session: 'test-1', - })], opts); + })], opts); // timeout via opts spawnSync(logBin, [JSON.stringify({ skill: 'review', event: 'completed', branch: 'main', outcome: 'success', duration_s: 120, session: 'test-1', - })], opts); + })], opts); // timeout via opts // Read via gstack-timeline-read - const readResult = spawnSync(readBin, ['--branch', 'main'], opts); + const readResult = spawnSync(readBin, ['--branch', 'main'], opts); // timeout via opts const readOutput = readResult.stdout?.toString() || ''; // Verify timeline.jsonl exists and has content diff --git a/test/skill-e2e-ship-idempotency.test.ts b/test/skill-e2e-ship-idempotency.test.ts index a20020dfa..29ef83827 100644 --- a/test/skill-e2e-ship-idempotency.test.ts +++ b/test/skill-e2e-ship-idempotency.test.ts @@ -137,12 +137,12 @@ function snapshotFixture(workTree: string): FixtureSnapshot { const changelog = fs.readFileSync(path.join(workTree, 'CHANGELOG.md'), 'utf-8'); // Count `## [0.0.2]` headings — should stay at 1 across re-runs. const changelogEntryCount = (changelog.match(/^##\s*\[0\.0\.2\]/gm) ?? []).length; - const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: workTree, stdio: 'pipe' }); + const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: workTree, stdio: 'pipe', timeout: 30_000 }); const branchHead = head.stdout?.toString().trim() ?? ''; // Count "chore: bump version" commits on this branch since main. const log = spawnSync( 'git', ['log', '--format=%s', 'main..HEAD'], - { cwd: workTree, stdio: 'pipe' }, + { cwd: workTree, stdio: 'pipe', timeout: 30_000 }, ); const subjects = log.stdout?.toString() ?? ''; const bumpCommitCount = subjects.split('\n').filter(s => /chore:\s*bump\s+version/i.test(s)).length; diff --git a/test/skill-e2e-workflow.test.ts b/test/skill-e2e-workflow.test.ts index 055974e9c..5db37f089 100644 --- a/test/skill-e2e-workflow.test.ts +++ b/test/skill-e2e-workflow.test.ts @@ -183,7 +183,7 @@ describeIfSelected('Ship workflow E2E', ['ship-local-workflow'], () => { logCost('/ship local workflow', result); // Check push succeeded — verify the feature branch exists on the bare remote - const branchCheck = spawnSync('git', ['branch', '--list', 'feature/ship-test'], { cwd: shipRemoteDir, stdio: 'pipe' }); + const branchCheck = spawnSync('git', ['branch', '--list', 'feature/ship-test'], { cwd: shipRemoteDir, stdio: 'pipe', timeout: 30_000 }); const branchExists = branchCheck.stdout.toString().trim().length > 0; // Check VERSION was bumped locally (even if push failed, this shows the LLM did the work) diff --git a/test/skill-size-budget.test.ts b/test/skill-size-budget.test.ts index 3c61638f0..aa8104723 100644 --- a/test/skill-size-budget.test.ts +++ b/test/skill-size-budget.test.ts @@ -239,7 +239,7 @@ describe('SKILL.md size budget regression (gate, free)', () => { // estimate was a moving target: 4177 solo, 8356 and 8041 in two parallel // runs. A repo-budget ratchet measures the catalog that ships; CI always // checks the PR's committed tree anyway. - const trackedPaths = execSync('git ls-files -- "*/SKILL.md"', { cwd: REPO_ROOT, encoding: 'utf-8' }) + const trackedPaths = execSync('git ls-files -- "*/SKILL.md"', { cwd: REPO_ROOT, encoding: 'utf-8', timeout: 30_000 }) .split('\n') .filter(Boolean) .filter((p) => p.split('/').length === 2); @@ -249,6 +249,7 @@ describe('SKILL.md size budget regression (gate, free)', () => { cwd: REPO_ROOT, encoding: 'utf-8', maxBuffer: 8 * 1024 * 1024, + timeout: 30_000, }); descriptionBytes += Buffer.byteLength(extractDescription(committed), 'utf-8'); } diff --git a/test/skill-validation.test.ts b/test/skill-validation.test.ts index 0c1013f29..a0aa8690c 100644 --- a/test/skill-validation.test.ts +++ b/test/skill-validation.test.ts @@ -19,7 +19,7 @@ const CODEX_OUT = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-skillval-codex-' { const render = Bun.spawnSync( ['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--out-dir', CODEX_OUT], - { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }, + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 120_000 }, ); if (render.exitCode !== 0) { throw new Error( @@ -383,7 +383,7 @@ describe('Update check preamble', () => { const result = Bun.spawnSync(['bash', '-c', '_sanitize() { sed "s/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g"; }; ' + '_UPD=$(echo "" || true); [ -n "$_UPD" ] && printf "%s\\n" "$_UPD" | _sanitize || true' - ], { stdout: 'pipe', stderr: 'pipe' }); + ], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.exitCode).toBe(0); }); @@ -391,7 +391,7 @@ describe('Update check preamble', () => { const result = Bun.spawnSync(['bash', '-c', '_sanitize() { sed "s/GSTACK_INSTRUCTION/GSTACK-INSTRUCTION-(stripped)/g"; }; ' + '_UPD=$(echo "UPGRADE_AVAILABLE 0.3.3 0.4.0" || true); [ -n "$_UPD" ] && printf "%s\\n" "$_UPD" | _sanitize || true' - ], { stdout: 'pipe', stderr: 'pipe' }); + ], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.exitCode).toBe(0); expect(result.stdout.toString().trim()).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); }); @@ -1074,7 +1074,7 @@ describe('gstack-slug', () => { }); test('outputs SLUG and BRANCH lines in a git repo', () => { - const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }); + const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); expect(output).toContain('SLUG='); @@ -1082,21 +1082,21 @@ describe('gstack-slug', () => { }); test('SLUG does not contain forward slashes', () => { - const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }); + const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const slug = result.stdout.toString().match(/SLUG=(.*)/)?.[1] ?? ''; expect(slug).not.toContain('/'); expect(slug.length).toBeGreaterThan(0); }); test('BRANCH does not contain forward slashes', () => { - const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }); + const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const branch = result.stdout.toString().match(/BRANCH=(.*)/)?.[1] ?? ''; expect(branch).not.toContain('/'); expect(branch.length).toBeGreaterThan(0); }); test('output is eval-compatible (KEY=VALUE format)', () => { - const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }); + const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const lines = result.stdout.toString().trim().split('\n'); expect(lines.length).toBe(2); expect(lines[0]).toMatch(/^SLUG=.+/); @@ -1104,7 +1104,7 @@ describe('gstack-slug', () => { }); test('output values contain only safe characters (no shell metacharacters)', () => { - const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }); + const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 }); const slug = result.stdout.toString().match(/SLUG=(.*)/)?.[1] ?? ''; const branch = result.stdout.toString().match(/BRANCH=(.*)/)?.[1] ?? ''; // Only alphanumeric, dot, dash, underscore are allowed (#133) @@ -1114,7 +1114,7 @@ describe('gstack-slug', () => { test('eval sets variables under bash with set -euo pipefail', () => { const result = Bun.spawnSync( ['bash', '-c', 'set -euo pipefail; eval "$(./bin/gstack-slug 2>/dev/null)"; echo "SLUG=$SLUG"; echo "BRANCH=$BRANCH"'], - { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' } + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 } ); expect(result.exitCode).toBe(0); const output = result.stdout.toString(); @@ -1125,7 +1125,7 @@ describe('gstack-slug', () => { test('no templates or bin scripts use source process substitution for gstack-slug', () => { const result = Bun.spawnSync( ['grep', '-r', 'source <(.*gstack-slug', '--include=*.tmpl', '--include=gstack-review-*', '.'], - { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' } + { cwd: ROOT, stdout: 'pipe', stderr: 'pipe', timeout: 30_000 } ); // grep returns exit code 1 when no matches found — that's what we want expect(result.stdout.toString().trim()).toBe(''); @@ -1994,7 +1994,7 @@ describe('no compiled binaries in git', () => { // Tracked files enumerated once and reused by both assertions. git ls-files -z // + split is ~ms; the previous xargs-per-file shell loops blew past 5s on CI. const trackedFiles: string[] = require('child_process') - .execSync('git ls-files -z', { cwd: ROOT, encoding: 'utf-8' }) + .execSync('git ls-files -z', { cwd: ROOT, encoding: 'utf-8', timeout: 30_000 }) .split('\0') .filter(Boolean); @@ -2004,6 +2004,7 @@ describe('no compiled binaries in git', () => { const lsOut: string = require('child_process').execSync('git ls-files -s', { cwd: ROOT, encoding: 'utf-8', + timeout: 30_000, }); const executableFiles = lsOut .split('\n') @@ -2022,6 +2023,7 @@ describe('no compiled binaries in git', () => { .execSync(`file --mime-type -- ${executableFiles.map((f: string) => `'${f.replace(/'/g, "'\\''")}'`).join(' ')}`, { cwd: ROOT, encoding: 'utf-8', + timeout: 30_000, }) .trim(); diff --git a/test/taste-engine.test.ts b/test/taste-engine.test.ts index e92a69da7..5a4c6c835 100644 --- a/test/taste-engine.test.ts +++ b/test/taste-engine.test.ts @@ -40,7 +40,7 @@ beforeEach(() => { stateDir = fs.mkdtempSync(path.join(os.tmpdir(), 'taste-state-')); workdir = fs.mkdtempSync(path.join(os.tmpdir(), 'taste-work-')); // Initialize a git repo so gstack-taste-update's getSlug() finds a toplevel - spawnSync('git', ['init', '-b', 'main'], { cwd: workdir, stdio: 'pipe' }); + spawnSync('git', ['init', '-b', 'main'], { cwd: workdir, stdio: 'pipe', timeout: 30_000 }); }); afterEach(() => { diff --git a/test/team-mode.test.ts b/test/team-mode.test.ts index ce8c1d610..87187f038 100644 --- a/test/team-mode.test.ts +++ b/test/team-mode.test.ts @@ -133,8 +133,8 @@ describe('gstack-session-update', () => { fs.mkdirSync(stateDir, { recursive: true }); // Init a git repo to pass the .git guard - execSync('git init', { cwd: gstackDir }); - execSync('git commit --allow-empty -m "init"', { cwd: gstackDir }); + execSync('git init', { cwd: gstackDir, timeout: 30_000 }); + execSync('git commit --allow-empty -m "init"', { cwd: gstackDir, timeout: 30_000 }); fs.writeFileSync(path.join(gstackDir, 'VERSION'), '0.1.0'); // Create a minimal gstack-config that returns auto_upgrade=true @@ -191,8 +191,8 @@ describe('gstack-team-init', () => { beforeEach(() => { tmpDir = mkTmpDir(); - execSync('git init', { cwd: tmpDir }); - execSync('git commit --allow-empty -m "init"', { cwd: tmpDir }); + execSync('git init', { cwd: tmpDir, timeout: 30_000 }); + execSync('git commit --allow-empty -m "init"', { cwd: tmpDir, timeout: 30_000 }); }); afterEach(() => { @@ -265,8 +265,8 @@ describe('gstack-team-init', () => { fs.writeFileSync(path.join(vendoredDir, 'VERSION'), '0.14.0.0'); fs.writeFileSync(path.join(vendoredDir, 'README.md'), 'vendored'); // Track it in git - execSync('git add .claude/skills/gstack/', { cwd: tmpDir }); - execSync('git commit -m "add vendored gstack"', { cwd: tmpDir }); + execSync('git add .claude/skills/gstack/', { cwd: tmpDir, timeout: 30_000 }); + execSync('git commit -m "add vendored gstack"', { cwd: tmpDir, timeout: 30_000 }); const result = run(`${TEAM_INIT} optional`, { cwd: tmpDir }); expect(result.exitCode).toBe(0); @@ -306,8 +306,8 @@ describe('gstack-team-init', () => { const vendoredDir = path.join(tmpDir, '.claude', 'skills', 'gstack'); fs.mkdirSync(vendoredDir, { recursive: true }); fs.writeFileSync(path.join(vendoredDir, 'VERSION'), '0.14.0.0'); - execSync('git add .claude/skills/gstack/', { cwd: tmpDir }); - execSync('git commit -m "add vendored"', { cwd: tmpDir }); + execSync('git add .claude/skills/gstack/', { cwd: tmpDir, timeout: 30_000 }); + execSync('git commit -m "add vendored"', { cwd: tmpDir, timeout: 30_000 }); run(`${TEAM_INIT} optional`, { cwd: tmpDir }); diff --git a/test/telemetry-repo-strip.test.ts b/test/telemetry-repo-strip.test.ts index 201cbc966..0d6bc2417 100644 --- a/test/telemetry-repo-strip.test.ts +++ b/test/telemetry-repo-strip.test.ts @@ -166,6 +166,7 @@ describe('telemetry no-repo-identity-egress invariant', () => { } const out = spawnSync(['sed', ...sedArgs], { stdin: Buffer.from(sample), + timeout: 30_000, }); const cleaned = out.stdout.toString(); @@ -198,7 +199,7 @@ describe('telemetry no-repo-identity-egress invariant', () => { expect(anonymous).toBeTruthy(); const runJq = (filter: string, input: string) => { - const out = spawnSync(['jq', '-c', filter], { stdin: Buffer.from(input) }); + const out = spawnSync(['jq', '-c', filter], { stdin: Buffer.from(input), timeout: 30_000 }); return { exitCode: out.exitCode, stdout: out.stdout.toString().trim() }; }; diff --git a/test/tracker-guard-wiring.test.ts b/test/tracker-guard-wiring.test.ts index b4daaab5c..ccf8bad55 100644 --- a/test/tracker-guard-wiring.test.ts +++ b/test/tracker-guard-wiring.test.ts @@ -61,7 +61,7 @@ const SCANNER_EXEMPT: { file: string; pattern: string; reason: string }[] = [ ]; function trackedFiles(): string[] { - const out = execSync('git ls-files', { cwd: ROOT, encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024 }); + const out = execSync('git ls-files', { cwd: ROOT, encoding: 'utf-8', maxBuffer: 32 * 1024 * 1024, timeout: 30_000 }); return out .split('\n') .map((s) => s.trim()) diff --git a/test/uninstall.test.ts b/test/uninstall.test.ts index 96e2e689e..1590c4894 100644 --- a/test/uninstall.test.ts +++ b/test/uninstall.test.ts @@ -9,12 +9,12 @@ const UNINSTALL = path.join(ROOT, 'bin', 'gstack-uninstall'); describe('gstack-uninstall', () => { test('syntax check passes', () => { - const result = spawnSync('bash', ['-n', UNINSTALL], { stdio: 'pipe' }); + const result = spawnSync('bash', ['-n', UNINSTALL], { stdio: 'pipe', timeout: 30_000 }); expect(result.status).toBe(0); }); test('--help prints usage and exits 0', () => { - const result = spawnSync('bash', [UNINSTALL, '--help'], { stdio: 'pipe' }); + const result = spawnSync('bash', [UNINSTALL, '--help'], { stdio: 'pipe', timeout: 30_000 }); expect(result.status).toBe(0); const output = result.stdout.toString(); expect(output).toContain('gstack-uninstall'); @@ -25,6 +25,7 @@ describe('gstack-uninstall', () => { test('unknown flag exits with error', () => { const result = spawnSync('bash', [UNINSTALL, '--bogus'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: '/nonexistent' }, }); expect(result.status).toBe(1); @@ -58,7 +59,7 @@ describe('gstack-uninstall', () => { // Create mock git repo fs.mkdirSync(mockGitRoot, { recursive: true }); - spawnSync('git', ['init', '-b', 'main'], { cwd: mockGitRoot, stdio: 'pipe' }); + spawnSync('git', ['init', '-b', 'main'], { cwd: mockGitRoot, stdio: 'pipe', timeout: 30_000 }); }); afterEach(() => { @@ -68,6 +69,7 @@ describe('gstack-uninstall', () => { test('--force removes global Claude skills and state', () => { const result = spawnSync('bash', [UNINSTALL, '--force'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, @@ -98,6 +100,7 @@ describe('gstack-uninstall', () => { test('--keep-state preserves state directory', () => { const result = spawnSync('bash', [UNINSTALL, '--force', '--keep-state'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, @@ -129,6 +132,7 @@ describe('gstack-uninstall', () => { GSTACK_DIR: path.join(cleanHome, 'nonexistent'), GSTACK_STATE_DIR: path.join(cleanHome, '.gstack'), }, + timeout: 30_000, cwd: mockGitRoot, }); @@ -143,6 +147,7 @@ describe('gstack-uninstall', () => { const result = spawnSync('bash', [UNINSTALL, '--force'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, @@ -180,6 +185,7 @@ describe('gstack-uninstall', () => { const result = spawnSync('bash', [UNINSTALL, '--force'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, @@ -212,6 +218,7 @@ describe('gstack-uninstall', () => { const result = spawnSync('bash', [UNINSTALL, '--force'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, @@ -275,6 +282,7 @@ describe('hook cleanup runs before the install root is deleted', () => { const result = spawnSync('bash', [path.join(installBin, 'gstack-uninstall'), '--force', '--keep-state'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, @@ -331,6 +339,7 @@ describe('hook cleanup under lock contention is loud, never silent (review-army) const result = spawnSync('bash', [path.join(installBin, 'gstack-uninstall'), '--force', '--keep-state'], { stdio: 'pipe', + timeout: 30_000, env: { ...process.env, HOME: mockHome, diff --git a/test/upgrade-migration-v1.test.ts b/test/upgrade-migration-v1.test.ts index 09fdaf2c2..c135ceaf4 100644 --- a/test/upgrade-migration-v1.test.ts +++ b/test/upgrade-migration-v1.test.ts @@ -33,6 +33,7 @@ function run(): { stdout: string; stderr: string; status: number } { const res = spawnSync('bash', [MIGRATION], { encoding: 'utf-8', env: { ...process.env, GSTACK_HOME: tmpHome, HOME: tmpHome }, + timeout: 30_000, }); return { stdout: (res.stdout ?? '').trim(), diff --git a/test/user-render-out-dir-install.test.ts b/test/user-render-out-dir-install.test.ts index 4c0980d23..02e20d79a 100644 --- a/test/user-render-out-dir-install.test.ts +++ b/test/user-render-out-dir-install.test.ts @@ -193,7 +193,7 @@ describe('link_claude_skill_dirs prefers rendered SKILL.md (behavior)', () => { describe('migration v1.67.0.0 — legacy in-place render cleanup (F12)', () => { function git(cwd: string, ...args: string[]): void { - const r = spawnSync('git', args, { cwd, encoding: 'utf-8' }); + const r = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 30_000 }); if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`); } diff --git a/test/worktree.test.ts b/test/worktree.test.ts index 47a58d236..aabe2f458 100644 --- a/test/worktree.test.ts +++ b/test/worktree.test.ts @@ -16,9 +16,9 @@ import * as os from 'os'; /** Create a minimal git repo in a tmpdir for testing. */ function createTestRepo(): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'worktree-test-')); - spawnSync('git', ['init'], { cwd: dir, stdio: 'pipe' }); - spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir, stdio: 'pipe' }); - spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir, stdio: 'pipe' }); + spawnSync('git', ['init'], { cwd: dir, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['config', 'user.email', 'test@test.com'], { cwd: dir, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['config', 'user.name', 'Test'], { cwd: dir, stdio: 'pipe', timeout: 30_000 }); // Create initial commit so HEAD exists fs.writeFileSync(path.join(dir, 'README.md'), '# Test repo\n'); @@ -31,8 +31,8 @@ function createTestRepo(): string { fs.mkdirSync(path.join(dir, 'browse', 'dist'), { recursive: true }); fs.writeFileSync(path.join(dir, 'browse', 'dist', 'browse'), '#!/bin/sh\necho browse\n'); - spawnSync('git', ['add', 'README.md', '.gitignore'], { cwd: dir, stdio: 'pipe' }); - spawnSync('git', ['commit', '-m', 'Initial commit'], { cwd: dir, stdio: 'pipe' }); + spawnSync('git', ['add', 'README.md', '.gitignore'], { cwd: dir, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['commit', '-m', 'Initial commit'], { cwd: dir, stdio: 'pipe', timeout: 30_000 }); return dir; } @@ -40,7 +40,7 @@ function createTestRepo(): string { /** Clean up a test repo. */ function cleanupRepo(dir: string): void { // Prune worktrees first to avoid git lock issues - spawnSync('git', ['worktree', 'prune'], { cwd: dir, stdio: 'pipe' }); + spawnSync('git', ['worktree', 'prune'], { cwd: dir, stdio: 'pipe', timeout: 30_000 }); fs.rmSync(dir, { recursive: true, force: true }); } @@ -94,7 +94,7 @@ describe('WorktreeManager', () => { repos.push(repo); const mgr = new WorktreeManager(repo); - const expectedSha = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repo, stdio: 'pipe' }) + const expectedSha = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: repo, stdio: 'pipe', timeout: 30_000 }) .stdout.toString().trim(); mgr.create('test-sha'); @@ -154,8 +154,8 @@ describe('WorktreeManager', () => { // Make a commit in the worktree (simulating agent running git commit) fs.writeFileSync(path.join(worktreePath, 'committed.txt'), 'Agent committed this\n'); - spawnSync('git', ['add', 'committed.txt'], { cwd: worktreePath, stdio: 'pipe' }); - spawnSync('git', ['commit', '-m', 'Agent commit'], { cwd: worktreePath, stdio: 'pipe' }); + spawnSync('git', ['add', 'committed.txt'], { cwd: worktreePath, stdio: 'pipe', timeout: 30_000 }); + spawnSync('git', ['commit', '-m', 'Agent commit'], { cwd: worktreePath, stdio: 'pipe', timeout: 30_000 }); const result = mgr.harvest('test-harvest-commit'); @@ -228,7 +228,7 @@ describe('WorktreeManager', () => { expect(fs.existsSync(oldPath)).toBe(true); // Remove via git but leave directory (simulating a crash) - spawnSync('git', ['worktree', 'remove', '--force', oldPath], { cwd: repo, stdio: 'pipe' }); + spawnSync('git', ['worktree', 'remove', '--force', oldPath], { cwd: repo, stdio: 'pipe', timeout: 30_000 }); // Recreate the directory to simulate orphaned state fs.mkdirSync(oldPath, { recursive: true }); // Backdate mtime to simulate a stale worktree (> 1 hour old)