mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
c791e2b128
* fix(cli): restore terminal state before exit in logs and hooks commands * fix(cli): route logs/hooks error exit through canonical defaultRuntime.exit * fix(cli): route terminal reset to stderr in JSON mode to keep stdout parseable * fix(cli): centralize stream-aware terminal reset exit Add optional resetStream parameter to RuntimeEnv.exit so JSON-mode callers can route the terminal reset to stderr through the shared defaultRuntime.exit path, keeping structured stdout parseable. - Extend RuntimeEnv.exit signature with optional resetStream option - Route JSON-mode logs fatal exit through unified defaultRuntime.exit instead of manually pairing restoreTerminalState with process.exit - Update test mock to exercise real terminal restore during exit * refactor(cli): tighten terminal exit contract Co-authored-by: Peter Lee <li.xialong@xydigit.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
127 lines
4.1 KiB
TypeScript
127 lines
4.1 KiB
TypeScript
// Terminal Core tests cover restore behavior.
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const clearActiveProgressLine = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("./progress-line.js", () => ({
|
|
clearActiveProgressLine,
|
|
}));
|
|
|
|
import { restoreTerminalState } from "./restore.js";
|
|
|
|
function configureTerminalIO(params: {
|
|
stdinIsTTY: boolean;
|
|
stdoutIsTTY: boolean;
|
|
setRawMode?: (mode: boolean) => void;
|
|
resume?: () => void;
|
|
isPaused?: () => boolean;
|
|
}) {
|
|
Object.defineProperty(process.stdin, "isTTY", { value: params.stdinIsTTY, configurable: true });
|
|
Object.defineProperty(process.stdout, "isTTY", { value: params.stdoutIsTTY, configurable: true });
|
|
(process.stdin as { setRawMode?: (mode: boolean) => void }).setRawMode = params.setRawMode;
|
|
(process.stdin as { resume?: () => void }).resume = params.resume;
|
|
(process.stdin as { isPaused?: () => boolean }).isPaused = params.isPaused;
|
|
}
|
|
|
|
function setupPausedTTYStdin() {
|
|
const setRawMode = vi.fn();
|
|
const resume = vi.fn();
|
|
const isPaused = vi.fn(() => true);
|
|
configureTerminalIO({
|
|
stdinIsTTY: true,
|
|
stdoutIsTTY: false,
|
|
setRawMode,
|
|
resume,
|
|
isPaused,
|
|
});
|
|
return { setRawMode, resume };
|
|
}
|
|
|
|
describe("restoreTerminalState", () => {
|
|
const originalStdinIsTTY = process.stdin.isTTY;
|
|
const originalStdoutIsTTY = process.stdout.isTTY;
|
|
const originalSetRawMode = (process.stdin as { setRawMode?: (mode: boolean) => void }).setRawMode;
|
|
const originalResume = (process.stdin as { resume?: () => void }).resume;
|
|
const originalIsPaused = (process.stdin as { isPaused?: () => boolean }).isPaused;
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
Object.defineProperty(process.stdin, "isTTY", {
|
|
value: originalStdinIsTTY,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(process.stdout, "isTTY", {
|
|
value: originalStdoutIsTTY,
|
|
configurable: true,
|
|
});
|
|
(process.stdin as { setRawMode?: (mode: boolean) => void }).setRawMode = originalSetRawMode;
|
|
(process.stdin as { resume?: () => void }).resume = originalResume;
|
|
(process.stdin as { isPaused?: () => boolean }).isPaused = originalIsPaused;
|
|
});
|
|
|
|
it("does not resume paused stdin by default", () => {
|
|
const { setRawMode, resume } = setupPausedTTYStdin();
|
|
|
|
restoreTerminalState("test");
|
|
|
|
expect(setRawMode).toHaveBeenCalledWith(false);
|
|
expect(resume).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resumes paused stdin when resumeStdin is true", () => {
|
|
const { setRawMode, resume } = setupPausedTTYStdin();
|
|
|
|
restoreTerminalState("test", { resumeStdinIfPaused: true });
|
|
|
|
expect(setRawMode).toHaveBeenCalledWith(false);
|
|
expect(resume).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("does not touch stdin when stdin is not a TTY", () => {
|
|
const setRawMode = vi.fn();
|
|
const resume = vi.fn();
|
|
const isPaused = vi.fn(() => true);
|
|
|
|
configureTerminalIO({
|
|
stdinIsTTY: false,
|
|
stdoutIsTTY: false,
|
|
setRawMode,
|
|
resume,
|
|
isPaused,
|
|
});
|
|
|
|
restoreTerminalState("test", { resumeStdinIfPaused: true });
|
|
|
|
expect(setRawMode).not.toHaveBeenCalled();
|
|
expect(resume).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("writes kitty and modifyOtherKeys reset sequences to stdout", () => {
|
|
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
|
|
configureTerminalIO({
|
|
stdinIsTTY: false,
|
|
stdoutIsTTY: true,
|
|
});
|
|
|
|
restoreTerminalState("test");
|
|
|
|
expect(writeSpy).toHaveBeenCalled();
|
|
const output = writeSpy.mock.calls.map(([chunk]) => String(chunk)).join("");
|
|
expect(output).toContain("\x1b[<u");
|
|
expect(output).toContain("\x1b[>4;0m");
|
|
});
|
|
|
|
it("writes reset sequences only to a custom TTY stream", () => {
|
|
const stdoutWrite = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
const resetWrite = vi.fn(() => true);
|
|
const resetStream = { isTTY: true, write: resetWrite } as unknown as NodeJS.WriteStream;
|
|
|
|
configureTerminalIO({ stdinIsTTY: false, stdoutIsTTY: true });
|
|
restoreTerminalState("test", { resetStream });
|
|
|
|
expect(resetWrite).toHaveBeenCalledWith(expect.stringContaining("\x1b[?25h"));
|
|
expect(stdoutWrite).not.toHaveBeenCalled();
|
|
});
|
|
});
|