Files
openclaw/src/logging/level-filter.test.ts
zhumengzhu f6bf8c7202 fix(logging): correct levelToMinLevel mapping and related filter logic for tslog v4 (#44646)
* fix: correct levelToMinLevel mapping and isFileLogLevelEnabled direction for tslog v4

* test: add regression tests for logging level filter and child logger inheritance

* fix: propagate minLevel to toPinoLikeLogger sub-loggers

* fix: correct shouldLogToConsole comparison direction in subsystem.ts

* test: cover logging threshold regressions

* fix(logging): treat silent as non-emittable level

---------

Co-authored-by: Altay <altay@uinaf.dev>
2026-04-08 00:19:20 +01:00

134 lines
5.3 KiB
TypeScript

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const { readLoggingConfigMock, shouldSkipMutatingLoggingConfigReadMock } = vi.hoisted(() => ({
readLoggingConfigMock: vi.fn(() => undefined),
shouldSkipMutatingLoggingConfigReadMock: vi.fn(() => false),
}));
vi.mock("./config.js", () => ({
readLoggingConfig: readLoggingConfigMock,
shouldSkipMutatingLoggingConfigRead: shouldSkipMutatingLoggingConfigReadMock,
}));
vi.mock("./node-require.js", () => ({
resolveNodeRequireFromMeta: () => () => {
throw new Error("config fallback not used");
},
}));
let logging: typeof import("../logging.js");
beforeAll(async () => {
logging = await import("../logging.js");
});
beforeEach(() => {
delete process.env.OPENCLAW_TEST_FILE_LOG;
delete process.env.OPENCLAW_LOG_LEVEL;
readLoggingConfigMock.mockClear();
shouldSkipMutatingLoggingConfigReadMock.mockReset();
shouldSkipMutatingLoggingConfigReadMock.mockReturnValue(false);
logging.resetLogger();
logging.setLoggerOverride(null);
});
afterEach(() => {
delete process.env.OPENCLAW_TEST_FILE_LOG;
delete process.env.OPENCLAW_LOG_LEVEL;
logging.resetLogger();
logging.setLoggerOverride(null);
vi.restoreAllMocks();
});
describe("isFileLogLevelEnabled", () => {
it("returns false for all levels when configured as silent", () => {
logging.setLoggerOverride({ level: "silent" });
expect(logging.isFileLogLevelEnabled("fatal")).toBe(false);
expect(logging.isFileLogLevelEnabled("error")).toBe(false);
expect(logging.isFileLogLevelEnabled("warn")).toBe(false);
expect(logging.isFileLogLevelEnabled("info")).toBe(false);
expect(logging.isFileLogLevelEnabled("debug")).toBe(false);
expect(logging.isFileLogLevelEnabled("trace")).toBe(false);
});
it("passes only fatal when configured as fatal", () => {
logging.setLoggerOverride({ level: "fatal" });
expect(logging.isFileLogLevelEnabled("fatal")).toBe(true);
expect(logging.isFileLogLevelEnabled("error")).toBe(false);
expect(logging.isFileLogLevelEnabled("warn")).toBe(false);
expect(logging.isFileLogLevelEnabled("info")).toBe(false);
expect(logging.isFileLogLevelEnabled("debug")).toBe(false);
expect(logging.isFileLogLevelEnabled("trace")).toBe(false);
});
it("passes fatal and error when configured as error", () => {
logging.setLoggerOverride({ level: "error" });
expect(logging.isFileLogLevelEnabled("fatal")).toBe(true);
expect(logging.isFileLogLevelEnabled("error")).toBe(true);
expect(logging.isFileLogLevelEnabled("warn")).toBe(false);
expect(logging.isFileLogLevelEnabled("info")).toBe(false);
expect(logging.isFileLogLevelEnabled("debug")).toBe(false);
expect(logging.isFileLogLevelEnabled("trace")).toBe(false);
});
it("passes fatal, error, warn, info when configured as info", () => {
logging.setLoggerOverride({ level: "info" });
expect(logging.isFileLogLevelEnabled("fatal")).toBe(true);
expect(logging.isFileLogLevelEnabled("error")).toBe(true);
expect(logging.isFileLogLevelEnabled("warn")).toBe(true);
expect(logging.isFileLogLevelEnabled("info")).toBe(true);
expect(logging.isFileLogLevelEnabled("debug")).toBe(false);
expect(logging.isFileLogLevelEnabled("trace")).toBe(false);
});
it("passes all levels when configured as trace", () => {
logging.setLoggerOverride({ level: "trace" });
expect(logging.isFileLogLevelEnabled("fatal")).toBe(true);
expect(logging.isFileLogLevelEnabled("error")).toBe(true);
expect(logging.isFileLogLevelEnabled("warn")).toBe(true);
expect(logging.isFileLogLevelEnabled("info")).toBe(true);
expect(logging.isFileLogLevelEnabled("debug")).toBe(true);
expect(logging.isFileLogLevelEnabled("trace")).toBe(true);
});
it("never treats silent as an emittable file level", () => {
logging.setLoggerOverride({ level: "info" });
expect(logging.isFileLogLevelEnabled("silent")).toBe(false);
});
});
describe("getChildLogger minLevel inheritance", () => {
it("child logger inherits parent minLevel when no level is specified", () => {
logging.setLoggerOverride({ level: "warn" });
const child = logging.getChildLogger({ component: "test" });
expect(child.settings.minLevel).toBe(logging.levelToMinLevel("warn"));
});
it("child logger uses its own level when explicitly specified", () => {
logging.setLoggerOverride({ level: "warn" });
const child = logging.getChildLogger({ component: "test" }, { level: "error" });
expect(child.settings.minLevel).toBe(logging.levelToMinLevel("error"));
});
it("child logger does not default to minLevel=0 (allow-all) when no level given", () => {
logging.setLoggerOverride({ level: "fatal" });
const child = logging.getChildLogger({ component: "test" });
expect(child.settings.minLevel).not.toBe(0);
expect(child.settings.minLevel).toBe(logging.levelToMinLevel("fatal"));
});
it("pino child logger propagates the parent minLevel", () => {
logging.setLoggerOverride({ level: "error" });
const base = logging.getLogger();
const getSubLoggerSpy = vi.spyOn(base, "getSubLogger");
logging.toPinoLikeLogger(base, "info").child({ component: "test" });
expect(getSubLoggerSpy).toHaveBeenCalledWith(
expect.objectContaining({
minLevel: logging.levelToMinLevel("error"),
}),
);
});
});