diff --git a/docs/help/testing.md b/docs/help/testing.md index 2773d94e6c01..55a7989088fc 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -694,6 +694,11 @@ Native dependency policy: after 5 minutes with no stdout or stderr output. Set `OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=0` to disable the watchdog for an intentionally silent investigation. + - `scripts/run-tsgo.mjs` waits indefinitely by default. Set + `OPENCLAW_TSGO_TIMEOUT_MS` to bound a run on hosts where a wedged + compiler would otherwise block its caller forever; on expiry the whole + tsgo process tree is killed and the run fails. Values above Node's + timer ceiling saturate instead of collapsing to a 1ms deadline. diff --git a/scripts/run-tsgo.mts b/scripts/run-tsgo.mts index 3ea27918e097..f0a090cf37d1 100644 --- a/scripts/run-tsgo.mts +++ b/scripts/run-tsgo.mts @@ -16,8 +16,6 @@ import { shouldSkipSparseTsgoGuardError, } from "./lib/tsgo-sparse-guard.mts"; -/** Watchdog bound for one tsgo run; sized well above a healthy whole-program check. */ -const DEFAULT_TSGO_TIMEOUT_MS = 45 * 60 * 1000; /** Node's timer ceiling: a longer delay silently becomes 1ms, so a raised override must saturate. */ const MAX_TSGO_TIMEOUT_MS = 2_147_483_647; @@ -51,10 +49,14 @@ async function main(): Promise { } ensureRepoToolNodeModulesLink(tsgoPath); - const timeoutMs = Math.min( - readPositiveEnvInt("OPENCLAW_TSGO_TIMEOUT_MS", env, DEFAULT_TSGO_TIMEOUT_MS), - MAX_TSGO_TIMEOUT_MS, - ); + // Opt-in deadline: no supported duration contract covers every host and project, + // so an unset value keeps the pre-existing unbounded wait rather than guessing one. + const timeoutMs = env.OPENCLAW_TSGO_TIMEOUT_MS?.trim() + ? Math.min( + readPositiveEnvInt("OPENCLAW_TSGO_TIMEOUT_MS", env, MAX_TSGO_TIMEOUT_MS), + MAX_TSGO_TIMEOUT_MS, + ) + : undefined; try { // Managed run owns the whole tsgo process tree: on timeout it SIGKILLs the // process group, because a wedged checker ignores SIGTERM and would otherwise diff --git a/test/scripts/run-tsgo.test.ts b/test/scripts/run-tsgo.test.ts index 2e819fa294a4..b325a0367b80 100644 --- a/test/scripts/run-tsgo.test.ts +++ b/test/scripts/run-tsgo.test.ts @@ -242,17 +242,16 @@ describe.skipIf(process.platform === "win32")("run-tsgo watchdog", () => { fs.chmodSync(fakeTsgo, 0o755); } - function runFakeTsgo(cwd: string, timeoutMs: string) { + function runFakeTsgo(cwd: string, timeoutMs: string | undefined) { + const { OPENCLAW_TSGO_TIMEOUT_MS: _unset, ...baseEnv } = process.env; return spawnSync( process.execPath, [path.resolve("scripts/run-tsgo.mjs"), "-p", "tsconfig.extensions.json"], { cwd, encoding: "utf8", - env: { - ...process.env, - OPENCLAW_TSGO_TIMEOUT_MS: timeoutMs, - }, + env: + timeoutMs === undefined ? baseEnv : { ...baseEnv, OPENCLAW_TSGO_TIMEOUT_MS: timeoutMs }, // spawnSync blocks this thread, so vitest's own per-test budget can never // fire; a regression here would hang the worker instead of failing. timeout: 25_000, @@ -264,8 +263,12 @@ describe.skipIf(process.platform === "win32")("run-tsgo watchdog", () => { it("kills a wedged tsgo that ignores SIGTERM instead of blocking its caller forever", () => { const cwd = createTempDir("openclaw-run-tsgo-watchdog-"); // Mirrors the observed wedge: the checker refuses SIGTERM and never reports, - // so only a process-group SIGKILL frees the caller. - writeFakeTsgo(cwd, "#!/bin/sh\ntrap '' TERM\nwhile true; do sleep 1; done\n"); + // so only a process-group SIGKILL frees the caller. The bounded loop is the + // harness backstop: a pre-fix or failing run must not leak this tree. + writeFakeTsgo( + cwd, + "#!/bin/sh\ntrap '' TERM\ni=0\nwhile [ $i -lt 60 ]; do sleep 1; i=$((i+1)); done\n", + ); const result = runFakeTsgo(cwd, "2000"); @@ -274,6 +277,16 @@ describe.skipIf(process.platform === "win32")("run-tsgo watchdog", () => { expect(result.stderr.trim().split("\n").at(-1)).toBe("[tsgo] FAILED (exit 1)"); }, 30_000); + it("arms no watchdog until an operator opts in", () => { + const cwd = createTempDir("openclaw-run-tsgo-watchdog-"); + writeFakeTsgo(cwd, "#!/bin/sh\nsleep 2\nexit 0\n"); + + const result = runFakeTsgo(cwd, undefined); + + expect(result.status).toBe(0); + expect(result.stderr).not.toContain("killed the tsgo process tree"); + }, 30_000); + it("leaves a tsgo that finishes inside the watchdog bound alone", () => { const cwd = createTempDir("openclaw-run-tsgo-watchdog-"); writeFakeTsgo(cwd, "#!/bin/sh\nexit 0\n");