mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
fix(scripts): make the tsgo watchdog opt-in and stop the harness leaking
ClawSweeper review on 6ba02c9d0a raised two findings.
[P1] The 45-minute default applied an unproven deadline to every tsgo
invocation. No supported duration contract covers every host and project, and
CI already bounds its own tsgo jobs at 15-20 minutes, so the default could only
ever fire outside CI where it was least validated. Drop it: an unset
OPENCLAW_TSGO_TIMEOUT_MS keeps the pre-existing unbounded wait, so no existing
run changes behavior, and operators opt in per host. Documented in
docs/help/testing.md beside the sibling Vitest watchdog.
[P2] The regression harness could leak its wedged child. The fake compiler
ignores SIGTERM by design, so a pre-fix or otherwise failing run left the tree
running after spawnSync gave up. Bound the fixture's loop as a backstop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user