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 long Claude commands expandable without repeating them in full detail", () => { const command = `printf 'first line\nsecond line'\n&& printf done`; const thread = makeThread({ id: ThreadId.make("thread-long-command"), projectId: ProjectId.make("project-1"), title: "Long command", activities: [ makeActivity({ id: EventId.make("long-command"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`, data: { toolName: "Bash", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row).toMatchObject({ detail: command, canExpand: true }); expect(row?.getFullDetail()).toBe(command); expect(row?.getCopyText()).toBe(`Command run\n${command}`); }); it("keeps command output when it equals the displayed command", () => { const command = "printf hello"; const thread = makeThread({ id: ThreadId.make("thread-matching-command-output"), projectId: ProjectId.make("project-1"), title: "Matching output", activities: [ makeActivity({ id: EventId.make("matching-command-output"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`, data: { toolName: "Bash", command, rawOutput: { content: command } }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.detail).toBe(command); expect(row?.getFullDetail()).toBe(`${command}\n\n${command}`); expect(row?.getCopyText()).toBe(`Command run\n${command}\n\n${command}`); }); it("keeps OpenCode detail-only output when it equals the command", () => { const command = "printf hello"; const thread = makeThread({ id: ThreadId.make("thread-opencode-detail-output"), projectId: ProjectId.make("project-1"), title: "OpenCode detail output", activities: [ makeActivity({ id: EventId.make("opencode-detail-output"), kind: "tool.completed", tone: "tool", summary: "bash", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "bash", detail: command, data: { command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBe(command); expect(row?.getFullDetail()).toBe(`${command}\n\n${command}`); }); it("drops a truncated Claude echo of a long command", () => { const command = `git add -A && git commit -m "${"x".repeat(200)}"`; const thread = makeThread({ id: ThreadId.make("thread-truncated-echo"), projectId: ProjectId.make("project-1"), title: "Truncated echo", activities: [ makeActivity({ id: EventId.make("truncated-echo"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`.slice(0, 177) + "...", data: { toolName: "Bash", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBeUndefined(); expect(row?.getFullDetail()).toBe(command); }); it("drops an ACP command echo when the update omits the tool kind", () => { const command = "pnpm test"; const thread = makeThread({ id: ThreadId.make("thread-acp-no-kind"), projectId: ProjectId.make("project-1"), title: "ACP no kind", activities: [ makeActivity({ id: EventId.make("acp-no-kind"), kind: "tool.completed", tone: "tool", summary: "Terminal", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Terminal", detail: command, data: { toolCallId: "tool-1", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBeUndefined(); expect(row?.getFullDetail()).toBe(command); }); it("drops ACP command metadata when detail only repeats the command", () => { const command = "printf hello"; const thread = makeThread({ id: ThreadId.make("thread-acp-command-detail"), projectId: ProjectId.make("project-1"), title: "ACP command detail", activities: [ makeActivity({ id: EventId.make("acp-command-detail"), kind: "tool.completed", tone: "tool", summary: "Terminal", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Terminal", detail: command, data: { kind: "execute", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBeUndefined(); expect(row?.getFullDetail()).toBe(command); }); it("does not show command output when the command input is missing", () => { const thread = makeThread({ id: ThreadId.make("thread-command-without-input"), projectId: ProjectId.make("project-1"), title: "Missing command input", activities: [ makeActivity({ id: EventId.make("command-without-input"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", data: { rawOutput: { content: "output without command metadata" } }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; expect(group.activities[0]?.detail).toBeNull(); expect(group.activities[0]?.getFullDetail()).toBeNull(); }); 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");Lines 1–500 of 2064. 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 long Claude commands expandable without repeating them in full detail", () => { const command = `printf 'first line\nsecond line'\n&& printf done`; const thread = makeThread({ id: ThreadId.make("thread-long-command"), projectId: ProjectId.make("project-1"), title: "Long command", activities: [ makeActivity({ id: EventId.make("long-command"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`, data: { toolName: "Bash", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row).toMatchObject({ detail: command, canExpand: true }); expect(row?.getFullDetail()).toBe(command); expect(row?.getCopyText()).toBe(`Command run\n${command}`); }); it("keeps command output when it equals the displayed command", () => { const command = "printf hello"; const thread = makeThread({ id: ThreadId.make("thread-matching-command-output"), projectId: ProjectId.make("project-1"), title: "Matching output", activities: [ makeActivity({ id: EventId.make("matching-command-output"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`, data: { toolName: "Bash", command, rawOutput: { content: command } }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.detail).toBe(command); expect(row?.getFullDetail()).toBe(`${command}\n\n${command}`); expect(row?.getCopyText()).toBe(`Command run\n${command}\n\n${command}`); }); it("keeps OpenCode detail-only output when it equals the command", () => { const command = "printf hello"; const thread = makeThread({ id: ThreadId.make("thread-opencode-detail-output"), projectId: ProjectId.make("project-1"), title: "OpenCode detail output", activities: [ makeActivity({ id: EventId.make("opencode-detail-output"), kind: "tool.completed", tone: "tool", summary: "bash", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "bash", detail: command, data: { command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBe(command); expect(row?.getFullDetail()).toBe(`${command}\n\n${command}`); }); it("drops a truncated Claude echo of a long command", () => { const command = `git add -A && git commit -m "${"x".repeat(200)}"`; const thread = makeThread({ id: ThreadId.make("thread-truncated-echo"), projectId: ProjectId.make("project-1"), title: "Truncated echo", activities: [ makeActivity({ id: EventId.make("truncated-echo"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", detail: `Bash: ${command}`.slice(0, 177) + "...", data: { toolName: "Bash", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBeUndefined(); expect(row?.getFullDetail()).toBe(command); }); it("drops an ACP command echo when the update omits the tool kind", () => { const command = "pnpm test"; const thread = makeThread({ id: ThreadId.make("thread-acp-no-kind"), projectId: ProjectId.make("project-1"), title: "ACP no kind", activities: [ makeActivity({ id: EventId.make("acp-no-kind"), kind: "tool.completed", tone: "tool", summary: "Terminal", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Terminal", detail: command, data: { toolCallId: "tool-1", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBeUndefined(); expect(row?.getFullDetail()).toBe(command); }); it("drops ACP command metadata when detail only repeats the command", () => { const command = "printf hello"; const thread = makeThread({ id: ThreadId.make("thread-acp-command-detail"), projectId: ProjectId.make("project-1"), title: "ACP command detail", activities: [ makeActivity({ id: EventId.make("acp-command-detail"), kind: "tool.completed", tone: "tool", summary: "Terminal", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Terminal", detail: command, data: { kind: "execute", command }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; const [row] = group.activities; expect(row?.workEntry.detail).toBeUndefined(); expect(row?.getFullDetail()).toBe(command); }); it("does not show command output when the command input is missing", () => { const thread = makeThread({ id: ThreadId.make("thread-command-without-input"), projectId: ProjectId.make("project-1"), title: "Missing command input", activities: [ makeActivity({ id: EventId.make("command-without-input"), kind: "tool.completed", tone: "tool", summary: "Command run", createdAt: "2026-09-01T00:00:00.000Z", payload: { itemType: "command_execution", title: "Command run", data: { rawOutput: { content: "output without command metadata" } }, }, }), ], }); const [group] = buildThreadFeed(thread); expect(group?.type).toBe("activity-group"); if (group?.type !== "activity-group") return; expect(group.activities[0]?.detail).toBeNull(); expect(group.activities[0]?.getFullDetail()).toBeNull(); }); 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");