import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getLogger, getResolvedLoggerSettings, resetLogger, setLoggerOverride, } from "../logging.js"; const DEFAULT_MAX_FILE_BYTES = 500 * 1024 * 1024; describe("log file size cap", () => { let logPath = ""; beforeEach(() => { logPath = path.join(os.tmpdir(), `openclaw-log-cap-${crypto.randomUUID()}.log`); resetLogger(); setLoggerOverride(null); }); afterEach(() => { resetLogger(); setLoggerOverride(null); vi.restoreAllMocks(); try { fs.rmSync(logPath, { force: true }); } catch { // ignore cleanup errors } }); it("defaults maxFileBytes to 500 MB when unset", () => { setLoggerOverride({ level: "info", file: logPath }); expect(getResolvedLoggerSettings().maxFileBytes).toBe(DEFAULT_MAX_FILE_BYTES); }); it("uses configured maxFileBytes", () => { setLoggerOverride({ level: "info", file: logPath, maxFileBytes: 2048 }); expect(getResolvedLoggerSettings().maxFileBytes).toBe(2048); }); it("suppresses file writes after cap is reached and warns once", () => { const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation( () => true as unknown as ReturnType, // preserve stream contract in test spy ); setLoggerOverride({ level: "info", file: logPath, maxFileBytes: 1024 }); const logger = getLogger(); for (let i = 0; i < 200; i++) { logger.error(`network-failure-${i}-${"x".repeat(80)}`); } const sizeAfterCap = fs.statSync(logPath).size; for (let i = 0; i < 20; i++) { logger.error(`post-cap-${i}-${"y".repeat(80)}`); } const sizeAfterExtraLogs = fs.statSync(logPath).size; expect(sizeAfterExtraLogs).toBe(sizeAfterCap); expect(sizeAfterCap).toBeLessThanOrEqual(1024 + 512); const capWarnings = stderrSpy.mock.calls .map(([firstArg]) => String(firstArg)) .filter((line) => line.includes("log file size cap reached")); expect(capWarnings).toHaveLength(1); }); });