import { EventId, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationEvent, type OrchestrationThread, type OrchestrationThreadActivity,} from "@t3tools/contracts";import { describe, expect, it } from "vite-plus/test"; import { buildThreadFeed, type ThreadFeedActivity } from "../../mobile/src/lib/threadActivity.ts";import { deriveLatestContextWindowSnapshot } from "../../web/src/lib/contextWindow.ts";import { deriveWorkLogEntries } from "../../web/src/session-logic.ts";import { projectActivityEvent, projectActivityPayload, projectThreadDetailSnapshot,} from "../src/orchestration/ActivityPayloadProjection.ts"; function makeActivity( id: string, itemType: string, data: Record<string, unknown>,): OrchestrationThreadActivity { return { id: EventId.make(id), tone: "tool", kind: "tool.completed", summary: `Completed ${itemType}`, payload: { itemType, title: itemType, detail: `${itemType} detail`, status: "completed", requestKind: "command", data, }, turnId: TurnId.make(`turn-${id}`), createdAt: "2026-07-27T00:00:00.000Z", };} function makeThread(activities: ReadonlyArray<OrchestrationThreadActivity>): OrchestrationThread { return { id: ThreadId.make("thread-projection"), projectId: ProjectId.make("project-projection"), title: "Activity projection", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-07-27T00:00:00.000Z", updatedAt: "2026-07-27T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, deletedAt: null, messages: [], proposedPlans: [], activities, checkpoints: [], session: null, };} const fixtures = [ makeActivity("command", "command_execution", { item: { command: ["bash", "-lc", "pnpm test"], input: { command: "fallback input", ignored: "input bulk" }, result: { command: "fallback result", aggregatedOutput: "x".repeat(10_000) }, commandActions: [{ type: "unknown", output: "y".repeat(5_000) }], }, command: "fallback data", kind: "execute", toolCallId: "tool-command", rawOutput: { content: "\n```\nfirst useful line\nsecond line", stdout: "unused stdout", ignored: "raw bulk", }, ignored: "top-level bulk", }), makeActivity("file-change", "file_change", { item: { changes: [ { oldPath: "src/old.ts", newPath: "src/new.ts", patch: "large patch".repeat(1_000) }, { filePath: "src/second.ts" }, ], }, ignored: "top-level bulk", }), makeActivity("dynamic", "dynamic_tool_call", { toolCallId: "tool-dynamic", rawOutput: { stdout: "dynamic summary\nlong output".repeat(1_000), }, ignored: "top-level bulk", }), makeActivity("collab", "collab_agent_tool_call", { kind: "delegate", rawOutput: { content: "``` \n```", stdout: "must not be used when content is present", }, ignored: "top-level bulk", }), makeActivity("mcp", "mcp_tool_call", { item: { server: "repository", tool: "search", arguments: { query: "activity projection" }, aggregatedOutput: "mcp bulk is dropped", }, ignored: "top-level bulk", }), makeActivity("search", "web_search", { rawOutput: { totalFiles: 42, truncated: true, content: "ignored because totalFiles wins", }, ignored: "top-level bulk", }), makeActivity("image", "image_view", { ignored: "top-level bulk", }),] satisfies ReadonlyArray<OrchestrationThreadActivity>; describe("projectActivityPayload", () => { function comparableActivity(activity: ThreadFeedActivity) { return { ...activity, fullDetail: activity.getFullDetail(), copyText: activity.getCopyText(), getFullDetail: undefined, getCopyText: undefined, }; } function comparableThreadFeed(activities: ReadonlyArray<OrchestrationThreadActivity>) { return buildThreadFeed(makeThread(activities)).map((entry) => entry.type === "activity-group" ? { ...entry, activities: entry.activities.map(comparableActivity), } : entry, ); } it("drops unread bulk while retaining command, file, tool, and summary inputs", () => { const projected = projectActivityPayload(fixtures[0]!); expect(projected.payload).toEqual({ itemType: "command_execution", title: "command_execution", detail: "command_execution detail", status: "completed", requestKind: "command", data: { item: { command: ["bash", "-lc", "pnpm test"], input: { command: "fallback input" }, result: { command: "fallback result" }, }, command: "fallback data", toolCallId: "tool-command", kind: "execute", rawOutput: { content: "first useful line" }, }, }); expect(projectActivityPayload(fixtures[1]!).payload).toMatchObject({ data: { files: [{ path: "src/new.ts" }, { path: "src/old.ts" }, { path: "src/second.ts" }], }, }); }); it("projects a Claude Bash result for the web and mobile expanded rows", () => { const command = `printf 'first line\nsecond line'\n&& printf done`; const source: OrchestrationThreadActivity = { ...makeActivity("claude-bash", "command_execution", {}), summary: "Command run", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`, status: "completed", data: { toolName: "Bash", input: { command }, result: { type: "tool_result", tool_use_id: "toolu_1", content: [ { type: "text", text: "first output line" }, { type: "text", text: "x".repeat(5_000) }, ], }, }, }, }; const projected = projectActivityPayload(source); expect(projected.payload).toMatchObject({ data: { toolName: "Bash", command, rawOutput: { content: "first output line" }, }, }); const [webEntry] = deriveWorkLogEntries([projected]); expect(webEntry).toMatchObject({ command, detail: "first output line" }); const [mobileGroup] = buildThreadFeed(makeThread([projected])); expect(mobileGroup?.type).toBe("activity-group"); if (mobileGroup?.type !== "activity-group") return; const [mobileRow] = mobileGroup.activities; expect(mobileRow).toMatchObject({ detail: command, canExpand: true }); expect(mobileRow?.getFullDetail()).toBe(`${command}\n\nfirst output line`); expect(mobileRow?.getCopyText()).toBe(`Command run\n${command}\n\nfirst output line`); }); it("slims MCP tool data to the fields the expanded row renders", () => { expect(projectActivityPayload(fixtures[4]!).payload).toEqual({ itemType: "mcp_tool_call", title: "mcp_tool_call", detail: "mcp_tool_call detail", status: "completed", requestKind: "command", data: { item: { server: "repository", tool: "search", arguments: { query: "activity projection" }, }, }, }); }); it("keeps current web and mobile derived fields for every tool item type", () => { for (const activity of fixtures) { const projected = projectActivityPayload(activity); if (activity === fixtures[0]) { expect(deriveWorkLogEntries([projected])).toMatchObject([ { command: "pnpm test", rawCommand: 'bash -lc "pnpm test"', detail: "first useful line", }, ]); expect(comparableThreadFeed([projected])).toMatchObject([ { type: "activity-group", activities: [ { detail: "pnpm test", fullDetail: 'bash -lc "pnpm test"\n\nfirst useful line', }, ], }, ]); continue; } if (activity === fixtures[4]) { // MCP is the one deliberate difference: the expanded row's toolData // loses result bulk but keeps the rendered identity fields. const [entry] = deriveWorkLogEntries([projected]); expect(entry?.toolData).toEqual({ server: "repository", tool: "search", arguments: { query: "activity projection" }, }); continue; } expect(deriveWorkLogEntries([projected])).toEqual(deriveWorkLogEntries([activity])); expect(comparableThreadFeed([projected])).toEqual(comparableThreadFeed([activity])); } }); it("preserves failed stored tool outcomes for web and mobile clients", () => { const activities = [ makeActivity("failed-command", "command_execution", { item: { command: "vp test run", exitCode: 1, status: "failed", }, }), makeActivity("failed-mcp", "mcp_tool_call", { item: { server: "simulator", tool: "build", arguments: {}, status: "failed", }, }), ]; for (const activity of activities) { const projected = projectActivityPayload(activity); expect(projected.payload).toMatchObject({ status: "failed" }); const [webEntry] = deriveWorkLogEntries([projected]); expect(webEntry?.toolLifecycleStatus).toBe("failed"); const [mobileGroup] = buildThreadFeed(makeThread([projected])); expect(mobileGroup).toMatchObject({ type: "activity-group" }); if (mobileGroup?.type === "activity-group") { expect(mobileGroup.activities[0]?.status).toBe("failure"); } } }); it("projects snapshot and event transports without mutating their sources", () => { const activity = fixtures[0]!; const thread = makeThread([activity]); const snapshot = { snapshotSequence: 7, thread }; const projectedSnapshot = projectThreadDetailSnapshot(snapshot); expect(projectedSnapshot.thread.activities[0]).not.toBe(activity); expect(snapshot.thread.activities[0]).toBe(activity); const event = { sequence: 8, eventId: EventId.make("event-activity"), aggregateKind: "thread", aggregateId: thread.id, occurredAt: "2026-07-27T00:00:01.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, type: "thread.activity-appended", payload: { threadId: thread.id, activity, }, } satisfies Extract<OrchestrationEvent, { type: "thread.activity-appended" }>; const projectedEvent = projectActivityEvent(event); expect(projectedEvent).not.toBe(event); expect( projectedEvent.type === "thread.activity-appended" ? projectedEvent.payload.activity : undefined, ).toEqual(projectActivityPayload(activity)); expect(event.payload.activity).toBe(activity); });}); describe("superseded tool.updated snapshot dedup", () => { function makeToolLifecycleActivity( id: string, kind: "tool.updated" | "tool.completed", options: { readonly turn?: string; readonly title?: string; readonly detail?: string; readonly toolCallId?: string; } = {}, ): OrchestrationThreadActivity { const { turn = "turn-a", title = "File change", detail, toolCallId } = options; return { id: EventId.make(id), tone: "tool", kind, summary: title, payload: { itemType: "file_change", title, ...(detail ? { detail } : {}), data: { ...(toolCallId ? { toolCallId } : {}), toolName: "Edit", input: { file_path: "src/app.ts" }, }, }, turnId: TurnId.make(turn), createdAt: "2026-07-27T00:00:00.000Z", }; } function projectedIds(activities: ReadonlyArray<OrchestrationThreadActivity>) { return projectThreadDetailSnapshot({ snapshotSequence: 7, thread: makeThread(activities), }).thread.activities.map((activity) => activity.id); } it("drops updates a later completion supersedes in the same turn", () => { const update1 = makeToolLifecycleActivity("upd-1", "tool.updated"); const update2 = makeToolLifecycleActivity("upd-2", "tool.updated"); const completed = makeToolLifecycleActivity("done-1", "tool.completed"); expect(projectedIds([update1, update2, completed])).toEqual([completed.id]); }); it("matches on toolCallId when the adapter emits one", () => { const otherCall = makeToolLifecycleActivity("upd-other", "tool.updated", { toolCallId: "call-b", }); const update = makeToolLifecycleActivity("upd-a", "tool.updated", { toolCallId: "call-a" }); const completed = makeToolLifecycleActivity("done-a", "tool.completed", { toolCallId: "call-a", }); // Same itemType/title, different call: only call-a's update is superseded. expect(projectedIds([otherCall, update, completed])).toEqual([otherCall.id, completed.id]); }); it("keeps updates with no matching completion", () => { const inFlight = makeToolLifecycleActivity("upd-live", "tool.updated", { title: "Running" }); const other = makeToolLifecycleActivity("upd-other", "tool.updated", { title: "Reading" }); const completed = makeToolLifecycleActivity("done-other", "tool.completed", { title: "Reading", }); expect(projectedIds([inFlight, other, completed])).toEqual([inFlight.id, completed.id]); }); it("drops interleaved superseded updates even when a parallel call separates them", () => { // Deliberate divergence from the clients' adjacency-based collapse: a // superseded update separated from its completion by an interleaved // parallel call renders as its own in-flight row on full history, and the // snapshot omits it. Its final state still shows via the retained // completion (1.5% of dropped rows on real data; see the projection's doc // comment). const updateA = makeToolLifecycleActivity("upd-a", "tool.updated", { toolCallId: "call-a" }); const updateB = makeToolLifecycleActivity("upd-b", "tool.updated", { toolCallId: "call-b" }); const completedA = makeToolLifecycleActivity("done-a", "tool.completed", { toolCallId: "call-a", }); const completedB = makeToolLifecycleActivity("done-b", "tool.completed", { toolCallId: "call-b", }); expect(projectedIds([updateA, updateB, completedA, completedB])).toEqual([ completedA.id, completedB.id, ]); }); it("keeps an update whose completion lives in another turn", () => { // A live thread.reverted can discard the completing turn while keeping // the updating one, which would leave the call unrepresented. const update = makeToolLifecycleActivity("upd-kept", "tool.updated", { turn: "turn-kept" }); const completed = makeToolLifecycleActivity("done-later", "tool.completed", { turn: "turn-reverted", }); expect(projectedIds([update, completed])).toEqual([update.id, completed.id]); }); it("keeps an update that follows its completion", () => { // A later update under the same identity is the next call, still in flight. const completed = makeToolLifecycleActivity("done-first", "tool.completed"); const nextCall = makeToolLifecycleActivity("upd-next", "tool.updated"); expect(projectedIds([completed, nextCall])).toEqual([completed.id, nextCall.id]); }); it("keeps identity-less rows the clients never collapse", () => { const anonymous: OrchestrationThreadActivity = { id: EventId.make("upd-anon"), tone: "tool", kind: "tool.updated", summary: " ", payload: { data: { toolName: "Edit" } }, turnId: TurnId.make("turn-a"), createdAt: "2026-07-27T00:00:00.000Z", }; const completed: OrchestrationThreadActivity = { ...anonymous, id: EventId.make("done-anon"), kind: "tool.completed", }; expect(projectedIds([anonymous, completed])).toEqual([anonymous.id, completed.id]); }); it("leaves the collapsed work log identical to the full history", () => { const activities = [ makeToolLifecycleActivity("upd-1", "tool.updated", { detail: "writing" }), makeToolLifecycleActivity("upd-2", "tool.updated", { detail: "writing" }), makeToolLifecycleActivity("done-1", "tool.completed", { detail: "writing" }), ]; const projected = projectThreadDetailSnapshot({ snapshotSequence: 7, thread: makeThread(activities), });Lines 1–500 of 611. Load more
import { EventId, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationEvent, type OrchestrationThread, type OrchestrationThreadActivity, } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { buildThreadFeed, type ThreadFeedActivity } from "../../mobile/src/lib/threadActivity.ts"; import { deriveLatestContextWindowSnapshot } from "../../web/src/lib/contextWindow.ts"; import { deriveWorkLogEntries } from "../../web/src/session-logic.ts"; import { projectActivityEvent, projectActivityPayload, projectThreadDetailSnapshot, } from "../src/orchestration/ActivityPayloadProjection.ts"; function makeActivity( id: string, itemType: string, data: Record<string, unknown>, ): OrchestrationThreadActivity { return { id: EventId.make(id), tone: "tool", kind: "tool.completed", summary: `Completed ${itemType}`, payload: { itemType, title: itemType, detail: `${itemType} detail`, status: "completed", requestKind: "command", data, }, turnId: TurnId.make(`turn-${id}`), createdAt: "2026-07-27T00:00:00.000Z", }; } function makeThread(activities: ReadonlyArray<OrchestrationThreadActivity>): OrchestrationThread { return { id: ThreadId.make("thread-projection"), projectId: ProjectId.make("project-projection"), title: "Activity projection", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4", }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-07-27T00:00:00.000Z", updatedAt: "2026-07-27T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, deletedAt: null, messages: [], proposedPlans: [], activities, checkpoints: [], session: null, }; } const fixtures = [ makeActivity("command", "command_execution", { item: { command: ["bash", "-lc", "pnpm test"], input: { command: "fallback input", ignored: "input bulk" }, result: { command: "fallback result", aggregatedOutput: "x".repeat(10_000) }, commandActions: [{ type: "unknown", output: "y".repeat(5_000) }], }, command: "fallback data", kind: "execute", toolCallId: "tool-command", rawOutput: { content: "\n```\nfirst useful line\nsecond line", stdout: "unused stdout", ignored: "raw bulk", }, ignored: "top-level bulk", }), makeActivity("file-change", "file_change", { item: { changes: [ { oldPath: "src/old.ts", newPath: "src/new.ts", patch: "large patch".repeat(1_000) }, { filePath: "src/second.ts" }, ], }, ignored: "top-level bulk", }), makeActivity("dynamic", "dynamic_tool_call", { toolCallId: "tool-dynamic", rawOutput: { stdout: "dynamic summary\nlong output".repeat(1_000), }, ignored: "top-level bulk", }), makeActivity("collab", "collab_agent_tool_call", { kind: "delegate", rawOutput: { content: "``` \n```", stdout: "must not be used when content is present", }, ignored: "top-level bulk", }), makeActivity("mcp", "mcp_tool_call", { item: { server: "repository", tool: "search", arguments: { query: "activity projection" }, aggregatedOutput: "mcp bulk is dropped", }, ignored: "top-level bulk", }), makeActivity("search", "web_search", { rawOutput: { totalFiles: 42, truncated: true, content: "ignored because totalFiles wins", }, ignored: "top-level bulk", }), makeActivity("image", "image_view", { ignored: "top-level bulk", }), ] satisfies ReadonlyArray<OrchestrationThreadActivity>; describe("projectActivityPayload", () => { function comparableActivity(activity: ThreadFeedActivity) { return { ...activity, fullDetail: activity.getFullDetail(), copyText: activity.getCopyText(), getFullDetail: undefined, getCopyText: undefined, }; } function comparableThreadFeed(activities: ReadonlyArray<OrchestrationThreadActivity>) { return buildThreadFeed(makeThread(activities)).map((entry) => entry.type === "activity-group" ? { ...entry, activities: entry.activities.map(comparableActivity), } : entry, ); } it("drops unread bulk while retaining command, file, tool, and summary inputs", () => { const projected = projectActivityPayload(fixtures[0]!); expect(projected.payload).toEqual({ itemType: "command_execution", title: "command_execution", detail: "command_execution detail", status: "completed", requestKind: "command", data: { item: { command: ["bash", "-lc", "pnpm test"], input: { command: "fallback input" }, result: { command: "fallback result" }, }, command: "fallback data", toolCallId: "tool-command", kind: "execute", rawOutput: { content: "first useful line" }, }, }); expect(projectActivityPayload(fixtures[1]!).payload).toMatchObject({ data: { files: [{ path: "src/new.ts" }, { path: "src/old.ts" }, { path: "src/second.ts" }], }, }); }); it("projects a Claude Bash result for the web and mobile expanded rows", () => { const command = `printf 'first line\nsecond line'\n&& printf done`; const source: OrchestrationThreadActivity = { ...makeActivity("claude-bash", "command_execution", {}), summary: "Command run", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`, status: "completed", data: { toolName: "Bash", input: { command }, result: { type: "tool_result", tool_use_id: "toolu_1", content: [ { type: "text", text: "first output line" }, { type: "text", text: "x".repeat(5_000) }, ], }, }, }, }; const projected = projectActivityPayload(source); expect(projected.payload).toMatchObject({ data: { toolName: "Bash", command, rawOutput: { content: "first output line" }, }, }); const [webEntry] = deriveWorkLogEntries([projected]); expect(webEntry).toMatchObject({ command, detail: "first output line" }); const [mobileGroup] = buildThreadFeed(makeThread([projected])); expect(mobileGroup?.type).toBe("activity-group"); if (mobileGroup?.type !== "activity-group") return; const [mobileRow] = mobileGroup.activities; expect(mobileRow).toMatchObject({ detail: command, canExpand: true }); expect(mobileRow?.getFullDetail()).toBe(`${command}\n\nfirst output line`); expect(mobileRow?.getCopyText()).toBe(`Command run\n${command}\n\nfirst output line`); }); it("slims MCP tool data to the fields the expanded row renders", () => { expect(projectActivityPayload(fixtures[4]!).payload).toEqual({ itemType: "mcp_tool_call", title: "mcp_tool_call", detail: "mcp_tool_call detail", status: "completed", requestKind: "command", data: { item: { server: "repository", tool: "search", arguments: { query: "activity projection" }, }, }, }); }); it("keeps current web and mobile derived fields for every tool item type", () => { for (const activity of fixtures) { const projected = projectActivityPayload(activity); if (activity === fixtures[0]) { expect(deriveWorkLogEntries([projected])).toMatchObject([ { command: "pnpm test", rawCommand: 'bash -lc "pnpm test"', detail: "first useful line", }, ]); expect(comparableThreadFeed([projected])).toMatchObject([ { type: "activity-group", activities: [ { detail: "pnpm test", fullDetail: 'bash -lc "pnpm test"\n\nfirst useful line', }, ], }, ]); continue; } if (activity === fixtures[4]) { // MCP is the one deliberate difference: the expanded row's toolData // loses result bulk but keeps the rendered identity fields. const [entry] = deriveWorkLogEntries([projected]); expect(entry?.toolData).toEqual({ server: "repository", tool: "search", arguments: { query: "activity projection" }, }); continue; } expect(deriveWorkLogEntries([projected])).toEqual(deriveWorkLogEntries([activity])); expect(comparableThreadFeed([projected])).toEqual(comparableThreadFeed([activity])); } }); it("preserves failed stored tool outcomes for web and mobile clients", () => { const activities = [ makeActivity("failed-command", "command_execution", { item: { command: "vp test run", exitCode: 1, status: "failed", }, }), makeActivity("failed-mcp", "mcp_tool_call", { item: { server: "simulator", tool: "build", arguments: {}, status: "failed", }, }), ]; for (const activity of activities) { const projected = projectActivityPayload(activity); expect(projected.payload).toMatchObject({ status: "failed" }); const [webEntry] = deriveWorkLogEntries([projected]); expect(webEntry?.toolLifecycleStatus).toBe("failed"); const [mobileGroup] = buildThreadFeed(makeThread([projected])); expect(mobileGroup).toMatchObject({ type: "activity-group" }); if (mobileGroup?.type === "activity-group") { expect(mobileGroup.activities[0]?.status).toBe("failure"); } } }); it("projects snapshot and event transports without mutating their sources", () => { const activity = fixtures[0]!; const thread = makeThread([activity]); const snapshot = { snapshotSequence: 7, thread }; const projectedSnapshot = projectThreadDetailSnapshot(snapshot); expect(projectedSnapshot.thread.activities[0]).not.toBe(activity); expect(snapshot.thread.activities[0]).toBe(activity); const event = { sequence: 8, eventId: EventId.make("event-activity"), aggregateKind: "thread", aggregateId: thread.id, occurredAt: "2026-07-27T00:00:01.000Z", commandId: null, causationEventId: null, correlationId: null, metadata: {}, type: "thread.activity-appended", payload: { threadId: thread.id, activity, }, } satisfies Extract<OrchestrationEvent, { type: "thread.activity-appended" }>; const projectedEvent = projectActivityEvent(event); expect(projectedEvent).not.toBe(event); expect( projectedEvent.type === "thread.activity-appended" ? projectedEvent.payload.activity : undefined, ).toEqual(projectActivityPayload(activity)); expect(event.payload.activity).toBe(activity); }); }); describe("superseded tool.updated snapshot dedup", () => { function makeToolLifecycleActivity( id: string, kind: "tool.updated" | "tool.completed", options: { readonly turn?: string; readonly title?: string; readonly detail?: string; readonly toolCallId?: string; } = {}, ): OrchestrationThreadActivity { const { turn = "turn-a", title = "File change", detail, toolCallId } = options; return { id: EventId.make(id), tone: "tool", kind, summary: title, payload: { itemType: "file_change", title, ...(detail ? { detail } : {}), data: { ...(toolCallId ? { toolCallId } : {}), toolName: "Edit", input: { file_path: "src/app.ts" }, }, }, turnId: TurnId.make(turn), createdAt: "2026-07-27T00:00:00.000Z", }; } function projectedIds(activities: ReadonlyArray<OrchestrationThreadActivity>) { return projectThreadDetailSnapshot({ snapshotSequence: 7, thread: makeThread(activities), }).thread.activities.map((activity) => activity.id); } it("drops updates a later completion supersedes in the same turn", () => { const update1 = makeToolLifecycleActivity("upd-1", "tool.updated"); const update2 = makeToolLifecycleActivity("upd-2", "tool.updated"); const completed = makeToolLifecycleActivity("done-1", "tool.completed"); expect(projectedIds([update1, update2, completed])).toEqual([completed.id]); }); it("matches on toolCallId when the adapter emits one", () => { const otherCall = makeToolLifecycleActivity("upd-other", "tool.updated", { toolCallId: "call-b", }); const update = makeToolLifecycleActivity("upd-a", "tool.updated", { toolCallId: "call-a" }); const completed = makeToolLifecycleActivity("done-a", "tool.completed", { toolCallId: "call-a", }); // Same itemType/title, different call: only call-a's update is superseded. expect(projectedIds([otherCall, update, completed])).toEqual([otherCall.id, completed.id]); }); it("keeps updates with no matching completion", () => { const inFlight = makeToolLifecycleActivity("upd-live", "tool.updated", { title: "Running" }); const other = makeToolLifecycleActivity("upd-other", "tool.updated", { title: "Reading" }); const completed = makeToolLifecycleActivity("done-other", "tool.completed", { title: "Reading", }); expect(projectedIds([inFlight, other, completed])).toEqual([inFlight.id, completed.id]); }); it("drops interleaved superseded updates even when a parallel call separates them", () => { // Deliberate divergence from the clients' adjacency-based collapse: a // superseded update separated from its completion by an interleaved // parallel call renders as its own in-flight row on full history, and the // snapshot omits it. Its final state still shows via the retained // completion (1.5% of dropped rows on real data; see the projection's doc // comment). const updateA = makeToolLifecycleActivity("upd-a", "tool.updated", { toolCallId: "call-a" }); const updateB = makeToolLifecycleActivity("upd-b", "tool.updated", { toolCallId: "call-b" }); const completedA = makeToolLifecycleActivity("done-a", "tool.completed", { toolCallId: "call-a", }); const completedB = makeToolLifecycleActivity("done-b", "tool.completed", { toolCallId: "call-b", }); expect(projectedIds([updateA, updateB, completedA, completedB])).toEqual([ completedA.id, completedB.id, ]); }); it("keeps an update whose completion lives in another turn", () => { // A live thread.reverted can discard the completing turn while keeping // the updating one, which would leave the call unrepresented. const update = makeToolLifecycleActivity("upd-kept", "tool.updated", { turn: "turn-kept" }); const completed = makeToolLifecycleActivity("done-later", "tool.completed", { turn: "turn-reverted", }); expect(projectedIds([update, completed])).toEqual([update.id, completed.id]); }); it("keeps an update that follows its completion", () => { // A later update under the same identity is the next call, still in flight. const completed = makeToolLifecycleActivity("done-first", "tool.completed"); const nextCall = makeToolLifecycleActivity("upd-next", "tool.updated"); expect(projectedIds([completed, nextCall])).toEqual([completed.id, nextCall.id]); }); it("keeps identity-less rows the clients never collapse", () => { const anonymous: OrchestrationThreadActivity = { id: EventId.make("upd-anon"), tone: "tool", kind: "tool.updated", summary: " ", payload: { data: { toolName: "Edit" } }, turnId: TurnId.make("turn-a"), createdAt: "2026-07-27T00:00:00.000Z", }; const completed: OrchestrationThreadActivity = { ...anonymous, id: EventId.make("done-anon"), kind: "tool.completed", }; expect(projectedIds([anonymous, completed])).toEqual([anonymous.id, completed.id]); }); it("leaves the collapsed work log identical to the full history", () => { const activities = [ makeToolLifecycleActivity("upd-1", "tool.updated", { detail: "writing" }), makeToolLifecycleActivity("upd-2", "tool.updated", { detail: "writing" }), makeToolLifecycleActivity("done-1", "tool.completed", { detail: "writing" }), ]; const projected = projectThreadDetailSnapshot({ snapshotSequence: 7, thread: makeThread(activities), });