Logging: guard console settings recursion

This commit is contained in:
Travis
2026-01-23 21:30:57 -05:00
committed by Peter Steinberger
parent 17f2a990a8
commit 3ba9821254
3 changed files with 97 additions and 6 deletions

View File

@@ -0,0 +1,83 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("./config.js", () => ({
readLoggingConfig: () => undefined,
}));
vi.mock("./logger.js", () => ({
getLogger: () => ({
trace: () => {},
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
fatal: () => {},
}),
}));
let loadConfigCalls = 0;
vi.mock("node:module", async () => {
const actual = await vi.importActual<typeof import("node:module")>("node:module");
return {
...actual,
createRequire: (url: string | URL) => {
const realRequire = actual.createRequire(url);
return (specifier: string) => {
if (specifier.endsWith("config.js")) {
return {
loadConfig: () => {
loadConfigCalls += 1;
if (loadConfigCalls > 5) {
return {};
}
console.error("config load failed");
return {};
},
};
}
return realRequire(specifier);
};
},
};
});
let originalIsTty: boolean | undefined;
beforeEach(() => {
loadConfigCalls = 0;
vi.resetModules();
originalIsTty = process.stdout.isTTY;
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
});
afterEach(() => {
Object.defineProperty(process.stdout, "isTTY", { value: originalIsTty, configurable: true });
vi.restoreAllMocks();
});
async function loadLogging() {
const logging = await import("../logging.js");
const state = await import("./state.js");
state.loggingState.cachedConsoleSettings = null;
return { logging, state };
}
describe("getConsoleSettings", () => {
it("does not recurse when loadConfig logs during resolution", async () => {
const { logging } = await loadLogging();
logging.setConsoleTimestampPrefix(true);
logging.enableConsoleCapture();
const { getConsoleSettings } = logging;
getConsoleSettings();
expect(loadConfigCalls).toBe(1);
});
it("skips config fallback during re-entrant resolution", async () => {
const { logging, state } = await loadLogging();
state.loggingState.resolvingConsoleSettings = true;
logging.setConsoleTimestampPrefix(true);
logging.enableConsoleCapture();
logging.getConsoleSettings();
expect(loadConfigCalls).toBe(0);
state.loggingState.resolvingConsoleSettings = false;
});
});

View File

@@ -35,13 +35,20 @@ function resolveConsoleSettings(): ConsoleSettings {
let cfg: ClawdbotConfig["logging"] | undefined =
(loggingState.overrideSettings as LoggerSettings | null) ?? readLoggingConfig();
if (!cfg) {
try {
const loaded = requireConfig("../config/config.js") as {
loadConfig?: () => ClawdbotConfig;
};
cfg = loaded.loadConfig?.().logging;
} catch {
if (loggingState.resolvingConsoleSettings) {
cfg = undefined;
} else {
loggingState.resolvingConsoleSettings = true;
try {
const loaded = requireConfig("../config/config.js") as {
loadConfig?: () => ClawdbotConfig;
};
cfg = loaded.loadConfig?.().logging;
} catch {
cfg = undefined;
} finally {
loggingState.resolvingConsoleSettings = false;
}
}
}
const level = normalizeConsoleLevel(cfg?.consoleLevel);

View File

@@ -7,6 +7,7 @@ export const loggingState = {
forceConsoleToStderr: false,
consoleTimestampPrefix: false,
consoleSubsystemFilter: null as string[] | null,
resolvingConsoleSettings: false,
rawConsole: null as {
log: typeof console.log;
info: typeof console.info;