mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 07:47:27 +01:00
fix(design): daemon log stream handles open errors instead of crashing the daemon; shutdown test awaits the stubbed exit
This commit is contained in:
1 parent
9b0a06b180
commit
8b4d74c4c2
2 files changed
+46
-9
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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<void>((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", () => {
|
||||
|
||||
Reference in new issue
Block a user