Files
openclaw/test/scripts/sqlite-reliability-process.test.ts
Vincent Koc ef2861d55a refactor(scripts): share SQLite reliability message waits (#146900)
* refactor(scripts): share SQLite reliability message waits

* fix(scripts): preserve IPC rejection values without lint errors

* test(scripts): register IPC rejection suppression
2026-09-14 01:45:43 +08:00

142 lines
4.6 KiB
TypeScript

import { ChildProcess } from "node:child_process";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { waitForReliabilityWorkerMessage } from "../../scripts/lib/sqlite-reliability-process.js";
import { waitForWriterMessage } from "../../scripts/lib/sqlite-reliability-writer.js";
function waitForReady(
child: ChildProcess,
overrides: Partial<Parameters<typeof waitForReliabilityWorkerMessage>[0]> = {},
) {
return waitForReliabilityWorkerMessage({
child,
matches: (message) => message === "ready",
timeoutMs: 30_000,
timeoutMessage: () => "worker timed out",
exitMessage: (code, signal) => `worker exited: ${code}/${signal}`,
...overrides,
});
}
function expectWaitCleanedUp(child: ChildProcess) {
for (const event of ["message", "error", "exit"]) {
expect(child.listenerCount(event)).toBe(0);
}
expect(vi.getTimerCount()).toBe(0);
}
describe("SQLite reliability worker messages", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("listens before sending, ignores other messages, and preserves other listeners", async () => {
const child = new ChildProcess();
const observed: unknown[] = [];
const observe = (message: unknown) => observed.push(message);
child.on("message", observe);
const ready = waitForReady(child, {
action: () => {
child.emit("message", "unrelated");
expect(child.listenerCount("message")).toBe(2);
child.emit("message", "ready");
},
});
await expect(ready).resolves.toBe("ready");
expect(observed).toEqual(["unrelated", "ready"]);
expect(child.listeners("message")).toEqual([observe]);
child.off("message", observe);
expectWaitCleanedUp(child);
});
it("rejects child errors without replacing the original error", async () => {
const child = new ChildProcess();
const error = new Error("IPC failed");
const ready = waitForReady(child);
child.emit("error", error);
await expect(ready).rejects.toBe(error);
expectWaitCleanedUp(child);
});
it("formats premature exit diagnostics when the child exits", async () => {
const child = new ChildProcess();
let stderr = "before";
const ready = waitForReady(child, {
exitMessage: (code, signal) => `exit ${code}/${signal}: ${stderr}`,
});
stderr = "last stderr";
child.emit("exit", null, "SIGKILL");
await expect(ready).rejects.toThrow("exit null/SIGKILL: last stderr");
expectWaitCleanedUp(child);
});
it.each([30_000, 120_000])(
"honors a %i ms timeout and reads final diagnostics",
async (timeoutMs) => {
const child = new ChildProcess();
let stderr = "before";
const ready = waitForReady(child, {
timeoutMs,
timeoutMessage: () => `timeout: ${stderr}`,
});
const rejected = expect(ready).rejects.toThrow("timeout: last stderr");
await vi.advanceTimersByTimeAsync(timeoutMs - 1);
expect(child.listenerCount("message")).toBe(1);
stderr = "last stderr";
await vi.advanceTimersByTimeAsync(1);
await rejected;
expectWaitCleanedUp(child);
},
);
it.each(["action", "matches"] as const)("cleans up when %s throws", async (source) => {
const child = new ChildProcess();
const error = new Error(`${source} failed`);
const ready = waitForReady(child, {
action: () => {
if (source === "action") {
throw error;
}
child.emit("message", "ready");
},
matches: () => {
throw error;
},
});
await expect(ready).rejects.toBe(error);
expectWaitCleanedUp(child);
});
it("rejects a writer error payload instead of waiting for the requested message", async () => {
const child = new ChildProcess();
const result = waitForWriterMessage({ child, stderr: [], stopped: false }, "result");
child.emit("message", { kind: "error", error: "write failed" });
await expect(result).rejects.toThrow("SQLite reliability writer failed: write failed");
expectWaitCleanedUp(child);
});
it("returns the requested writer payload from a synchronous action reply", async () => {
const child = new ChildProcess();
const result = await waitForWriterMessage(
{ child, stderr: [], stopped: false },
"result",
() => {
child.emit("message", { kind: "ready" });
child.emit("message", { kind: "result", batchesCommitted: 2, rowsCommitted: 16 });
},
);
expect(result.batchesCommitted).toBe(2);
expect(result.rowsCommitted).toBe(16);
expectWaitCleanedUp(child);
});
});