import { describe, expect, test } from "bun:test"; process.env.AGENTCHAT_DB = ":memory:"; const { handle } = await import("../src/server"); const { db } = await import("../src/db"); async function post(path: string, body: unknown): Promise { return handle( new Request(`http://test${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }), ); } async function getJson(path: string): Promise { const response = await handle(new Request(`http://test${path}`)); expect(response.status).toBe(200); return response.json() as Promise; } type Message = { id: number; from: string; to: string | null; body: string }; describe("messages", () => { test("direct message reaches its recipient but not the sender's inbox", async () => { const sent = await post("/api/messages", { from: "desktop", to: "vps", body: "deploy done?" }); expect(sent.status).toBe(201); const inbox = await getJson("/api/messages?for=vps"); expect(inbox.map((m) => m.body)).toContain("deploy done?"); const senderInbox = await getJson("/api/messages?for=desktop"); expect(senderInbox.map((m) => m.body)).not.toContain("deploy done?"); }); test("broadcast (no `to`) reaches other agents", async () => { await post("/api/messages", { from: "desktop", body: "heads up everyone" }); const inbox = await getJson("/api/messages?for=server"); const broadcast = inbox.find((m) => m.body === "heads up everyone"); expect(broadcast?.to).toBeNull(); }); test("since filters out already-seen messages", async () => { const first = (await (await post("/api/messages", { from: "a", to: "b", body: "first" })).json()) as Message; await post("/api/messages", { from: "a", to: "b", body: "second" }); const inbox = await getJson(`/api/messages?for=b&since=${first.id}`); expect(inbox.map((m) => m.body)).toEqual(["second"]); }); test("rejects missing from/body", async () => { expect((await post("/api/messages", { from: "a" })).status).toBe(400); expect((await post("/api/messages", { body: "hi" })).status).toBe(400); expect((await post("/api/messages", { from: " ", body: " " })).status).toBe(400); }); }); describe("agents", () => { test("senders and readers are tracked", async () => { await post("/api/messages", { from: "desktop", to: "vps", body: "hi" }); await getJson("/api/messages?for=laptop"); const agents = await getJson<{ name: string }[]>("/api/agents"); const names = agents.map((agent) => agent.name); expect(names).toContain("desktop"); expect(names).toContain("laptop"); }); test("explicit check-in registers an agent", async () => { const response = await post("/api/agents", { name: "pi", machine: "raspberry pi 5" }); expect(response.status).toBe(200); expect(((await response.json()) as { machine: string }).machine).toBe("raspberry pi 5"); }); }); describe("archive and clear", () => { test("messages older than 24h disappear from active views but stay in the archive", async () => { const old = (await (await post("/api/messages", { from: "a", to: "b", body: "stale" })).json()) as Message; db.query(`UPDATE messages SET ts = strftime('%Y-%m-%dT%H:%M:%SZ', 'now', '-25 hours') WHERE id = $id`).run({ $id: old.id, }); const active = await getJson("/api/messages?for=b"); expect(active.map((m) => m.body)).not.toContain("stale"); const archived = await getJson("/api/messages?archived=1"); expect(archived.map((m) => m.body)).toContain("stale"); }); test("clear archives all active messages for everyone", async () => { await post("/api/messages", { from: "a", to: "b", body: "about to be cleared" }); const response = await handle(new Request("http://test/api/messages/clear", { method: "POST" })); expect(response.status).toBe(200); expect(((await response.json()) as { cleared: number }).cleared).toBeGreaterThanOrEqual(1); expect(await getJson("/api/messages")).toEqual([]); expect(await getJson("/api/messages?for=b")).toEqual([]); const archived = await getJson("/api/messages?archived=1&limit=500"); expect(archived.map((m) => m.body)).toContain("about to be cleared"); const fresh = (await (await post("/api/messages", { from: "a", to: "b", body: "after the clear" })).json()) as Message; expect((await getJson("/api/messages?for=b")).map((m) => m.id)).toEqual([fresh.id]); }); }); describe("web pages", () => { test("setup page shows the install command and chat page has the user form", async () => { const setup = await (await handle(new Request("http://chat.example.com/"))).text(); expect(setup).toContain("curl -fsSL http://chat.example.com/install.sh | sh"); const chat = await (await handle(new Request("http://chat.example.com/chat"))).text(); expect(chat).toContain('id="send"'); expect(chat).toContain('id="clear"'); }); }); describe("served assets", () => { test("skill.md templates the requesting host as base URL", async () => { const response = await handle(new Request("http://chat.example.com/skill.md")); const text = await response.text(); expect(response.status).toBe(200); expect(text).toContain("http://chat.example.com/api/messages"); expect(text).not.toContain("{{BASE_URL}}"); }); });