111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { MailboxStore } from "../src/db.js";
|
|
|
|
let dir: string;
|
|
let store: MailboxStore;
|
|
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(join(tmpdir(), "claude-mailbox-wait-"));
|
|
store = new MailboxStore(join(dir, "test.db"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
store.close();
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("MailboxStore.waitForMessage", () => {
|
|
it("returns an already-pending message immediately", async () => {
|
|
store.upsertMailbox("alice");
|
|
store.upsertMailbox("bob");
|
|
store.send("alice", "bob", "hello");
|
|
|
|
const ac = new AbortController();
|
|
const result = await store.waitForMessage("bob", 1000, ac.signal);
|
|
expect(result.kind).toBe("message");
|
|
if (result.kind === "message") {
|
|
expect(result.message.body).toBe("hello");
|
|
expect(result.message.from_mailbox).toBe("alice");
|
|
expect(result.message.delivered_at).not.toBeNull();
|
|
}
|
|
});
|
|
|
|
it("blocks until a message arrives, then resolves", async () => {
|
|
store.upsertMailbox("alice");
|
|
store.upsertMailbox("bob");
|
|
|
|
const ac = new AbortController();
|
|
const pending = store.waitForMessage("bob", 5000, ac.signal);
|
|
|
|
setTimeout(() => store.send("alice", "bob", "later"), 50);
|
|
|
|
const result = await pending;
|
|
expect(result.kind).toBe("message");
|
|
if (result.kind === "message") expect(result.message.body).toBe("later");
|
|
});
|
|
|
|
it("resolves with timeout when nothing arrives", async () => {
|
|
store.upsertMailbox("bob");
|
|
const ac = new AbortController();
|
|
const result = await store.waitForMessage("bob", 80, ac.signal);
|
|
expect(result.kind).toBe("timeout");
|
|
});
|
|
|
|
it("resolves with aborted when the signal fires", async () => {
|
|
store.upsertMailbox("bob");
|
|
const ac = new AbortController();
|
|
const pending = store.waitForMessage("bob", 5000, ac.signal);
|
|
setTimeout(() => ac.abort(), 30);
|
|
const result = await pending;
|
|
expect(result.kind).toBe("aborted");
|
|
});
|
|
|
|
it("resolves with renamed when the mailbox is renamed mid-wait", async () => {
|
|
store.upsertMailbox("oldname");
|
|
const ac = new AbortController();
|
|
const pending = store.waitForMessage("oldname", 5000, ac.signal);
|
|
setTimeout(() => store.rename("oldname", "newname"), 30);
|
|
const result = await pending;
|
|
expect(result.kind).toBe("renamed");
|
|
if (result.kind === "renamed") expect(result.to).toBe("newname");
|
|
});
|
|
|
|
it("FIFO single-delivery: two waiters, one send, only the first gets the message", async () => {
|
|
store.upsertMailbox("alice");
|
|
store.upsertMailbox("bob");
|
|
|
|
const ac1 = new AbortController();
|
|
const ac2 = new AbortController();
|
|
const w1 = store.waitForMessage("bob", 5000, ac1.signal);
|
|
// Stagger so w1 registers first.
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
const w2 = store.waitForMessage("bob", 200, ac2.signal);
|
|
|
|
store.send("alice", "bob", "for-w1");
|
|
|
|
const [r1, r2] = await Promise.all([w1, w2]);
|
|
expect(r1.kind).toBe("message");
|
|
if (r1.kind === "message") expect(r1.message.body).toBe("for-w1");
|
|
expect(r2.kind).toBe("timeout");
|
|
});
|
|
|
|
it("two pending messages are drained by two reconnecting waiters", async () => {
|
|
store.upsertMailbox("alice");
|
|
store.upsertMailbox("bob");
|
|
store.send("alice", "bob", "m1");
|
|
store.send("alice", "bob", "m2");
|
|
|
|
const ac = new AbortController();
|
|
const r1 = await store.waitForMessage("bob", 1000, ac.signal);
|
|
const r2 = await store.waitForMessage("bob", 1000, ac.signal);
|
|
expect(r1.kind).toBe("message");
|
|
expect(r2.kind).toBe("message");
|
|
if (r1.kind === "message" && r2.kind === "message") {
|
|
expect([r1.message.body, r2.message.body]).toEqual(["m1", "m2"]);
|
|
}
|
|
});
|
|
});
|