diff options
Diffstat (limited to 'packages/host-bin/src/collector-supervisor.test.ts')
| -rw-r--r-- | packages/host-bin/src/collector-supervisor.test.ts | 574 |
1 files changed, 287 insertions, 287 deletions
diff --git a/packages/host-bin/src/collector-supervisor.test.ts b/packages/host-bin/src/collector-supervisor.test.ts index 8c1f104..f590806 100644 --- a/packages/host-bin/src/collector-supervisor.test.ts +++ b/packages/host-bin/src/collector-supervisor.test.ts @@ -2,300 +2,300 @@ 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[]; + readonly handle: ChildHandle; + resolveExit: (code: number) => void; + readonly signals: string[]; } function createFakeChild(code = 0): FakeChild { - let resolveExit!: (code: number) => void; - const exited = new Promise<number>((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 }; + let resolveExit!: (code: number) => void; + const exited = new Promise<number>((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: () => {}, - }), - }; + 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<void>((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<void>((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<void>((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(); - }); + 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<void>((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<void>((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<void>((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(); + }); }); |
