diff --git a/src/logging/logger.browser-import.test.ts b/src/logging/logger.browser-import.test.ts new file mode 100644 index 00000000000..5704770d3ed --- /dev/null +++ b/src/logging/logger.browser-import.test.ts @@ -0,0 +1,70 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +type LoggerModule = typeof import("./logger.js"); + +const originalGetBuiltinModule = ( + process as NodeJS.Process & { getBuiltinModule?: (id: string) => unknown } +).getBuiltinModule; + +async function importBrowserSafeLogger(params?: { + resolvePreferredOpenClawTmpDir?: ReturnType; +}): Promise<{ + module: LoggerModule; + resolvePreferredOpenClawTmpDir: ReturnType; +}> { + vi.resetModules(); + const resolvePreferredOpenClawTmpDir = + params?.resolvePreferredOpenClawTmpDir ?? + vi.fn(() => { + throw new Error("resolvePreferredOpenClawTmpDir should not run during browser-safe import"); + }); + + vi.doMock("../infra/tmp-openclaw-dir.js", async () => { + const actual = await vi.importActual( + "../infra/tmp-openclaw-dir.js", + ); + return { + ...actual, + resolvePreferredOpenClawTmpDir, + }; + }); + + Object.defineProperty(process, "getBuiltinModule", { + configurable: true, + value: undefined, + }); + + const module = await import("./logger.js"); + return { module, resolvePreferredOpenClawTmpDir }; +} + +describe("logging/logger browser-safe import", () => { + afterEach(() => { + vi.resetModules(); + vi.doUnmock("../infra/tmp-openclaw-dir.js"); + Object.defineProperty(process, "getBuiltinModule", { + configurable: true, + value: originalGetBuiltinModule, + }); + }); + + it("does not resolve the preferred temp dir at import time when node fs is unavailable", async () => { + const { module, resolvePreferredOpenClawTmpDir } = await importBrowserSafeLogger(); + + expect(resolvePreferredOpenClawTmpDir).not.toHaveBeenCalled(); + expect(module.DEFAULT_LOG_DIR).toBe("/tmp/openclaw"); + expect(module.DEFAULT_LOG_FILE).toBe("/tmp/openclaw/openclaw.log"); + }); + + it("disables file logging when imported in a browser-like environment", async () => { + const { module, resolvePreferredOpenClawTmpDir } = await importBrowserSafeLogger(); + + expect(module.getResolvedLoggerSettings()).toMatchObject({ + level: "silent", + file: "/tmp/openclaw/openclaw.log", + }); + expect(module.isFileLogLevelEnabled("info")).toBe(false); + expect(() => module.getLogger().info("browser-safe")).not.toThrow(); + expect(resolvePreferredOpenClawTmpDir).not.toHaveBeenCalled(); + }); +}); diff --git a/src/logging/logger.ts b/src/logging/logger.ts index 47e5624dc20..d73009fc696 100644 --- a/src/logging/logger.ts +++ b/src/logging/logger.ts @@ -3,7 +3,10 @@ import path from "node:path"; import { Logger as TsLogger } from "tslog"; import { getCommandPathWithRootOptions } from "../cli/argv.js"; import type { OpenClawConfig } from "../config/types.js"; -import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; +import { + POSIX_OPENCLAW_TMP_DIR, + resolvePreferredOpenClawTmpDir, +} from "../infra/tmp-openclaw-dir.js"; import { readLoggingConfig } from "./config.js"; import type { ConsoleStyle } from "./console.js"; import { resolveEnvLogLevelOverride } from "./env-log-level.js"; @@ -12,7 +15,27 @@ import { resolveNodeRequireFromMeta } from "./node-require.js"; import { loggingState } from "./state.js"; import { formatLocalIsoWithOffset } from "./timestamps.js"; -export const DEFAULT_LOG_DIR = resolvePreferredOpenClawTmpDir(); +type ProcessWithBuiltinModule = NodeJS.Process & { + getBuiltinModule?: (id: string) => unknown; +}; + +function canUseNodeFs(): boolean { + const getBuiltinModule = (process as ProcessWithBuiltinModule).getBuiltinModule; + if (typeof getBuiltinModule !== "function") { + return false; + } + try { + return getBuiltinModule("fs") !== undefined; + } catch { + return false; + } +} + +function resolveDefaultLogDir(): string { + return canUseNodeFs() ? resolvePreferredOpenClawTmpDir() : POSIX_OPENCLAW_TMP_DIR; +} + +export const DEFAULT_LOG_DIR = resolveDefaultLogDir(); export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log"); // legacy single-file path const LOG_PREFIX = "openclaw"; @@ -71,6 +94,14 @@ function canUseSilentVitestFileLogFastPath(envLevel: LogLevel | undefined): bool } function resolveSettings(): ResolvedSettings { + if (!canUseNodeFs()) { + return { + level: "silent", + file: DEFAULT_LOG_FILE, + maxFileBytes: DEFAULT_MAX_LOG_FILE_BYTES, + }; + } + const envLevel = resolveEnvLogLevelOverride(); // Test runs default file logs to silent. Skip config reads and fallback load in the // common case to avoid pulling heavy config/schema stacks on startup.