mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 19:40:40 +00:00
formatConsoleTimestamp previously used Date.toISOString() which always returns UTC time (suffixed with Z). This confused users whose local timezone differs from UTC. Now uses local time methods (getHours, getMinutes, etc.) and appends the local UTC offset (e.g. +08:00) instead of Z. The pretty style returns local HH:MM:SS. The hasTimestampPrefix regex is updated to accept both Z and +/-HH:MM offset suffixes. Closes #14699
162 lines
5.0 KiB
TypeScript
162 lines
5.0 KiB
TypeScript
import crypto from "node:crypto";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
enableConsoleCapture,
|
|
resetLogger,
|
|
routeLogsToStderr,
|
|
setConsoleTimestampPrefix,
|
|
setLoggerOverride,
|
|
} from "../logging.js";
|
|
import { loggingState } from "./state.js";
|
|
|
|
type ConsoleSnapshot = {
|
|
log: typeof console.log;
|
|
info: typeof console.info;
|
|
warn: typeof console.warn;
|
|
error: typeof console.error;
|
|
debug: typeof console.debug;
|
|
trace: typeof console.trace;
|
|
};
|
|
|
|
let snapshot: ConsoleSnapshot;
|
|
|
|
beforeEach(() => {
|
|
snapshot = {
|
|
log: console.log,
|
|
info: console.info,
|
|
warn: console.warn,
|
|
error: console.error,
|
|
debug: console.debug,
|
|
trace: console.trace,
|
|
};
|
|
loggingState.consolePatched = false;
|
|
loggingState.forceConsoleToStderr = false;
|
|
loggingState.consoleTimestampPrefix = false;
|
|
loggingState.rawConsole = null;
|
|
resetLogger();
|
|
});
|
|
|
|
afterEach(() => {
|
|
console.log = snapshot.log;
|
|
console.info = snapshot.info;
|
|
console.warn = snapshot.warn;
|
|
console.error = snapshot.error;
|
|
console.debug = snapshot.debug;
|
|
console.trace = snapshot.trace;
|
|
loggingState.consolePatched = false;
|
|
loggingState.forceConsoleToStderr = false;
|
|
loggingState.consoleTimestampPrefix = false;
|
|
loggingState.rawConsole = null;
|
|
resetLogger();
|
|
setLoggerOverride(null);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("enableConsoleCapture", () => {
|
|
it("swallows EIO from stderr writes", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
vi.spyOn(process.stderr, "write").mockImplementation(() => {
|
|
throw eioError();
|
|
});
|
|
routeLogsToStderr();
|
|
enableConsoleCapture();
|
|
expect(() => console.log("hello")).not.toThrow();
|
|
});
|
|
|
|
it("swallows EIO from original console writes", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
console.log = () => {
|
|
throw eioError();
|
|
};
|
|
enableConsoleCapture();
|
|
expect(() => console.log("hello")).not.toThrow();
|
|
});
|
|
|
|
it("prefixes console output with timestamps when enabled", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
const now = new Date("2026-01-17T18:01:02.000Z");
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(now);
|
|
const warn = vi.fn();
|
|
console.warn = warn;
|
|
setConsoleTimestampPrefix(true);
|
|
enableConsoleCapture();
|
|
console.warn("[EventQueue] Slow listener detected");
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
const firstArg = String(warn.mock.calls[0]?.[0] ?? "");
|
|
// Timestamp uses local time with timezone offset instead of UTC "Z" suffix
|
|
expect(firstArg).toMatch(
|
|
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2} \[EventQueue\]/,
|
|
);
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("suppresses discord EventQueue slow listener duplicates", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
const warn = vi.fn();
|
|
console.warn = warn;
|
|
enableConsoleCapture();
|
|
console.warn(
|
|
"[EventQueue] Slow listener detected: DiscordMessageListener took 12.3 seconds for event MESSAGE_CREATE",
|
|
);
|
|
expect(warn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not double-prefix timestamps", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
const warn = vi.fn();
|
|
console.warn = warn;
|
|
setConsoleTimestampPrefix(true);
|
|
enableConsoleCapture();
|
|
console.warn("12:34:56 [exec] hello");
|
|
expect(warn).toHaveBeenCalledWith("12:34:56 [exec] hello");
|
|
});
|
|
|
|
it("leaves JSON output unchanged when timestamp prefix is enabled", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
const log = vi.fn();
|
|
console.log = log;
|
|
setConsoleTimestampPrefix(true);
|
|
enableConsoleCapture();
|
|
const payload = JSON.stringify({ ok: true });
|
|
console.log(payload);
|
|
expect(log).toHaveBeenCalledWith(payload);
|
|
});
|
|
|
|
it("swallows async EPIPE on stdout", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
enableConsoleCapture();
|
|
const epipe = new Error("write EPIPE") as NodeJS.ErrnoException;
|
|
epipe.code = "EPIPE";
|
|
expect(() => process.stdout.emit("error", epipe)).not.toThrow();
|
|
});
|
|
|
|
it("swallows async EPIPE on stderr", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
enableConsoleCapture();
|
|
const epipe = new Error("write EPIPE") as NodeJS.ErrnoException;
|
|
epipe.code = "EPIPE";
|
|
expect(() => process.stderr.emit("error", epipe)).not.toThrow();
|
|
});
|
|
|
|
it("rethrows non-EPIPE errors on stdout", () => {
|
|
setLoggerOverride({ level: "info", file: tempLogPath() });
|
|
enableConsoleCapture();
|
|
const other = new Error("EACCES") as NodeJS.ErrnoException;
|
|
other.code = "EACCES";
|
|
expect(() => process.stdout.emit("error", other)).toThrow("EACCES");
|
|
});
|
|
});
|
|
|
|
function tempLogPath() {
|
|
return path.join(os.tmpdir(), `openclaw-log-${crypto.randomUUID()}.log`);
|
|
}
|
|
|
|
function eioError() {
|
|
const err = new Error("EIO") as NodeJS.ErrnoException;
|
|
err.code = "EIO";
|
|
return err;
|
|
}
|