diff --git a/scripts/check-extension-package-tsc-boundary.mjs b/scripts/check-extension-package-tsc-boundary.mjs index 910ec8ea0b33..fd10fa55a698 100644 --- a/scripts/check-extension-package-tsc-boundary.mjs +++ b/scripts/check-extension-package-tsc-boundary.mjs @@ -37,6 +37,7 @@ const FAILURE_OUTPUT_TAIL_LINES = 40; const STEP_OUTPUT_MAX_CHARS = 256 * 1024; const STEP_PROCESS_GROUP_EXIT_POLL_MS = 25; const STEP_POST_FORCE_KILL_WAIT_MS = 1_000; +const MAX_TIMER_TIMEOUT_MS = 2_147_000_000; const SLOW_COMPILE_SUMMARY_LIMIT = 10; const COMPILE_INPUT_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".json"]); const ROOTDIR_BOUNDARY_CANARY_IMPORT_PATH = @@ -337,13 +338,22 @@ function writeStampFile(filePath) { writeFileSync(filePath, `${new Date().toISOString()}\n`, "utf8"); } +function resolveStepTimerTimeoutMs(valueMs) { + const value = Number(valueMs); + if (!Number.isFinite(value)) { + return MAX_TIMER_TIMEOUT_MS; + } + return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS); +} + function runNodeStep(label, args, timeoutMs) { + const resolvedTimeoutMs = resolveStepTimerTimeoutMs(timeoutMs); const startedAt = Date.now(); const result = spawnSync(process.execPath, args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 16 * 1024 * 1024, - timeout: timeoutMs, + timeout: resolvedTimeoutMs, }); if (result.status === 0 && !result.error) { @@ -352,7 +362,7 @@ function runNodeStep(label, args, timeoutMs) { const timeoutSuffix = result.error?.name === "Error" && result.error.message.includes("ETIMEDOUT") - ? `${label} timed out after ${timeoutMs}ms` + ? `${label} timed out after ${resolvedTimeoutMs}ms` : ""; const errorSuffix = result.error ? result.error.message : ""; const note = [timeoutSuffix, errorSuffix].filter(Boolean).join("\n"); @@ -391,6 +401,7 @@ function abortSiblingSteps(abortController) { * Runs one node-based boundary check step with timeout and output capture. */ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { + const resolvedTimeoutMs = resolveStepTimerTimeoutMs(timeoutMs); const abortController = params.abortController; const killProcess = params.killProcess ?? process.kill.bind(process); const onFailure = params.onFailure; @@ -499,7 +510,7 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { stderr: stderrText, kind: "timeout", elapsedMs: Date.now() - startedAt, - note: `${label} timed out after ${timeoutMs}ms`, + note: `${label} timed out after ${resolvedTimeoutMs}ms`, }), ), label, @@ -508,14 +519,14 @@ export function runNodeStepAsync(label, args, timeoutMs, params = {}) { stderr: stderrText, kind: "timeout", elapsedMs: Date.now() - startedAt, - note: `${label} timed out after ${timeoutMs}ms`, + note: `${label} timed out after ${resolvedTimeoutMs}ms`, }, ); onFailure?.(error); abortSiblingSteps(abortController); rejectPromise(toLintErrorObject(error, "Step timed out")); })(); - }, timeoutMs); + }, resolvedTimeoutMs); child.stdout.setEncoding("utf8"); child.stderr.setEncoding("utf8"); diff --git a/test/scripts/check-extension-package-tsc-boundary.test.ts b/test/scripts/check-extension-package-tsc-boundary.test.ts index 54d53ee6b18a..04df853da310 100644 --- a/test/scripts/check-extension-package-tsc-boundary.test.ts +++ b/test/scripts/check-extension-package-tsc-boundary.test.ts @@ -5,6 +5,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; +import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; import { afterEach, describe, expect, it } from "vitest"; import { acquireBoundaryCheckLock, @@ -432,6 +433,19 @@ describe("check-extension-package-tsc-boundary", () => { expect(elapsedMs).toBeGreaterThanOrEqual(0); }, 30_000); + it("clamps oversized async node step timers before scheduling", async () => { + await expect( + runNodeStepAsync( + "slow-success", + ["--eval", "setTimeout(() => process.exit(0), 25);"], + MAX_TIMER_TIMEOUT_MS + 1, + ), + ).resolves.toMatchObject({ + stderr: "", + stdout: "", + }); + }); + it("keeps async node step failure output bounded", async () => { const child = new EventEmitter() as EventEmitter & { kill: (signal?: NodeJS.Signals | number) => boolean;