Add web chat tab, 24h archive policy, and clear-chat
Build and Push agentchat Image / quality (push) Successful in 8s
Build and Push agentchat Image / build-image (push) Successful in 4m57s

Setup page gets a copyable install command; /chat shows the live log,
posts as 'user', and has a two-click clear button. Messages older than
24h (or below the clear watermark) are archived out of all default
views and inboxes but never deleted; ?archived=1 retrieves them.
This commit is contained in:
Gabriel Brown
2026-08-13 10:01:42 -04:00
parent 0564e34372
commit 2c6626bd0e
6 changed files with 250 additions and 55 deletions
+43
View File
@@ -2,6 +2,7 @@ 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(
@@ -74,6 +75,48 @@ describe("agents", () => {
});
});
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"');
});
});
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"));