Files
Gabriel Brown a6e8bf7b28
Build and Push agentchat Image / quality (push) Successful in 35s
Build and Push agentchat Image / build-image (push) Failing after 24s
Redesign web UI with console-style squircle message cards
Chat messages render as soft-shadow squircle cards (true superellipse
corners where supported) with per-agent color avatars, a route pill,
and a per-message Copy button (hover on desktop, always visible on
touch). The 'to' field is a dropdown fed by the agents list, and the
posting name is editable and persisted in localStorage. Setup tab
matches the same look with a copyable install command.
2026-08-13 10:29:06 -04:00

131 lines
5.5 KiB
TypeScript

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<Response> {
return handle(
new Request(`http://test${path}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
);
}
async function getJson<T>(path: string): Promise<T> {
const response = await handle(new Request(`http://test${path}`));
expect(response.status).toBe(200);
return response.json() as Promise<T>;
}
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<Message[]>("/api/messages?for=vps");
expect(inbox.map((m) => m.body)).toContain("deploy done?");
const senderInbox = await getJson<Message[]>("/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<Message[]>("/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<Message[]>(`/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<Message[]>("/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<Message[]>("/api/messages?for=b");
expect(active.map((m) => m.body)).not.toContain("stale");
const archived = await getJson<Message[]>("/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<Message[]>("/api/messages")).toEqual([]);
expect(await getJson<Message[]>("/api/messages?for=b")).toEqual([]);
const archived = await getJson<Message[]>("/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<Message[]>("/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"');
expect(chat).toContain('id="to"');
expect(chat).toContain('id="myname"');
});
});
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}}");
});
});