import { describe, expect, it } from "vite-plus/test";import { codexFeedbackMessage } from "@t3tools/client-runtime/state/threads"; import { EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationThread, type OrchestrationThreadActivity,} from "@t3tools/contracts"; import { buildPendingUserInputAnswers, buildThreadFeed, derivePendingApprovals, deriveThreadFeedPresentation, isPendingUserInputOptionSelected, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type ThreadFeedActivity, type ThreadFeedEntry,} from "./threadActivity"; describe("Codex feedback pseudo-messages", () => { it("keeps pending and completed feedback messages in the mobile thread body", () => { const pending = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:00.000Z", status: "uploading" as const, }; const entries = [codexFeedbackMessage(pending), codexFeedbackMessage(pending, "assistant")].map( (message) => ({ type: "message" as const, id: message.id, createdAt: message.createdAt, message, }), ); expect(deriveThreadFeedPresentation(entries, null, new Set())).toEqual(entries); expect(entries[1]?.message.text).toBe("Sending feedback to OpenAI..."); const completed = codexFeedbackMessage( { ...pending, status: "sent", feedbackId: "codex-thread-1" }, "assistant", ); expect(completed.text).toContain("codex-thread-1"); });}); const singleSelectQuestion = { id: "runtime", header: "Runtime", question: "Which runtime should be used?", options: [ { label: "Go", description: "One binary" }, { label: "Node.js", description: "Reuse TypeScript" }, ], multiSelect: false,} as const; const multiSelectQuestion = { id: "scope", header: "Scope", question: "Which data should be collected?", options: [ { label: "Orders", description: "Receipts" }, { label: "Listings", description: "Inventory" }, ], multiSelect: true,} as const; describe("pending user input answers", () => { it("replaces single-select options and toggles multi-select options", () => { expect( togglePendingUserInputOptionSelection( singleSelectQuestion, { selectedOptionLabels: ["Go"] }, "Node.js", ), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Node.js"] }); const orders = togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Orders"); const ordersAndListings = togglePendingUserInputOptionSelection( multiSelectQuestion, orders, "Listings", ); expect(ordersAndListings).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders", "Listings"], }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, ordersAndListings, "Orders"), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Listings"] }); const paddedOrders = togglePendingUserInputOptionSelection( multiSelectQuestion, undefined, " Orders ", ); expect(paddedOrders).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders"] }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, paddedOrders, " Orders "), ).toEqual({ customAnswer: "" }); }); it("builds array answers for multi-select questions", () => { expect( buildPendingUserInputAnswers([singleSelectQuestion, multiSelectQuestion], { runtime: { selectedOptionLabels: ["Go"] }, scope: { selectedOptionLabels: ["Orders", "Listings"] }, }), ).toEqual({ runtime: "Go", scope: ["Orders", "Listings"], }); }); it("clears selected options while a custom answer is active", () => { expect( setPendingUserInputCustomAnswer( { selectedOptionLabels: ["Orders", "Listings"] }, "Orders first", ), ).toEqual({ customAnswer: "Orders first" }); }); it("matches selected chips against normalized option labels", () => { expect( isPendingUserInputOptionSelected({ selectedOptionLabels: ["Orders"] }, " Orders "), ).toBe(true); expect( isPendingUserInputOptionSelected( { selectedOptionLabels: ["Orders"], customAnswer: "Orders first" }, " Orders ", ), ).toBe(false); });}); describe("pending approvals", () => { it("keeps app access approvals and persistence choices from remote environments", () => { const options = [ { decision: "decline", label: "Decline" }, { decision: "acceptAlways", label: "Always allow Safari" }, { decision: "accept", label: "Approve" }, ]; const activity = makeActivity({ id: EventId.make("approval-safari"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestType: "mcp_elicitation_approval", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, }); expect(derivePendingApprovals([activity])).toEqual([ { requestId: "req-safari", requestKind: "mcp-elicitation", createdAt: "2026-08-24T00:00:00.000Z", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, ]); }); it("removes an app access approval after a remote client rejects it", () => { const requested = makeActivity({ id: EventId.make("approval-safari-open"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestKind: "mcp-elicitation" }, }); const resolved = makeActivity({ id: EventId.make("approval-safari-resolved"), kind: "approval.resolved", summary: "Approval resolved", createdAt: "2026-08-24T00:00:01.000Z", payload: { requestId: "req-safari", decision: "decline" }, }); expect(derivePendingApprovals([requested, resolved])).toEqual([]); });}); function makeActivity( input: Partial<OrchestrationThreadActivity> & Pick<OrchestrationThreadActivity, "id" | "kind" | "summary" | "createdAt">,): OrchestrationThreadActivity { return { tone: "info", payload: {}, turnId: null, ...input, };} function makeThread( input: Partial<OrchestrationThread> & Pick<OrchestrationThread, "id" | "projectId" | "title">,): OrchestrationThread { return { modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null, ...input, settledOverride: input.settledOverride ?? null, settledAt: input.settledAt ?? null, };} describe("buildThreadFeed", () => { it("keeps setup failures visible without routine setup notices before or after a turn", () => { const thread = makeThread({ id: ThreadId.make("thread-worktree-setup"), projectId: ProjectId.make("project-1"), title: "Worktree setup", activities: [ makeActivity({ id: EventId.make("setup-requested"), kind: "setup-script.requested", summary: "Starting setup script", createdAt: "2026-08-30T00:00:00.000Z", }), makeActivity({ id: EventId.make("setup-started"), kind: "setup-script.started", summary: "Setup script started", createdAt: "2026-08-30T00:00:01.000Z", }), makeActivity({ id: EventId.make("setup-failed"), kind: "setup-script.failed", summary: "Setup script failed to start", createdAt: "2026-08-30T00:00:02.000Z", tone: "error", payload: { detail: "Setup command was not found" }, }), ], }); const latestTurn = { turnId: TurnId.make("turn-after-setup"), state: "running" as const, requestedAt: "2026-08-30T00:00:03.000Z", startedAt: "2026-08-30T00:00:04.000Z", completedAt: null, assistantMessageId: null, }; for (const currentTurn of [null, latestTurn]) { const feed = buildThreadFeed({ ...thread, latestTurn: currentTurn }); expect(feed).toMatchObject([ { type: "activity-group", activities: [{ id: "setup-failed", status: "failure" }], }, ]); const group = feed[0]; if (group?.type !== "activity-group") throw new Error("Expected the setup failure group"); expect(group.activities[0]?.getCopyText()).toContain("Setup command was not found"); } }); it.each(["setup-script.requested", "setup-script.started"])( "keeps error-toned %s notices visible", (kind) => { const feed = buildThreadFeed( makeThread({ id: ThreadId.make("thread-setup-error"), projectId: ProjectId.make("project-1"), title: "Setup error", activities: [ makeActivity({ id: EventId.make("setup-error"), kind, summary: "Setup failed", createdAt: "2026-08-30T00:00:00.000Z", tone: "error", }), ], }), ); expect(feed).toMatchObject([ { type: "activity-group", activities: [{ id: "setup-error", status: "failure" }] }, ]); }, ); it("keeps older local feedback before newer messages returned by the server", () => { const submission = { id: MessageId.make("feedback-command-ordering"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:01.000Z", status: "sent" as const, feedbackId: "codex-thread-1", }; const laterMessage = { id: MessageId.make("later-server-message"), role: "assistant" as const, text: "Newer server response", turnId: null, createdAt: "2026-08-23T00:00:02.000Z", updatedAt: "2026-08-23T00:00:02.000Z", streaming: false, }; const thread = makeThread({ id: ThreadId.make("thread-feedback-ordering"), projectId: ProjectId.make("project-1"), title: "Feedback ordering", messages: [laterMessage], }); const feed = buildThreadFeed(thread, { localMessages: [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ], }); expect(feed.map((entry) => entry.id)).toEqual([ "feedback-command-ordering", "feedback-command-ordering:feedback", "later-server-message", ]); }); it("keeps historic work entries attributed to their turns", () => { const thread = makeThread({ id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Runtime warning thread", latestTurn: { turnId: TurnId.make("turn-latest"), state: "running", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: null, assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("activity-old"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-old"), payload: { message: "Old warning", }, }), makeActivity({ id: EventId.make("activity-latest"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:03.000Z", turnId: TurnId.make("turn-latest"), payload: { message: "Latest warning", }, }), ], }); const feed = buildThreadFeed(thread); expect(feed).toMatchObject([ { type: "activity-group", turnId: "turn-old", activities: [{ id: "activity-old", turnId: "turn-old" }], }, { type: "activity-group", turnId: "turn-latest", activities: [{ id: "activity-latest", turnId: "turn-latest" }], }, ]); }); it("drops runtime warnings with no displayable content", () => { const thread = makeThread({ id: ThreadId.make("thread-noise"), projectId: ProjectId.make("project-1"), title: "Warning noise thread", activities: [ makeActivity({ id: EventId.make("activity-noise"), kind: "runtime.warning", summary: "Claude system message 'background_tasks_changed' (no displayable text content)", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-1"), }), makeActivity({ id: EventId.make("activity-signal"), kind: "runtime.warning", summary: "Reconnecting... 2/5", createdAt: "2026-04-01T00:00:03.000Z", turnId: TurnId.make("turn-1"), }), ], }); const feed = buildThreadFeed(thread); expect(feed).toMatchObject([ { type: "activity-group", activities: [{ id: "activity-signal" }], }, ]); }); it("collapses matching tool lifecycle rows like desktop", () => { const thread = makeThread({ id: ThreadId.make("thread-2"), projectId: ProjectId.make("project-1"), title: "Collapsed tools", latestTurn: { turnId: TurnId.make("turn-1"), state: "completed", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: "2026-04-01T00:00:03.000Z", assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("tool-updated"), kind: "tool.updated", tone: "tool", summary: "Run tests", createdAt: "2026-04-01T00:00:01.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), makeActivity({ id: EventId.make("tool-completed"), kind: "tool.completed", tone: "tool", summary: "Run tests completed", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), ], }); const feed = buildThreadFeed(thread); const group = feed[0]; expect(group).toMatchObject({ type: "activity-group", }); if (!group || group.type !== "activity-group") { return; } expect(group.activities).toHaveLength(1); expect(group.activities[0]).toMatchObject({ id: "tool-updated", createdAt: "2026-04-01T00:00:01.000Z", turnId: "turn-1", summary: "Run tests", detail: "bun run test", canExpand: true, icon: "command", toolLike: true, status: "success", });Lines 1–500 of 1189. Load more
import { describe, expect, it } from "vite-plus/test"; import { codexFeedbackMessage } from "@t3tools/client-runtime/state/threads"; import { EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationThread, type OrchestrationThreadActivity, } from "@t3tools/contracts"; import { buildPendingUserInputAnswers, buildThreadFeed, derivePendingApprovals, deriveThreadFeedPresentation, isPendingUserInputOptionSelected, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type ThreadFeedActivity, type ThreadFeedEntry, } from "./threadActivity"; describe("Codex feedback pseudo-messages", () => { it("keeps pending and completed feedback messages in the mobile thread body", () => { const pending = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:00.000Z", status: "uploading" as const, }; const entries = [codexFeedbackMessage(pending), codexFeedbackMessage(pending, "assistant")].map( (message) => ({ type: "message" as const, id: message.id, createdAt: message.createdAt, message, }), ); expect(deriveThreadFeedPresentation(entries, null, new Set())).toEqual(entries); expect(entries[1]?.message.text).toBe("Sending feedback to OpenAI..."); const completed = codexFeedbackMessage( { ...pending, status: "sent", feedbackId: "codex-thread-1" }, "assistant", ); expect(completed.text).toContain("codex-thread-1"); }); }); const singleSelectQuestion = { id: "runtime", header: "Runtime", question: "Which runtime should be used?", options: [ { label: "Go", description: "One binary" }, { label: "Node.js", description: "Reuse TypeScript" }, ], multiSelect: false, } as const; const multiSelectQuestion = { id: "scope", header: "Scope", question: "Which data should be collected?", options: [ { label: "Orders", description: "Receipts" }, { label: "Listings", description: "Inventory" }, ], multiSelect: true, } as const; describe("pending user input answers", () => { it("replaces single-select options and toggles multi-select options", () => { expect( togglePendingUserInputOptionSelection( singleSelectQuestion, { selectedOptionLabels: ["Go"] }, "Node.js", ), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Node.js"] }); const orders = togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Orders"); const ordersAndListings = togglePendingUserInputOptionSelection( multiSelectQuestion, orders, "Listings", ); expect(ordersAndListings).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders", "Listings"], }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, ordersAndListings, "Orders"), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Listings"] }); const paddedOrders = togglePendingUserInputOptionSelection( multiSelectQuestion, undefined, " Orders ", ); expect(paddedOrders).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders"] }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, paddedOrders, " Orders "), ).toEqual({ customAnswer: "" }); }); it("builds array answers for multi-select questions", () => { expect( buildPendingUserInputAnswers([singleSelectQuestion, multiSelectQuestion], { runtime: { selectedOptionLabels: ["Go"] }, scope: { selectedOptionLabels: ["Orders", "Listings"] }, }), ).toEqual({ runtime: "Go", scope: ["Orders", "Listings"], }); }); it("clears selected options while a custom answer is active", () => { expect( setPendingUserInputCustomAnswer( { selectedOptionLabels: ["Orders", "Listings"] }, "Orders first", ), ).toEqual({ customAnswer: "Orders first" }); }); it("matches selected chips against normalized option labels", () => { expect( isPendingUserInputOptionSelected({ selectedOptionLabels: ["Orders"] }, " Orders "), ).toBe(true); expect( isPendingUserInputOptionSelected( { selectedOptionLabels: ["Orders"], customAnswer: "Orders first" }, " Orders ", ), ).toBe(false); }); }); describe("pending approvals", () => { it("keeps app access approvals and persistence choices from remote environments", () => { const options = [ { decision: "decline", label: "Decline" }, { decision: "acceptAlways", label: "Always allow Safari" }, { decision: "accept", label: "Approve" }, ]; const activity = makeActivity({ id: EventId.make("approval-safari"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestType: "mcp_elicitation_approval", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, }); expect(derivePendingApprovals([activity])).toEqual([ { requestId: "req-safari", requestKind: "mcp-elicitation", createdAt: "2026-08-24T00:00:00.000Z", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, ]); }); it("removes an app access approval after a remote client rejects it", () => { const requested = makeActivity({ id: EventId.make("approval-safari-open"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestKind: "mcp-elicitation" }, }); const resolved = makeActivity({ id: EventId.make("approval-safari-resolved"), kind: "approval.resolved", summary: "Approval resolved", createdAt: "2026-08-24T00:00:01.000Z", payload: { requestId: "req-safari", decision: "decline" }, }); expect(derivePendingApprovals([requested, resolved])).toEqual([]); }); }); function makeActivity( input: Partial<OrchestrationThreadActivity> & Pick<OrchestrationThreadActivity, "id" | "kind" | "summary" | "createdAt">, ): OrchestrationThreadActivity { return { tone: "info", payload: {}, turnId: null, ...input, }; } function makeThread( input: Partial<OrchestrationThread> & Pick<OrchestrationThread, "id" | "projectId" | "title">, ): OrchestrationThread { return { modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null, ...input, settledOverride: input.settledOverride ?? null, settledAt: input.settledAt ?? null, }; } describe("buildThreadFeed", () => { it("keeps setup failures visible without routine setup notices before or after a turn", () => { const thread = makeThread({ id: ThreadId.make("thread-worktree-setup"), projectId: ProjectId.make("project-1"), title: "Worktree setup", activities: [ makeActivity({ id: EventId.make("setup-requested"), kind: "setup-script.requested", summary: "Starting setup script", createdAt: "2026-08-30T00:00:00.000Z", }), makeActivity({ id: EventId.make("setup-started"), kind: "setup-script.started", summary: "Setup script started", createdAt: "2026-08-30T00:00:01.000Z", }), makeActivity({ id: EventId.make("setup-failed"), kind: "setup-script.failed", summary: "Setup script failed to start", createdAt: "2026-08-30T00:00:02.000Z", tone: "error", payload: { detail: "Setup command was not found" }, }), ], }); const latestTurn = { turnId: TurnId.make("turn-after-setup"), state: "running" as const, requestedAt: "2026-08-30T00:00:03.000Z", startedAt: "2026-08-30T00:00:04.000Z", completedAt: null, assistantMessageId: null, }; for (const currentTurn of [null, latestTurn]) { const feed = buildThreadFeed({ ...thread, latestTurn: currentTurn }); expect(feed).toMatchObject([ { type: "activity-group", activities: [{ id: "setup-failed", status: "failure" }], }, ]); const group = feed[0]; if (group?.type !== "activity-group") throw new Error("Expected the setup failure group"); expect(group.activities[0]?.getCopyText()).toContain("Setup command was not found"); } }); it.each(["setup-script.requested", "setup-script.started"])( "keeps error-toned %s notices visible", (kind) => { const feed = buildThreadFeed( makeThread({ id: ThreadId.make("thread-setup-error"), projectId: ProjectId.make("project-1"), title: "Setup error", activities: [ makeActivity({ id: EventId.make("setup-error"), kind, summary: "Setup failed", createdAt: "2026-08-30T00:00:00.000Z", tone: "error", }), ], }), ); expect(feed).toMatchObject([ { type: "activity-group", activities: [{ id: "setup-error", status: "failure" }] }, ]); }, ); it("keeps older local feedback before newer messages returned by the server", () => { const submission = { id: MessageId.make("feedback-command-ordering"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:01.000Z", status: "sent" as const, feedbackId: "codex-thread-1", }; const laterMessage = { id: MessageId.make("later-server-message"), role: "assistant" as const, text: "Newer server response", turnId: null, createdAt: "2026-08-23T00:00:02.000Z", updatedAt: "2026-08-23T00:00:02.000Z", streaming: false, }; const thread = makeThread({ id: ThreadId.make("thread-feedback-ordering"), projectId: ProjectId.make("project-1"), title: "Feedback ordering", messages: [laterMessage], }); const feed = buildThreadFeed(thread, { localMessages: [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ], }); expect(feed.map((entry) => entry.id)).toEqual([ "feedback-command-ordering", "feedback-command-ordering:feedback", "later-server-message", ]); }); it("keeps historic work entries attributed to their turns", () => { const thread = makeThread({ id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Runtime warning thread", latestTurn: { turnId: TurnId.make("turn-latest"), state: "running", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: null, assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("activity-old"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-old"), payload: { message: "Old warning", }, }), makeActivity({ id: EventId.make("activity-latest"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:03.000Z", turnId: TurnId.make("turn-latest"), payload: { message: "Latest warning", }, }), ], }); const feed = buildThreadFeed(thread); expect(feed).toMatchObject([ { type: "activity-group", turnId: "turn-old", activities: [{ id: "activity-old", turnId: "turn-old" }], }, { type: "activity-group", turnId: "turn-latest", activities: [{ id: "activity-latest", turnId: "turn-latest" }], }, ]); }); it("drops runtime warnings with no displayable content", () => { const thread = makeThread({ id: ThreadId.make("thread-noise"), projectId: ProjectId.make("project-1"), title: "Warning noise thread", activities: [ makeActivity({ id: EventId.make("activity-noise"), kind: "runtime.warning", summary: "Claude system message 'background_tasks_changed' (no displayable text content)", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-1"), }), makeActivity({ id: EventId.make("activity-signal"), kind: "runtime.warning", summary: "Reconnecting... 2/5", createdAt: "2026-04-01T00:00:03.000Z", turnId: TurnId.make("turn-1"), }), ], }); const feed = buildThreadFeed(thread); expect(feed).toMatchObject([ { type: "activity-group", activities: [{ id: "activity-signal" }], }, ]); }); it("collapses matching tool lifecycle rows like desktop", () => { const thread = makeThread({ id: ThreadId.make("thread-2"), projectId: ProjectId.make("project-1"), title: "Collapsed tools", latestTurn: { turnId: TurnId.make("turn-1"), state: "completed", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: "2026-04-01T00:00:03.000Z", assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("tool-updated"), kind: "tool.updated", tone: "tool", summary: "Run tests", createdAt: "2026-04-01T00:00:01.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), makeActivity({ id: EventId.make("tool-completed"), kind: "tool.completed", tone: "tool", summary: "Run tests completed", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), ], }); const feed = buildThreadFeed(thread); const group = feed[0]; expect(group).toMatchObject({ type: "activity-group", }); if (!group || group.type !== "activity-group") { return; } expect(group.activities).toHaveLength(1); expect(group.activities[0]).toMatchObject({ id: "tool-updated", createdAt: "2026-04-01T00:00:01.000Z", turnId: "turn-1", summary: "Run tests", detail: "bun run test", canExpand: true, icon: "command", toolLike: true, status: "success", });