Files
openclaw/src/cli/banner.test.ts
Peter Steinberger abb85ccc86 fix(cli): validate timeout and banner TTY state
Fixes two CLI edge cases found by clawpatch.

- `emitCliBanner` now honors injected TTY state before writing to stdout.
- Nodes RPC timeout handling now rejects malformed `--timeout` values with the existing timeout parser instead of forwarding `NaN` into gateway transport calls.

Proof:
- `node scripts/run-vitest.mjs src/cli/banner.test.ts src/cli/nodes-cli/register.invoke.approval-transport-timeout.test.ts`
- `pnpm exec oxfmt --check --threads=1 src/cli/banner.ts src/cli/banner.test.ts src/cli/nodes-cli/rpc.runtime.ts src/cli/nodes-cli/register.invoke.approval-transport-timeout.test.ts`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- Real CLI proof: `pnpm openclaw nodes list --timeout nope --json` exits 1 with `Invalid --timeout`.
- Runtime banner proof: injected `isTty:false` with `stdout.isTTY=true` produced `writes=0`, `emitted=false`.
2026-05-26 21:08:11 +01:00

135 lines
3.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { formatCliBannerLine } from "./banner.js";
const readCliBannerTaglineModeMock = vi.hoisted(() => vi.fn());
const stdoutIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "isTTY");
vi.mock("./banner-config-lite.js", () => ({
parseTaglineMode: (value: unknown) =>
value === "random" || value === "default" || value === "off" ? value : undefined,
readCliBannerTaglineMode: readCliBannerTaglineModeMock,
}));
beforeEach(() => {
readCliBannerTaglineModeMock.mockReset();
readCliBannerTaglineModeMock.mockReturnValue(undefined);
});
afterEach(() => {
vi.restoreAllMocks();
if (stdoutIsTtyDescriptor) {
Object.defineProperty(process.stdout, "isTTY", stdoutIsTtyDescriptor);
} else {
delete (process.stdout as { isTTY?: boolean }).isTTY;
}
});
async function importFreshBannerModule() {
vi.resetModules();
return await import("./banner.js");
}
function setStdoutIsTty(value: boolean) {
Object.defineProperty(process.stdout, "isTTY", {
configurable: true,
value,
});
}
describe("formatCliBannerLine", () => {
it("hides tagline text when cli.banner.taglineMode is off", () => {
readCliBannerTaglineModeMock.mockReturnValue("off");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",
env: { LANG: "en_US.UTF-8" },
isTty: true,
platform: "darwin",
richTty: false,
});
expect(line).toBe("🦞 OpenClaw 2026.3.7 (abc1234)");
});
it("uses default tagline when cli.banner.taglineMode is default", () => {
readCliBannerTaglineModeMock.mockReturnValue("default");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",
env: { LANG: "en_US.UTF-8" },
isTty: true,
platform: "darwin",
richTty: false,
});
expect(line).toBe("🦞 OpenClaw 2026.3.7 (abc1234) — All your chats, one OpenClaw.");
});
it("prefers explicit tagline mode over config", () => {
readCliBannerTaglineModeMock.mockReturnValue("off");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",
env: { LANG: "en_US.UTF-8" },
isTty: true,
platform: "darwin",
richTty: false,
mode: "default",
});
expect(line).toBe("🦞 OpenClaw 2026.3.7 (abc1234) — All your chats, one OpenClaw.");
});
it("drops decorative emoji for generic Linux terminals", () => {
readCliBannerTaglineModeMock.mockReturnValue("off");
const line = formatCliBannerLine("2026.3.7", {
commit: "abc1234",
env: { TERM: "xterm-256color", LANG: "en_US.UTF-8" },
isTty: true,
platform: "linux",
richTty: false,
});
expect(line).toBe("OpenClaw 2026.3.7 (abc1234)");
});
});
describe("emitCliBanner", () => {
it("uses injected non-TTY state before writing to stdout", async () => {
const { emitCliBanner, hasEmittedCliBanner } = await importFreshBannerModule();
setStdoutIsTty(true);
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
emitCliBanner("2026.3.7", {
argv: ["node", "openclaw"],
commit: "abc1234",
isTty: false,
mode: "off",
richTty: false,
});
expect(writeSpy).not.toHaveBeenCalled();
expect(hasEmittedCliBanner()).toBe(false);
});
it("allows injected TTY state to emit when stdout lacks isTTY", async () => {
const { emitCliBanner, hasEmittedCliBanner } = await importFreshBannerModule();
setStdoutIsTty(false);
const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
emitCliBanner("2026.3.7", {
argv: ["node", "openclaw"],
commit: "abc1234",
env: { LANG: "en_US.UTF-8" },
isTty: true,
mode: "off",
platform: "darwin",
richTty: false,
});
expect(writeSpy).toHaveBeenCalledWith("\n🦞 OpenClaw 2026.3.7 (abc1234)\n\n");
expect(hasEmittedCliBanner()).toBe(true);
});
});