From 8b4d74c4c21ff009eb2b5d024e8d29488a9968b0 Mon Sep 17 00:00:00 2001 From: garrytan Date: Fri, 2 Oct 2026 23:10:38 +0000 Subject: [PATCH] fix(design): daemon log stream handles open errors instead of crashing the daemon; shutdown test awaits the stubbed exit --- design/src/daemon.ts | 9 +++++++- design/test/daemon.test.ts | 46 +++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/design/src/daemon.ts b/design/src/daemon.ts index 94de82a71..2aeb300d3 100644 --- a/design/src/daemon.ts +++ b/design/src/daemon.ts @@ -93,7 +93,11 @@ function openDaemonLog(): fs.WriteStream | null { try { const p = resolveDaemonLogPath(); fs.mkdirSync(path.dirname(p), { recursive: true }); - return fs.createWriteStream(p, { flags: "a" }); + const stream = fs.createWriteStream(p, { flags: "a" }); + stream.on("error", () => { + if (daemonLog === stream) daemonLog = null; + }); + return stream; } catch { return null; } @@ -574,7 +578,10 @@ export const __testInternals__ = { fetchHandler, idleCheckTick, markMeaningfulActivity, + daemonLog: () => daemonLog, resetForTest: (): void => { + daemonLog?.end(); + daemonLog = undefined; boards.clear(); boardMutex.clear(); lastMeaningfulActivity = Date.now(); diff --git a/design/test/daemon.test.ts b/design/test/daemon.test.ts index a008c2186..c3f9dabb6 100644 --- a/design/test/daemon.test.ts +++ b/design/test/daemon.test.ts @@ -16,7 +16,7 @@ const { markMeaningfulActivity } = __testInternals__; import { makeBoardHtml, makeTmpDir, req, resetDaemon } from "./daemon-tests-fixtures"; import { usePrivateStateRoot } from "../../test/helpers/private-state-root"; -usePrivateStateRoot(); +const stateRoot = usePrivateStateRoot(); let tmpDir: string; @@ -368,19 +368,25 @@ describe("daemon /shutdown", () => { // arms setTimeout(process.exit, 50). bun test runs ALL files in one // process, so letting that exit fire would kill the whole suite ~100ms // later (exit 0, no summary — see test/no-suicide-exit.test.ts). Stub - // process.exit, wait past both timers so they fire harmlessly while - // stubbed, then restore. (resetForTest does NOT defuse the timers: the - // exit callback is unconditional.) + // process.exit and restore it only after the exit callback has reached + // the stub; a fixed sleep lets a late timer reach the real exit under + // load. (resetForTest does NOT defuse the timers: the exit callback is + // unconditional.) const origExit = process.exit; - (process as any).exit = (() => undefined) as any; + let exitCode: number | undefined; + const exited = new Promise((resolve) => { + (process as any).exit = ((code?: number) => { + exitCode = code; + resolve(); + }) as any; + }); try { const r = await fetchHandler(req("POST", "/shutdown")); expect(r.status).toBe(200); const body = (await r.json()) as any; expect(body.shuttingDown).toBe(true); - // Let both 50ms timers (gracefulShutdown, then its process.exit) fire - // against the stub before restoring the real process.exit. - await new Promise((resolve) => setTimeout(resolve, 200)); + await exited; + expect(exitCode).toBe(0); } finally { (process as any).exit = origExit; } @@ -533,6 +539,30 @@ describe("daemon malformed body handling", () => { }); }); +// ─── Daemon log ────────────────────────────────────────────────── + +describe("daemon log", () => { + test("an unopenable log disables file logging instead of raising an uncaught error", async () => { + // A directory where the log file belongs makes the asynchronous open fail + // with EISDIR, the same path as a state root removed before the open lands. + fs.mkdirSync(path.join(stateRoot.dir, "design-daemon.log")); + await publishTestBoard(); + const log = __testInternals__.daemonLog(); + expect(log).toBeTruthy(); + while (__testInternals__.daemonLog() === log) await new Promise((r) => setTimeout(r, 1)); + expect(log!.destroyed).toBe(true); + expect(__testInternals__.daemonLog()).toBeNull(); + + const dirB = makeTmpDir("log-b"); + try { + await publishTestBoard({ dir: dirB }); + expect(__testInternals__.boards.size).toBe(2); + } finally { + try { fs.rmSync(dirB, { recursive: true, force: true }); } catch {} + } + }); +}); + // ─── Unknown routes ────────────────────────────────────────────── describe("daemon unknown routes", () => {