Files
openclaw/src/runtime.test.ts
T
2026-08-01 10:03:33 -07:00

229 lines
8.0 KiB
TypeScript

// Tests for terminal runtime helpers.
import { afterEach, describe, expect, it, vi } from "vitest";
// Mock dependencies
vi.mock("../packages/terminal-core/src/progress-line.js", () => ({
clearActiveProgressLine: vi.fn(),
}));
vi.mock("../packages/terminal-core/src/restore.js", () => ({
restoreTerminalState: vi.fn(),
}));
import { restoreTerminalState } from "../packages/terminal-core/src/restore.js";
import { loggingState } from "./logging/state.js";
import {
createNonExitingRuntime,
defaultRuntime,
ExitError,
writeRuntimeJson,
writeRuntimeStdout,
} from "./runtime.js";
describe("createNonExitingRuntime", () => {
it("returns runtime with exit function", () => {
const runtime = createNonExitingRuntime();
expect(typeof runtime.exit).toBe("function");
});
it("exit function throws ExitError", () => {
const runtime = createNonExitingRuntime();
expect(() => runtime.exit(1)).toThrow(ExitError);
});
it("ExitError includes exit code", () => {
const runtime = createNonExitingRuntime();
expect(() => runtime.exit(42)).toThrow("exit 42");
expect(() => runtime.exit(42)).toThrow(ExitError);
});
it("ExitError is distinguishable from generic Error", () => {
const runtime = createNonExitingRuntime();
try {
runtime.exit(1);
} catch (err) {
expect(err instanceof ExitError).toBe(true);
expect(err instanceof Error).toBe(true);
}
});
});
describe("writeRuntimeJson", () => {
it("writes JSON using writeJson when available", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
writeStdout: vi.fn(),
writeJson: vi.fn(),
};
writeRuntimeJson(runtime, { key: "value" });
expect(runtime.writeJson).toHaveBeenCalledWith({ key: "value" }, 2);
});
it("writes JSON using log when writeJson not available", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
writeRuntimeJson(runtime, { key: "value" });
expect(runtime.log).toHaveBeenCalled();
});
it("uses custom space parameter", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
writeStdout: vi.fn(),
writeJson: vi.fn(),
};
writeRuntimeJson(runtime, { key: "value" }, 4);
expect(runtime.writeJson).toHaveBeenCalledWith({ key: "value" }, 4);
});
it("handles zero space parameter", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
writeRuntimeJson(runtime, { key: "value" }, 0);
expect(runtime.log).toHaveBeenCalledWith('{"key":"value"}');
});
});
describe("writeRuntimeStdout", () => {
it("uses the direct stdout writer when available", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
writeStdout: vi.fn(),
writeJson: vi.fn(),
};
writeRuntimeStdout(runtime, "plain output");
expect(runtime.writeStdout).toHaveBeenCalledWith("plain output");
expect(runtime.log).not.toHaveBeenCalled();
});
it("falls back to the runtime logger when no stdout writer is available", () => {
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
writeRuntimeStdout(runtime, "plain output");
expect(runtime.log).toHaveBeenCalledWith("plain output");
});
});
describe("defaultRuntime terminal restoration", () => {
const originalForceConsoleToStderr = loggingState.forceConsoleToStderr;
const originalStdoutIsTTY = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
const originalStderrIsTTY = Object.getOwnPropertyDescriptor(process.stderr, "isTTY");
afterEach(() => {
vi.restoreAllMocks();
vi.mocked(restoreTerminalState).mockReset();
loggingState.forceConsoleToStderr = originalForceConsoleToStderr;
if (originalStdoutIsTTY) {
Object.defineProperty(process.stdout, "isTTY", originalStdoutIsTTY);
} else {
Reflect.deleteProperty(process.stdout, "isTTY");
}
if (originalStderrIsTTY) {
Object.defineProperty(process.stderr, "isTTY", originalStderrIsTTY);
} else {
Reflect.deleteProperty(process.stderr, "isTTY");
}
});
it.each([0, 1])("keeps machine-readable stdout clean on exit %i", async (exitCode) => {
const actualRestore = await vi.importActual<
typeof import("../packages/terminal-core/src/restore.js")
>("../packages/terminal-core/src/restore.js");
vi.mocked(restoreTerminalState).mockImplementation(actualRestore.restoreTerminalState);
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });
const stdout: string[] = [];
const stderr: string[] = [];
vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
stdout.push(String(chunk));
return true;
}) as typeof process.stdout.write);
vi.spyOn(process.stderr, "write").mockImplementation(((chunk: unknown) => {
stderr.push(String(chunk));
return true;
}) as typeof process.stderr.write);
vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
throw new ExitError(code ?? 0);
}) as typeof process.exit);
loggingState.forceConsoleToStderr = true;
defaultRuntime.writeJson({ ok: exitCode === 0 });
expect(() => defaultRuntime.exit(exitCode)).toThrow(ExitError);
expect(JSON.parse(stdout.join(""))).toEqual({ ok: exitCode === 0 });
expect(stderr.join("")).toContain("\x1b[?25h");
});
it("preserves stdout terminal restoration for human output", async () => {
const actualRestore = await vi.importActual<
typeof import("../packages/terminal-core/src/restore.js")
>("../packages/terminal-core/src/restore.js");
vi.mocked(restoreTerminalState).mockImplementation(actualRestore.restoreTerminalState);
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });
const stdout: string[] = [];
const stderr: string[] = [];
vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
stdout.push(String(chunk));
return true;
}) as typeof process.stdout.write);
vi.spyOn(process.stderr, "write").mockImplementation(((chunk: unknown) => {
stderr.push(String(chunk));
return true;
}) as typeof process.stderr.write);
vi.spyOn(process, "exit").mockImplementation((() => {
throw new ExitError(1);
}) as typeof process.exit);
loggingState.forceConsoleToStderr = false;
defaultRuntime.writeStdout("operator-visible output");
expect(() => defaultRuntime.exit(1)).toThrow(ExitError);
expect(stdout.join("")).toContain("operator-visible output\n");
expect(stdout.join("")).toContain("\x1b[?25h");
expect(stderr).toEqual([]);
});
it("honors an explicitly selected reset stream in machine-output mode", async () => {
const actualRestore = await vi.importActual<
typeof import("../packages/terminal-core/src/restore.js")
>("../packages/terminal-core/src/restore.js");
vi.mocked(restoreTerminalState).mockImplementation(actualRestore.restoreTerminalState);
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });
const stdout = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
const resetWrite = vi.fn(() => true);
const resetStream = { isTTY: true, write: resetWrite } as unknown as NodeJS.WriteStream;
vi.spyOn(process, "exit").mockImplementation((() => {
throw new ExitError(1);
}) as typeof process.exit);
loggingState.forceConsoleToStderr = true;
expect(() => defaultRuntime.exit(1, { resetStream })).toThrow(ExitError);
expect(resetWrite).toHaveBeenCalledWith(expect.stringContaining("\x1b[?25h"));
expect(stdout).not.toHaveBeenCalled();
expect(stderr).not.toHaveBeenCalled();
});
});