fix(e2e): fail pty transcript log errors

This commit is contained in:
Vincent Koc
2026-06-04 06:16:03 +02:00
parent b90fb1ef71
commit 2baa9d550e
3 changed files with 57 additions and 11 deletions

View File

@@ -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

View File

@@ -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));

View File

@@ -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");