| 1 | import assert from "node:assert/strict"; |
| 2 | import test from "node:test"; |
| 3 | import { TranscriptStore } from "../lib/transcriptStore"; |
| 4 | import type { Item } from "../lib/useController"; |
| 5 | import { FakeBackend } from "./helpers/transcriptFakeBackend"; |
| 6 | |
| 7 | const toolsOf = (items: Item[]) => items.filter((item): item is Extract<Item, { kind: "tool" }> => item.kind === "tool"); |
| 8 | |
| 9 | test("a persisted background-output read does not restart when its job snapshot was running", () => { |
| 10 | const store = new TranscriptStore(new FakeBackend([])); |
| 11 | const projection = store.installSlice("poll", "session-id:poll", { |
| 12 | entries: [ |
| 13 | { entryId: "m:call", turn: 1, order: 0, message: { role: "assistant", content: "", toolCalls: [{ id: "poll", name: "job_output", arguments: "{}", resultObservation: { state: "completed", messageId: "result" } }] }, refs: [] }, |
| 14 | { entryId: "m:result", turn: 1, order: 1, message: { role: "tool", toolCallId: "poll", toolName: "job_output", messageId: "result", content: "[status: running]", execution: { state: "running" } }, refs: [] }, |
| 15 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 16 | revision: 1, revisionKnown: true, digest: "poll", stale: false, |
| 17 | }); |
| 18 | assert.equal(toolsOf(projection.items)[0].status, "done"); |
| 19 | }); |
| 20 | |
| 21 | test("formal message aliases retain one canonical result", () => { |
| 22 | const store = new TranscriptStore(new FakeBackend([])); |
| 23 | const projection = store.installSlice("formal-representation-alias", "/formal-representation-alias", { |
| 24 | entries: [ |
| 25 | { entryId: "m:owner", turn: 1, order: 0, message: { role: "assistant", messageId: "owner", content: "", toolCalls: [{ |
| 26 | id: "call", name: "bash", arguments: "echo ok", resultObservation: { messageId: "result", state: "completed" }, |
| 27 | }] }, refs: [] }, |
| 28 | { entryId: "m:result", turn: 1, order: 1, message: { role: "tool", messageId: "result", toolCallId: "call", content: "ok" }, refs: [] }, |
| 29 | { entryId: "snapshot:result", turn: 1, order: 2, message: { role: "tool", messageId: "result", toolCallId: "call", content: "stale duplicate" }, refs: [] }, |
| 30 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 31 | revision: 1, revisionKnown: true, digest: "formal-alias", stale: false, |
| 32 | }); |
| 33 | const tools = toolsOf(projection.items); |
| 34 | assert.equal(tools.length, 1); |
| 35 | assert.equal(tools[0].id, "call"); |
| 36 | assert.equal(tools[0].output, "ok"); |
| 37 | }); |
| 38 | |
| 39 | test("conflicting formal identities stay visible and independently readable", async () => { |
| 40 | const store = new TranscriptStore(new FakeBackend([])); |
| 41 | const projection = store.installSlice("identity-conflict", "/identity-conflict", { |
| 42 | entries: [ |
| 43 | { entryId: "m:result-a", turn: 1, order: 0, message: { role: "tool", messageId: "result-a", toolCallId: "conflict", content: "a" }, refs: [] }, |
| 44 | { entryId: "m:result-b", turn: 1, order: 1, message: { role: "tool", messageId: "result-b", toolCallId: "conflict", content: "b" }, refs: [] }, |
| 45 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 46 | revision: 1, revisionKnown: true, digest: "conflict", stale: false, |
| 47 | }); |
| 48 | const tools = toolsOf(projection.items); |
| 49 | assert.equal(store.residentWindowEntries(), 2); |
| 50 | assert.equal(tools.length, 2); |
| 51 | assert.equal(new Set(tools.map(item => item.id)).size, 2); |
| 52 | assert.ok(tools.every(item => item.identityConflict)); |
| 53 | const details = await Promise.all(tools.map(item => store.requestToolContent("identity-conflict", item, { output: item.output }))); |
| 54 | assert.deepEqual(details.map(detail => JSON.parse(detail!).output).sort(), ["a", "b"]); |
| 55 | }); |
| 56 | |
| 57 | test("an ambiguous call and both formal results remain distinct", () => { |
| 58 | const store = new TranscriptStore(new FakeBackend([])); |
| 59 | const projection = store.installSlice("call-conflict", "/call-conflict", { |
| 60 | entries: [ |
| 61 | { entryId: "m:owner", turn: 1, order: 0, message: { role: "assistant", messageId: "owner", content: "", toolCalls: [{ id: "conflict", name: "bash", arguments: "echo ok" }] }, refs: [] }, |
| 62 | { entryId: "m:result-a", turn: 1, order: 1, message: { role: "tool", messageId: "result-a", toolCallId: "conflict", content: "a" }, refs: [] }, |
| 63 | { entryId: "m:result-b", turn: 1, order: 2, message: { role: "tool", messageId: "result-b", toolCallId: "conflict", content: "b" }, refs: [] }, |
| 64 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 65 | revision: 1, revisionKnown: true, digest: "call-conflict", stale: false, |
| 66 | }); |
| 67 | const tools = toolsOf(projection.items); |
| 68 | assert.equal(tools.length, 3); |
| 69 | assert.equal(new Set(tools.map(item => item.id)).size, 3); |
| 70 | assert.ok(tools.every(item => item.identityConflict)); |
| 71 | }); |
| 72 | |
| 73 | test("a reused call id stays isolated by turn", () => { |
| 74 | const store = new TranscriptStore(new FakeBackend([])); |
| 75 | const projection = store.installSlice("reused-call", "/reused-call", { |
| 76 | entries: [ |
| 77 | { entryId: "m:owner-1", turn: 1, order: 0, message: { role: "assistant", messageId: "owner-1", content: "", toolCalls: [{ id: "reused", name: "bash", arguments: "one" }] }, refs: [] }, |
| 78 | { entryId: "m:result-1", turn: 1, order: 1, message: { role: "tool", messageId: "result-1", toolCallId: "reused", content: "first" }, refs: [] }, |
| 79 | { entryId: "m:owner-2", turn: 2, order: 2, message: { role: "assistant", messageId: "owner-2", content: "", toolCalls: [{ id: "reused", name: "bash", arguments: "two" }] }, refs: [] }, |
| 80 | { entryId: "m:result-2", turn: 2, order: 3, message: { role: "tool", messageId: "result-2", toolCallId: "reused", content: "second" }, refs: [] }, |
| 81 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 2, startTurn: 1, endTurn: 2, |
| 82 | revision: 1, revisionKnown: true, digest: "reused-call", stale: false, |
| 83 | }); |
| 84 | const tools = toolsOf(projection.items); |
| 85 | assert.equal(tools.length, 2); |
| 86 | assert.equal(new Set(tools.map(item => item.id)).size, 2); |
| 87 | assert.deepEqual(tools.map(item => [item.args, item.output]), [["one", "first"], ["two", "second"]]); |
| 88 | assert.ok(tools.every(item => !item.identityConflict)); |
| 89 | }); |
| 90 | |
| 91 | test("paging a formal result replaces its resident event alias", async () => { |
| 92 | const backend = new FakeBackend([]); |
| 93 | backend.HistorySliceForTab = async () => ({ |
| 94 | entries: [ |
| 95 | { entryId: "m:result", turn: 1, order: 1, message: { role: "tool", messageId: "result", toolCallId: "page-call", toolName: "write_file", content: "full", execution: { state: "completed", durationMs: 99 } }, refs: [] }, |
| 96 | { entryId: "m:final", turn: 1, order: 2, message: { role: "assistant", messageId: "final", content: "done", turnFinal: true }, refs: [] }, |
| 97 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 98 | revision: 1, revisionKnown: true, digest: "page-alias", stale: false, |
| 99 | }); |
| 100 | const store = new TranscriptStore(backend); |
| 101 | store.installSlice("page-alias", "/page-alias", { |
| 102 | entries: [ |
| 103 | { entryId: "m:owner", turn: 1, order: 0, message: { role: "assistant", messageId: "owner", content: "", toolCalls: [{ id: "page-call", name: "write_file", arguments: '{"path":"page.html"}' }] }, refs: [] }, |
| 104 | { entryId: "tool:page-call", turn: 1, order: 1, message: { role: "tool", toolCallId: "page-call", toolName: "write_file", content: "preview" }, refs: [] }, |
| 105 | ], nextCursor: "", newerCursor: "next", hasOlder: false, hasNewer: true, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 106 | revision: 1, revisionKnown: true, digest: "page-alias", stale: false, |
| 107 | }); |
| 108 | const tools = toolsOf((await store.loadNewer("page-alias", "/page-alias"))!.items); |
| 109 | assert.equal(tools.length, 1); |
| 110 | assert.equal(tools[0].args, '{"path":"page.html"}'); |
| 111 | assert.equal(tools[0].execution?.durationMs, 99); |
| 112 | }); |
| 113 | |
| 114 | test("a lazy formal result joins through its observation before body load", () => { |
| 115 | const store = new TranscriptStore(new FakeBackend([])); |
| 116 | const projection = store.installSlice("lazy-result-identity", "/lazy-result-identity", { |
| 117 | entries: [ |
| 118 | { entryId: "m:owner", turn: 1, order: 0, message: { role: "assistant", messageId: "owner", content: "", toolCalls: [{ |
| 119 | id: "lazy-result", name: "write_file", arguments: '{"path":"lazy.html"}', |
| 120 | resultObservation: { state: "completed", messageId: "result", version: 1, contentRef: { digest: "body", bytes: 8192 } }, |
| 121 | }] }, refs: [] }, |
| 122 | { entryId: "m:result", turn: 1, order: 1, message: { role: "tool", messageId: "result", content: "written" }, |
| 123 | refs: [{ entryId: "m:result", field: "canonicalMessage", size: 8192, chunks: 1, revision: 1, digest: "body" }] }, |
| 124 | { entryId: "m:final", turn: 1, order: 2, message: { role: "assistant", messageId: "final", content: "done", turnFinal: true }, refs: [] }, |
| 125 | { entryId: "tool:lazy-result", turn: 1, order: 3, message: { role: "tool", toolCallId: "lazy-result", toolName: "write_file", content: "written" }, refs: [] }, |
| 126 | ], nextCursor: "", newerCursor: "", hasOlder: false, hasNewer: false, totalTurns: 1, startTurn: 1, endTurn: 1, |
| 127 | revision: 1, revisionKnown: true, digest: "lazy-result", stale: false, |
| 128 | }); |
| 129 | const tools = toolsOf(projection.items); |
| 130 | assert.equal(tools.length, 1); |
| 131 | assert.equal(tools[0].id, "lazy-result"); |
| 132 | assert.ok(projection.items.findIndex(item => item.id === "lazy-result") < projection.items.findIndex(item => item.id === "m:final")); |
| 133 | }); |
| 134 |