import { assertEquals } from "@std/assert"; import type { SystemMessage, Tool } from "@earendil-works/pi-ai"; import type { ConversationId, EntryId, EntryRecord } from "@earendil-works/pi-durable"; import { withDom } from "@berry/util/dom.ts"; const record = (id: number, patch: Partial): EntryRecord => ({ id: id as EntryId, conversationId: 1 as ConversationId, kind: "pi.system", model: [{ role: "system", content: "", timestamp: id, ...patch }], }); Deno.test("system summaries show net tool changes, not replay churn", async (t) => { await withDom(async () => { const { transcript } = await import("./transcript.tsx"); const tool = (name: string, description = name): Tool => ({ name, description, parameters: { type: "object", properties: {} }, }); const codemode = tool("codemode"); const read = tool("read"); const write = tool("write"); const baseline = record(1, { toolsAdded: [codemode, read, write] }); const cases: { name: string; patch: Partial; expected: string }[] = [ { name: "unchanged remove/re-add pairs disappear but additions remain", patch: { sections: { berry: "guide", codemode: "catalog" }, toolsAdded: [tool("subagent"), codemode, read, write], toolsRemoved: [codemode, read, write], }, expected: "prompt sections +berry +codemode tools +subagent", }, { name: "a changed declaration is shown once", patch: { toolsAdded: [tool("codemode", "updated"), read, write], toolsRemoved: [codemode, read, write], }, expected: "prompt tools +codemode", }, { name: "real removals remain visible", patch: { toolsAdded: [codemode, read], toolsRemoved: [codemode, read, write], }, expected: "prompt tools -write", }, { name: "reordering alone is not a tool interface change", patch: { sections: { berry: "guide" }, toolsAdded: [write, read, codemode], toolsRemoved: [codemode, read, write], }, expected: "prompt sections +berry", }, ]; for (const { name, patch, expected } of cases) { await t.step(name, () => { const view = transcript(); try { view.update([baseline], {}); view.update([baseline, record(2, patch)], {}); const summaries = view.element.querySelectorAll(".system summary"); assertEquals(summaries[0].textContent, "prompt tools +codemode +read +write"); assertEquals(summaries[1].textContent, expected); } finally { view.dispose(); } }); } await t.step("summaries are recalculated when earlier context changes", () => { const view = transcript(); try { const updatedTools = [tool("codemode", "updated"), read, write]; const update = record(2, { toolsAdded: updatedTools, toolsRemoved: [codemode, read, write], }); view.update([baseline, update], {}); const summary = view.element.querySelectorAll(".system summary")[1]; assertEquals(summary.textContent, "prompt tools +codemode"); view.update([ record(3, { toolsAdded: updatedTools }), update, ], {}); assertEquals(view.element.querySelectorAll(".system summary")[1], summary); assertEquals(summary.textContent, "prompt"); } finally { view.dispose(); } }); }); }); Deno.test("section summaries compare the net effect of a reorder's patches", async (t) => { await withDom(async () => { const { transcript } = await import("./transcript.tsx"); const sections = { berry: "guide", code: "coding", web: "browsing" }; const baseline = record(1, { sections }); const removal = record(2, { sections: { berry: null, code: null, web: null } }); const cases: { name: string; sections: Record; expected: string[] }[] = [ { name: "inserting a section does not report unchanged sections", sections: { berry: "guide", code: "coding", code_project: "project", web: "browsing" }, expected: ["+code_project"], }, { name: "changed values and real removals survive replay churn", sections: { code: "updated coding", berry: "guide" }, expected: ["code", "-web"], }, { name: "reordering alone does not report content changes", sections: { web: "browsing", code: "coding", berry: "guide" }, expected: [], }, ]; for (const { name, sections, expected } of cases) { await t.step(name, () => { const view = transcript(); try { view.update([baseline, removal, record(3, { timestamp: 2, sections })], {}); const summaries = view.element.querySelectorAll(".system summary"); assertEquals(summaries.length, 2); assertEquals( Array.from(summaries[1].querySelectorAll(".system-name"), (name) => name.textContent), expected, ); } finally { view.dispose(); } }); } await t.step("split delivery and rewinds update the same summary and its expanded body", () => { const view = transcript(); try { const addition = record(3, { timestamp: 2, sections: { ...sections, code_project: "project" }, }); view.update([baseline, removal], {}); const details = view.element.querySelectorAll(".system")[1]; const summary = details.querySelector("summary")!; details.open = true; details.dispatchEvent(new Event("toggle")); view.update([baseline, removal, addition], {}); assertEquals(view.element.querySelectorAll(".system").length, 2); assertEquals(details.querySelector("summary"), summary); assertEquals(summary.textContent, "prompt sections +code_project"); assertEquals(Array.from(details.querySelectorAll("pre"), (pre) => pre.textContent), [ "guide", "coding", "browsing", "project", ]); assertEquals(details.open, true); view.update([baseline, removal], {}); assertEquals(summary.textContent, "prompt sections -berry -code -web"); assertEquals(details.querySelectorAll("pre").length, 0); view.update([ record(4, { sections: { ...sections, code_project: "project" } }), removal, addition, ], {}); assertEquals(details.querySelector("summary"), summary); assertEquals(summary.textContent, "prompt"); } finally { view.dispose(); } }); await t.step("an intervening message prevents unrelated changes from cancelling out", () => { const view = transcript(); try { const user: EntryRecord = { id: 3 as EntryId, conversationId: 1 as ConversationId, kind: "pi.user", model: [{ role: "user", content: "continue", timestamp: 2 }], }; view.update([baseline, removal, user, record(4, { timestamp: 2, sections })], {}); assertEquals( Array.from(view.element.querySelectorAll(".system summary"), (node) => node.textContent), [ "prompt sections +berry +code +web", "prompt sections -berry -code -web", "prompt sections +berry +code +web", ], ); } finally { view.dispose(); } }); }); });