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)