mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import { runRegisteredCli } from "../test-utils/command-runner.js";
|
|
import { formatLogTimestamp } from "./logs-cli.js";
|
|
|
|
const callGatewayFromCli = vi.fn();
|
|
|
|
vi.mock("./gateway-rpc.js", async () => {
|
|
const actual = await vi.importActual<typeof import("./gateway-rpc.js")>("./gateway-rpc.js");
|
|
return {
|
|
...actual,
|
|
callGatewayFromCli: (...args: unknown[]) => callGatewayFromCli(...args),
|
|
};
|
|
});
|
|
|
|
let registerLogsCli: typeof import("./logs-cli.js").registerLogsCli;
|
|
|
|
beforeAll(async () => {
|
|
({ registerLogsCli } = await import("./logs-cli.js"));
|
|
});
|
|
|
|
async function runLogsCli(argv: string[]) {
|
|
await runRegisteredCli({
|
|
register: registerLogsCli as (program: import("commander").Command) => void,
|
|
argv,
|
|
});
|
|
}
|
|
|
|
describe("logs cli", () => {
|
|
afterEach(() => {
|
|
callGatewayFromCli.mockClear();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("writes output directly to stdout/stderr", async () => {
|
|
callGatewayFromCli.mockResolvedValueOnce({
|
|
file: "/tmp/openclaw.log",
|
|
cursor: 1,
|
|
size: 123,
|
|
lines: ["raw line"],
|
|
truncated: true,
|
|
reset: true,
|
|
});
|
|
|
|
const stdoutWrites: string[] = [];
|
|
const stderrWrites: string[] = [];
|
|
vi.spyOn(process.stdout, "write").mockImplementation((chunk: unknown) => {
|
|
stdoutWrites.push(String(chunk));
|
|
return true;
|
|
});
|
|
vi.spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
|
|
stderrWrites.push(String(chunk));
|
|
return true;
|
|
});
|
|
|
|
await runLogsCli(["logs"]);
|
|
|
|
expect(stdoutWrites.join("")).toContain("Log file:");
|
|
expect(stdoutWrites.join("")).toContain("raw line");
|
|
expect(stderrWrites.join("")).toContain("Log tail truncated");
|
|
expect(stderrWrites.join("")).toContain("Log cursor reset");
|
|
});
|
|
|
|
it("wires --local-time through CLI parsing and emits local timestamps", async () => {
|
|
callGatewayFromCli.mockResolvedValueOnce({
|
|
file: "/tmp/openclaw.log",
|
|
lines: [
|
|
JSON.stringify({
|
|
time: "2025-01-01T12:00:00.000Z",
|
|
_meta: { logLevelName: "INFO", name: JSON.stringify({ subsystem: "gateway" }) },
|
|
0: "line one",
|
|
}),
|
|
],
|
|
});
|
|
|
|
const stdoutWrites: string[] = [];
|
|
vi.spyOn(process.stdout, "write").mockImplementation((chunk: unknown) => {
|
|
stdoutWrites.push(String(chunk));
|
|
return true;
|
|
});
|
|
|
|
await runLogsCli(["logs", "--local-time", "--plain"]);
|
|
|
|
const output = stdoutWrites.join("");
|
|
expect(output).toContain("line one");
|
|
const timestamp = output.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z?/u)?.[0];
|
|
expect(timestamp).toBeTruthy();
|
|
expect(timestamp?.endsWith("Z")).toBe(false);
|
|
});
|
|
|
|
it("warns when the output pipe closes", async () => {
|
|
callGatewayFromCli.mockResolvedValueOnce({
|
|
file: "/tmp/openclaw.log",
|
|
lines: ["line one"],
|
|
});
|
|
|
|
const stderrWrites: string[] = [];
|
|
vi.spyOn(process.stdout, "write").mockImplementation(() => {
|
|
const err = new Error("EPIPE") as NodeJS.ErrnoException;
|
|
err.code = "EPIPE";
|
|
throw err;
|
|
});
|
|
vi.spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
|
|
stderrWrites.push(String(chunk));
|
|
return true;
|
|
});
|
|
|
|
await runLogsCli(["logs"]);
|
|
|
|
expect(stderrWrites.join("")).toContain("output stdout closed");
|
|
});
|
|
|
|
describe("formatLogTimestamp", () => {
|
|
it("formats UTC timestamp in plain mode by default", () => {
|
|
const result = formatLogTimestamp("2025-01-01T12:00:00.000Z");
|
|
expect(result).toBe("2025-01-01T12:00:00.000Z");
|
|
});
|
|
|
|
it("formats UTC timestamp in pretty mode", () => {
|
|
const result = formatLogTimestamp("2025-01-01T12:00:00.000Z", "pretty");
|
|
expect(result).toBe("12:00:00");
|
|
});
|
|
|
|
it("formats local time in plain mode when localTime is true", () => {
|
|
const utcTime = "2025-01-01T12:00:00.000Z";
|
|
const result = formatLogTimestamp(utcTime, "plain", true);
|
|
// Should be local time with explicit timezone offset (not 'Z' suffix).
|
|
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
|
|
// The exact time depends on timezone, but should be different from UTC
|
|
expect(result).not.toBe(utcTime);
|
|
});
|
|
|
|
it("formats local time in pretty mode when localTime is true", () => {
|
|
const utcTime = "2025-01-01T12:00:00.000Z";
|
|
const result = formatLogTimestamp(utcTime, "pretty", true);
|
|
// Should be HH:MM:SS format
|
|
expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
|
|
// Should be different from UTC time (12:00:00) if not in UTC timezone
|
|
const tzOffset = new Date(utcTime).getTimezoneOffset();
|
|
if (tzOffset !== 0) {
|
|
expect(result).not.toBe("12:00:00");
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
{ input: undefined, expected: "" },
|
|
{ input: "", expected: "" },
|
|
{ input: "invalid-date", expected: "invalid-date" },
|
|
{ input: "not-a-date", expected: "not-a-date" },
|
|
])("preserves timestamp fallback for $input", ({ input, expected }) => {
|
|
expect(formatLogTimestamp(input)).toBe(expected);
|
|
});
|
|
});
|
|
});
|