diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f37a26a4ed..dec778fbc153 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai - Release/CI/E2E: abort stalled Kitchen Sink RPC readiness probes as soon as the gateway exits so proof failures return promptly. - Release/CI/E2E: keep Parallels JSON-mode progress on stderr so macOS, Linux, Windows, and aggregate update smoke summaries stay parseable on stdout. - Release/CI/E2E: fail Crabbox sparse-sync runs clearly when their temporary full checkout disappears while the child process is running, instead of pretending the child's deleted cwd can be repaired. +- Release/CI/E2E: fail PTY-backed E2E commands when transcript logs cannot be written instead of letting missing proof capture crash around a live child process. ## 2026.6.1 diff --git a/scripts/e2e/lib/run-with-pty.mjs b/scripts/e2e/lib/run-with-pty.mjs index 003b57d1d532..14a668f65d84 100644 --- a/scripts/e2e/lib/run-with-pty.mjs +++ b/scripts/e2e/lib/run-with-pty.mjs @@ -13,6 +13,16 @@ if (!logPath || !command) { process.exit(2); } +let exiting = false; +let forwardedSignal = null; +let forceKillTimer = null; +let logFailed = false; +const outputLimitMarker = `\n[run-with-pty output truncated after ${OUTPUT_MAX_BYTES} bytes]\n`; +const outputState = { + bytes: 0, + truncated: false, +}; + const log = fs.createWriteStream(logPath, { flags: "w" }); const pty = spawn(command, args, { name: process.env.TERM || "xterm-256color", @@ -22,14 +32,23 @@ const pty = spawn(command, args, { env: process.env, }); -let exiting = false; -let forwardedSignal = null; -let forceKillTimer = null; -const outputLimitMarker = `\n[run-with-pty output truncated after ${OUTPUT_MAX_BYTES} bytes]\n`; -const outputState = { - bytes: 0, - truncated: false, -}; +log.on("error", (error) => { + if (logFailed) { + return; + } + logFailed = true; + console.error(`run-with-pty transcript log failed: ${error.message}`); + if (exiting) { + process.exit(1); + } + if (!exiting) { + pty.kill("SIGTERM"); + forceKillTimer ??= setTimeout(() => { + pty.kill("SIGKILL"); + }, FORCE_KILL_MS); + forceKillTimer.unref?.(); + } +}); function writeCappedOutput(data) { if (outputState.truncated) { @@ -39,18 +58,24 @@ function writeCappedOutput(data) { const remainingBytes = OUTPUT_MAX_BYTES - outputState.bytes; if (buffer.byteLength <= remainingBytes) { outputState.bytes += buffer.byteLength; - log.write(buffer); + if (!logFailed) { + log.write(buffer); + } process.stdout.write(buffer); return; } if (remainingBytes > 0) { const head = buffer.subarray(0, remainingBytes); - log.write(head); + if (!logFailed) { + log.write(head); + } process.stdout.write(head); } outputState.bytes = OUTPUT_MAX_BYTES; outputState.truncated = true; - log.write(outputLimitMarker); + if (!logFailed) { + log.write(outputLimitMarker); + } process.stdout.write(outputLimitMarker); } @@ -61,6 +86,9 @@ pty.onData((data) => { pty.onExit(({ exitCode, signal }) => { exiting = true; clearTimeout(forceKillTimer); + if (logFailed) { + process.exit(1); + } log.end(() => { if (forwardedSignal) { process.exit(signalExitCode(forwardedSignal)); diff --git a/test/scripts/e2e-run-with-pty.test.ts b/test/scripts/e2e-run-with-pty.test.ts index 112229cebc3a..2c5e89bc0123 100644 --- a/test/scripts/e2e-run-with-pty.test.ts +++ b/test/scripts/e2e-run-with-pty.test.ts @@ -105,6 +105,23 @@ describe("run-with-pty", () => { } }); + it("fails when the transcript log cannot be written", async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), "openclaw-run-with-pty-")); + try { + const result = await runPtyProbe( + tempRoot, + {}, + [process.execPath, "-e", "console.log('ready')"], + "", + ); + + expect(result.code).toBe(1); + expect(result.stderr).toContain("run-with-pty transcript log failed:"); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } + }); + posixIt("escalates forwarded termination signals for PTY commands that ignore them", async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), "openclaw-run-with-pty-")); const logPath = path.join(tempRoot, "pty.log");