openclaw/src/plugin-sdk/channel-lifecycle.test.ts

122 lines
3.3 KiB
TypeScript

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();
});
});