fix(tui): report one terminal outcome for local shell failures (#118452)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-08-02 21:38:30 -07:00
committed by GitHub
parent 763c97b2de
commit c1cbcf047b
2 changed files with 29 additions and 5 deletions
+24
View File
@@ -1,5 +1,7 @@
// Verifies local shell process handling for TUI local mode.
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import { join } from "node:path";
import type { OverlayHandle } from "@earendil-works/pi-tui";
import { describe, expect, it, vi } from "vitest";
import { createLocalShellRunner } from "./tui-local-shell.js";
@@ -242,6 +244,28 @@ describe("createLocalShellRunner", () => {
);
});
it("finishes a failed child before reporting the next local command", async () => {
const harness = createShellHarness({
spawnCommand: spawn,
getCwd: vi
.fn(() => process.cwd())
.mockReturnValueOnce(join(process.cwd(), ".missing-openclaw-local-shell-directory")),
});
const failedRun = harness.runLocalShellLine("!echo first");
harness.getLastSelector()?.onSelect?.({ value: "yes", label: "Yes" });
await failedRun;
await harness.runLocalShellLine("!echo second");
expect(harness.messages.filter((message) => message.startsWith("[local]"))).toEqual([
"[local] $ echo first",
expect.stringContaining("[local] error: "),
"[local] $ echo second",
"[local] second",
"[local] exit 0",
]);
});
it("does not crash when stdout or stderr emit an error event", async () => {
const stdout = new EventEmitter();
const stderr = new EventEmitter();
+5 -5
View File
@@ -131,9 +131,10 @@ export function createLocalShellRunner(deps: LocalShellDeps) {
let stdout = "";
let stderr = "";
let error: Error | undefined;
const stdoutDecoder = new StringDecoder("utf8");
const stderrDecoder = new StringDecoder("utf8");
// Output pipes may fail independently; child close/error remains authoritative.
// Pipe errors are incidental; close owns completion after any recorded spawn error.
const ignoreOutputStreamError = () => {};
child.stdout.on("error", ignoreOutputStreamError);
child.stderr.on("error", ignoreOutputStreamError);
@@ -160,15 +161,14 @@ export function createLocalShellRunner(deps: LocalShellDeps) {
deps.chatLog.addSystem(`[local] ${lineLocal}`);
}
}
deps.chatLog.addSystem(`[local] exit ${code ?? "?"}${signal ? ` (signal ${signal})` : ""}`);
const status = error ? `error: ${formatTuiErrorMessage(error)}` : `exit ${code ?? "?"}`;
deps.chatLog.addSystem(`[local] ${status}${signal ? ` (signal ${signal})` : ""}`);
deps.tui.requestRender();
resolve();
});
child.on("error", (err) => {
deps.chatLog.addSystem(`[local] error: ${formatTuiErrorMessage(err)}`);
deps.tui.requestRender();
resolve();
error = err;
});
});
};