import { describe, expect, it } from "vitest"; import { type ChildHandle, createCollectorSupervisor } from "./collector-supervisor.js"; interface FakeChild { readonly handle: ChildHandle; resolveExit: (code: number) => void; readonly signals: string[]; } function createFakeChild(code = 0): FakeChild { let resolveExit!: (code: number) => void; const exited = new Promise((r) => { resolveExit = r; }); const signals: string[] = []; const handle: ChildHandle = { kill: (signal?: string) => { signals.push(signal ?? "SIGTERM"); if (signal === "SIGKILL") resolveExit(code); }, exited, }; return { handle, resolveExit, signals }; } function createFakeLogger() { const msgs: Array<{ level: string; msg: string }> = []; return { msgs, debug: () => {}, info: (msg: string) => msgs.push({ level: "info", msg }), warn: (msg: string) => msgs.push({ level: "warn", msg }), error: () => {}, child: () => createFakeLogger(), span: () => ({ id: "s", log: createFakeLogger(), setAttributes: () => {}, addLink: () => {}, child: () => ({}) as never, end: () => {}, }), }; } describe("createCollectorSupervisor", () => { const DEFAULTS = { journalPath: "/tmp/journal.ndjson", dbPath: "/tmp/traces.db", }; it("start() spawns with the correct command and args", () => { let capturedCmd: string[] = []; const children: FakeChild[] = []; const spawn = (cmd: string[]) => { capturedCmd = cmd; const child = createFakeChild(); children.push(child); return child.handle; }; const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: createFakeLogger() as never, }); supervisor.start(); expect(capturedCmd).toEqual([ "bun", "packages/observability-collector/src/main.ts", "--journal", "/tmp/journal.ndjson", "--db", "/tmp/traces.db", ]); }); it("start() passes --interval when provided", () => { let capturedCmd: string[] = []; const spawn = (cmd: string[]) => { capturedCmd = cmd; return createFakeChild().handle; }; const supervisor = createCollectorSupervisor({ ...DEFAULTS, interval: 500, spawn, logger: createFakeLogger() as never, }); supervisor.start(); expect(capturedCmd).toContain("--interval"); expect(capturedCmd).toContain("500"); }); it("unexpected child exit respawns the collector", async () => { const children: FakeChild[] = []; let spawnCount = 0; const spawn = () => { spawnCount++; const child = createFakeChild(); children.push(child); return child.handle; }; const time = 0; const now = () => time; const delayResolvers: Array<() => void> = []; const delay = (_ms: number) => new Promise((r) => { delayResolvers.push(r); }); const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: createFakeLogger() as never, now, delay, }); supervisor.start(); expect(spawnCount).toBe(1); // Simulate unexpected exit children[0]?.resolveExit(1); await Promise.resolve(); await Promise.resolve(); // Trigger the backoff delay resolver expect(delayResolvers.length).toBe(1); delayResolvers[0]?.(); await Promise.resolve(); await Promise.resolve(); expect(spawnCount).toBe(2); }); it("restart guard caps respawns in a tight loop", async () => { const children: FakeChild[] = []; let spawnCount = 0; const spawn = () => { spawnCount++; const child = createFakeChild(); children.push(child); return child.handle; }; const time = 0; const now = () => time; const delayResolvers: Array<() => void> = []; const delay = (_ms: number) => new Promise((r) => { delayResolvers.push(r); }); const logger = createFakeLogger(); const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: logger as never, now, delay, }); supervisor.start(); // Simulate rapid crashes (within the restart window) for (let i = 0; i < 5; i++) { children[i]?.resolveExit(1); await Promise.resolve(); await Promise.resolve(); if (delayResolvers.length > i) { delayResolvers[i]?.(); await Promise.resolve(); await Promise.resolve(); } } // Should have spawned 6 times (1 initial + 5 restarts) expect(spawnCount).toBe(6); // 6th child also dies — should NOT respawn (cap reached) children[5]?.resolveExit(1); await Promise.resolve(); await Promise.resolve(); // spawnCount should still be 6 expect(spawnCount).toBe(6); expect(logger.msgs.some((m) => m.msg === "Collector restart cap reached; giving up")).toBe( true, ); }); it("stop() sends SIGTERM and does not respawn", async () => { const child = createFakeChild(); const spawn = () => child.handle; const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: createFakeLogger() as never, }); supervisor.start(); // Resolve exit after SIGTERM (simulating graceful shutdown) const stopPromise = supervisor.stop(); child.resolveExit(0); await stopPromise; expect(child.signals).toContain("SIGTERM"); }); it("stop() sends SIGKILL when child does not exit in time", async () => { const child = createFakeChild(); const spawn = () => child.handle; const time = 0; const now = () => time; const delayResolvers: Array<() => void> = []; const delay = (_ms: number) => new Promise((r) => { delayResolvers.push(r); }); const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: createFakeLogger() as never, now, delay, }); supervisor.start(); const stopPromise = supervisor.stop(); // Don't resolve exit — simulate hung child // Resolve the timeout delay instead expect(delayResolvers.length).toBe(1); delayResolvers[0]?.(); await stopPromise; expect(child.signals).toContain("SIGTERM"); expect(child.signals).toContain("SIGKILL"); }); it("stop() does not respawn after unexpected exit during stop", async () => { const children: FakeChild[] = []; let spawnCount = 0; const spawn = () => { spawnCount++; const child = createFakeChild(); children.push(child); return child.handle; }; const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: createFakeLogger() as never, }); supervisor.start(); expect(spawnCount).toBe(1); // Child exits during stop — supervisor is already stopping const stopPromise = supervisor.stop(); children[0]?.resolveExit(1); await stopPromise; // Should NOT have respawned expect(spawnCount).toBe(1); }); it("spawn throwing does not throw to caller", () => { const spawn = () => { throw new Error("spawn failed"); }; const logger = createFakeLogger(); const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: logger as never, }); expect(() => supervisor.start()).not.toThrow(); expect(logger.msgs.some((m) => m.msg === "Failed to spawn collector")).toBe(true); }); it("stop() is safe to call when no child was started", async () => { const spawn = () => createFakeChild().handle; const supervisor = createCollectorSupervisor({ ...DEFAULTS, spawn, logger: createFakeLogger() as never, }); await expect(supervisor.stop()).resolves.toBeUndefined(); }); });