| import { EventEmitter } from "node:events"; |
| import { describe, expect, it, vi } from "vitest"; |
| import { |
| createAccountStatusSink, |
| keepHttpServerTaskAlive, |
| runPassiveAccountLifecycle, |
| waitUntilAbort, |
| } from "./channel-lifecycle.js"; |
|
|
| type FakeServer = EventEmitter & { |
| close: (callback?: () => void) => void; |
| }; |
|
|
| function createFakeServer(): FakeServer { |
| const server = new EventEmitter() as FakeServer; |
| server.close = (callback) => { |
| queueMicrotask(() => { |
| server.emit("close"); |
| callback?.(); |
| }); |
| }; |
| return server; |
| } |
|
|
| describe("plugin-sdk channel lifecycle helpers", () => { |
| it("binds account id onto status patches", () => { |
| const setStatus = vi.fn(); |
| const statusSink = createAccountStatusSink({ |
| accountId: "default", |
| setStatus, |
| }); |
|
|
| statusSink({ running: true, lastStartAt: 123 }); |
|
|
| expect(setStatus).toHaveBeenCalledWith({ |
| accountId: "default", |
| running: true, |
| lastStartAt: 123, |
| }); |
| }); |
|
|
| it("resolves waitUntilAbort when signal aborts", async () => { |
| const abort = new AbortController(); |
| const task = waitUntilAbort(abort.signal); |
|
|
| const early = await Promise.race([ |
| task.then(() => "resolved"), |
| new Promise<"pending">((resolve) => setTimeout(() => resolve("pending"), 25)), |
| ]); |
| expect(early).toBe("pending"); |
|
|
| abort.abort(); |
| await expect(task).resolves.toBeUndefined(); |
| }); |
|
|
| it("runs abort cleanup before resolving", async () => { |
| const abort = new AbortController(); |
| const onAbort = vi.fn(async () => undefined); |
|
|
| const task = waitUntilAbort(abort.signal, onAbort); |
| abort.abort(); |
|
|
| await expect(task).resolves.toBeUndefined(); |
| expect(onAbort).toHaveBeenCalledOnce(); |
| }); |
|
|
| it("keeps passive account lifecycle pending until abort, then stops once", async () => { |
| const abort = new AbortController(); |
| const stop = vi.fn(); |
| const task = runPassiveAccountLifecycle({ |
| abortSignal: abort.signal, |
| start: async () => ({ stop }), |
| stop: async (handle) => { |
| handle.stop(); |
| }, |
| }); |
|
|
| const early = await Promise.race([ |
| task.then(() => "resolved"), |
| new Promise<"pending">((resolve) => setTimeout(() => resolve("pending"), 25)), |
| ]); |
| expect(early).toBe("pending"); |
| expect(stop).not.toHaveBeenCalled(); |
|
|
| abort.abort(); |
| await expect(task).resolves.toBeUndefined(); |
| expect(stop).toHaveBeenCalledOnce(); |
| }); |
|
|
| it("keeps server task pending until close, then resolves", async () => { |
| const server = createFakeServer(); |
| const task = keepHttpServerTaskAlive({ server }); |
|
|
| const early = await Promise.race([ |
| task.then(() => "resolved"), |
| new Promise<"pending">((resolve) => setTimeout(() => resolve("pending"), 25)), |
| ]); |
| expect(early).toBe("pending"); |
|
|
| server.close(); |
| await expect(task).resolves.toBeUndefined(); |
| }); |
|
|
| it("triggers abort hook once and resolves after close", async () => { |
| const server = createFakeServer(); |
| const abort = new AbortController(); |
| const onAbort = vi.fn(async () => { |
| server.close(); |
| }); |
|
|
| const task = keepHttpServerTaskAlive({ |
| server, |
| abortSignal: abort.signal, |
| onAbort, |
| }); |
|
|
| abort.abort(); |
| await expect(task).resolves.toBeUndefined(); |
| expect(onAbort).toHaveBeenCalledOnce(); |
| }); |
| }); |
|
|