import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resetAgentHostApiForTests, setAgentHostApiForTests } from "../../../agentActivityHost"; import { AgentEnvPanelActionProvider } from "../../agentEnv"; import type { AgentGUIRuntime } from "../../../agentActivityRuntime"; import { AgentMessageBlock } from "./AgentMessageBlock"; import { AgentTranscriptItemView } from "./AgentTranscriptItemView"; import type { AgentGoalControlRowVM } from "../contracts/agentGoalControlRowVM"; import type { AgentMessageContentVM } from "../contracts/agentMessageRowVM"; import type { AgentMessageRowVM } from "../contracts/agentMessageRowVM"; import type { AgentToolCallVM } from "../contracts/agentToolCallVM"; import type { AgentToolGroupRowVM } from "../contracts/agentToolGroupRowVM"; const mockState = vi.hoisted(() => ({ markdownOnLinkClicks: [] as Array<((href: string) => void) | undefined>, markdownStreamingFlags: [] as Array, toolGroupOnLinkClicks: [] as Array<((href: string) => void) | undefined> })); const agentActivityStyles = readFileSync( resolve(process.cwd(), "app/renderer/agentactivity.css"), "utf8" ); const agentActivityStyleElement = document.createElement("style"); agentActivityStyleElement.textContent = agentActivityStyles; afterEach(() => { agentActivityStyleElement.remove(); }); vi.mock("../../../i18n/index", () => ({ getActiveUiLanguage: () => "en", useTranslation: () => ({ t: (key: string) => key }), translate: (key: string) => key })); vi.mock("../../AgentMessageMarkdown", () => ({ AgentMessageMarkdown: ({ content, onLinkClick, streaming }: { content: string; onLinkClick?: (href: string) => void; streaming?: boolean; }) => { mockState.markdownOnLinkClicks.push(onLinkClick); mockState.markdownStreamingFlags.push(streaming); return
{content}
; } })); vi.mock("../../AgentRichTextReadonly", () => ({ AgentRichTextReadonly: ({ className, onLinkClick, value }: { className?: string; onLinkClick?: (href: string) => void; value?: string; }) => { mockState.markdownOnLinkClicks.push(onLinkClick); return
{value ?? "User message"}
; } })); vi.mock("./AgentToolGroupRow", () => ({ AgentToolGroupRow: ({ onLinkClick }: { onLinkClick?: (href: string) => void; }) => { mockState.toolGroupOnLinkClicks.push(onLinkClick); return
Tool group
; } })); beforeEach(() => { mockState.markdownOnLinkClicks.length = 0; mockState.markdownStreamingFlags.length = 0; mockState.toolGroupOnLinkClicks.length = 0; }); describe("AgentTranscriptItemView render stability", () => { it("renders and copies a goal control as a dedicated transcript row", async () => { const row: AgentGoalControlRowVM = { kind: "goal-control", id: "goal-control:client-submit:user:submit-goal-1", turnId: null, action: "set", body: "/goal ship it", occurredAtUnixMs: 1 }; const writeText = vi.fn(async () => undefined); installAgentHostClipboard(writeText); const { container } = render( ); expect(screen.getByText("/goal ship it")).toBeInTheDocument(); expect( container.querySelector('[data-agent-goal-control-action="set"]') ).toBeInTheDocument(); expect( container.querySelector( '[data-agent-message-footer="true"] [aria-label="agentHost.agentGui.copyMessage"]' ) ).toBeInTheDocument(); fireEvent.click( screen.getByRole("button", { name: "agentHost.agentGui.copyMessage" }) ); await waitFor(() => { expect(writeText).toHaveBeenCalledWith("/goal ship it"); }); }); it("keeps tool link handlers stable when an unchanged tool row is reprojected", () => { const onLinkAction = vi.fn(); const labels = transcriptLabels(); const { rerender } = render( ); rerender( ); expect(mockState.toolGroupOnLinkClicks).toHaveLength(2); expect(mockState.toolGroupOnLinkClicks[1]).toBe( mockState.toolGroupOnLinkClicks[0] ); }); it("keeps assistant markdown link handlers stable when an unchanged message row is reprojected", () => { const onLinkAction = vi.fn(); const { rerender } = render( ); rerender( ); expect(mockState.markdownOnLinkClicks).toHaveLength(2); expect(mockState.markdownOnLinkClicks[1]).toBe( mockState.markdownOnLinkClicks[0] ); }); it("routes a clicked app artifact reference from a sent user message to the owning app", () => { const onLinkAction = vi.fn(); render( ); mockState.markdownOnLinkClicks.at(-1)?.( "mention://workspace-reference/ai-canvas?source=app&workspaceId=workspace-1" ); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-app", workspaceId: "workspace-1", appId: "ai-canvas", source: "agent-markdown" }); }); it("toasts when a sent composer-file mention can no longer be resolved", () => { const onLinkAction = vi.fn(); const hostToastError = vi.fn(); setAgentHostApiForTests({ toast: { error: hostToastError } } as never); try { render( ); mockState.markdownOnLinkClicks.at(-1)?.( "mention://composer-file/file-1?status=ready" ); expect(onLinkAction).not.toHaveBeenCalled(); expect(hostToastError).toHaveBeenCalledWith( "agentHost.agentGui.composerFileOpenUnavailable" ); } finally { resetAgentHostApiForTests(); } }); it("keeps markdown streaming for active turns and unsettled messages", () => { render( ); render( ); render( ); expect(mockState.markdownStreamingFlags.at(-3)).toBe(true); expect(mockState.markdownStreamingFlags.at(-2)).toBe(false); expect(mockState.markdownStreamingFlags.at(-1)).toBe(true); }); it("keeps participant presentation off by default", () => { const { container } = render( ); const flow = document.querySelector( ".agent-gui-conversation__user-message-flow" ); expect(flow).toBeInstanceOf(HTMLElement); expect(flow?.children).toHaveLength(1); const group = flow?.children.item(0); expect(group).toBeInstanceOf(HTMLElement); if (!(group instanceof HTMLElement)) { throw new Error("Expected user message group to render."); } const bubble = group.children.item(0); expect(bubble).toBeInstanceOf(HTMLElement); if (!(bubble instanceof HTMLElement)) { throw new Error("Expected user message bubble to render."); } expect(group).toHaveAttribute("data-agent-message-speaker", "user"); expect(group).toHaveAttribute("data-agent-message-footer", "true"); expect(group).toHaveTextContent("User asks for a fix"); expect( group.querySelector(".agent-gui-conversation__message-copy-button") ).toBeInstanceOf(HTMLButtonElement); expect( container.querySelector("[data-agent-conversation-participant-avatar]") ).toBeNull(); expect( container.querySelector( ".agent-gui-conversation__participant-message-layout" ) ).toBeNull(); }); it("preserves the existing message DOM when participant presentation is disabled", () => { const { container } = render( ); expect( container.querySelector( ".agent-gui-conversation__participant-message-layout" ) ).toBeNull(); expect( container.querySelector("[data-agent-conversation-participant-avatar]") ).toBeNull(); expect( container.querySelector(".agent-gui-conversation__user-message-flow") ?.children ).toHaveLength(1); }); it("renders fixed participant avatar loading slots without hiding messages", () => { const participantPresentation = { enabled: true, status: "loading" } as const; const { container } = render( <> ); const avatars = container.querySelectorAll( "[data-agent-conversation-participant-avatar]" ); expect(avatars).toHaveLength(2); expect(avatars[0]).toHaveAttribute( "data-agent-conversation-participant-avatar", "assistant" ); expect(avatars[1]).toHaveAttribute( "data-agent-conversation-participant-avatar", "user" ); expect(avatars[0]).toHaveAttribute("data-avatar-state", "loading"); expect(avatars[1]).toHaveAttribute("data-avatar-state", "loading"); expect(screen.getByText(/Assistant answer/)).toBeInTheDocument(); expect(screen.getByText("User asks for a fix")).toBeInTheDocument(); }); it("renders the avatar and sender name in a header row above the message", () => { const participantPresentation = { enabled: true, status: "ready", agent: { name: "Codex" }, user: { name: "Alice" } } as const; const { container } = render( <> ); const assistantLayout = container.querySelector( '.agent-gui-conversation__participant-message-layout[data-agent-message-speaker="assistant"]' ); const userLayout = container.querySelector( '.agent-gui-conversation__participant-message-layout[data-agent-message-speaker="user"]' ); const assistantHeader = container.querySelector( '[data-agent-conversation-participant-header="assistant"]' ); expect(assistantHeader).toHaveClass( "agent-gui-conversation__participant-message-header" ); expect( assistantHeader?.querySelector( '[data-agent-conversation-participant-avatar="assistant"]' ) ).toHaveAttribute("aria-label", "Codex"); expect(assistantHeader).toHaveTextContent("Codex"); // The header row renders above the thinking/tool-call info and the // message content. expect( assistantHeader?.compareDocumentPosition(assistantLayout as Element) ).toBe(Node.DOCUMENT_POSITION_FOLLOWING); const userHeader = container.querySelector( '[data-agent-conversation-participant-header="user"]' ); expect(userHeader).toHaveClass( "agent-gui-conversation__participant-message-header" ); expect( userHeader?.querySelector( '[data-agent-conversation-participant-avatar="user"]' ) ).toHaveAttribute("aria-label", "Alice"); expect(userHeader).toHaveTextContent("Alice"); expect(userHeader?.compareDocumentPosition(userLayout as Element)).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); }); it("renders leading tool rows under the participant header above the message", () => { const participantPresentation = { enabled: true, status: "ready", agent: { name: "Codex" }, user: { name: "Alice" } } as const; const leadingToolGroup: AgentToolGroupRowVM = { kind: "tool-group", id: "leading-tools-1", turnId: "turn-1", grouped: true, calls: [toolCall(), toolCall()], entries: [], occurredAtUnixMs: 1 }; const row: AgentMessageRowVM = { ...assistantMessageRow(), leadingToolRows: [leadingToolGroup] }; const { container, getByText } = render( ); const header = container.querySelector( '[data-agent-conversation-participant-header="assistant"]' ); const toolContent = getByText("Tool group"); const messageContent = getByText(/Assistant answer/); expect(header).not.toBeNull(); expect(header?.compareDocumentPosition(toolContent)).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); expect(toolContent.compareDocumentPosition(messageContent)).toBe( Node.DOCUMENT_POSITION_FOLLOWING ); }); it("copies user message text through the agent host clipboard", async () => { const writeText = vi.fn(async () => undefined); installAgentHostClipboard(writeText); render( ); fireEvent.click( screen.getByRole("button", { name: "agentHost.agentGui.copyMessage" }) ); await waitFor(() => { expect(writeText).toHaveBeenCalledWith("User asks for a fix"); }); expect( screen.getByRole("button", { name: "agentHost.agentGui.messageCopied" }) ).toBeTruthy(); }); it("copies assistant markdown source through the agent host clipboard", async () => { const writeText = vi.fn(async () => undefined); installAgentHostClipboard(writeText); render( ); fireEvent.click( screen.getByRole("button", { name: "agentHost.agentGui.copyMessage" }) ); await waitFor(() => { expect(writeText).toHaveBeenCalledWith( "Assistant **summary** with `code`" ); }); }); it("loads user prompt image attachments from the activity runtime", async () => { const readSessionAttachment = vi.fn(async () => ({ attachmentId: "attachment-1", data: "aW1hZ2U=", mimeType: "image/png" as const, name: "screen.png" })); Object.defineProperty(window, "agentGUIRuntime", { configurable: true, value: { readSessionAttachment } as Partial }); render( ); await waitFor(() => { expect(readSessionAttachment).toHaveBeenCalledWith({ workspaceId: "room-1", agentSessionId: "session-1", attachmentId: "attachment-1" }); }); const image = await screen.findByRole("img", { name: "screen.png" }); expect(image).toHaveAttribute("src", "data:image/png;base64,aW1hZ2U="); fireEvent.load(image); await waitFor(() => { expect( screen.getByRole("button", { name: "common.expandImage" }) ).toBeTruthy(); }); delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; }); it("offers retry for an ordinary prompt image read failure", async () => { const readSessionAttachment = vi.fn(async () => { throw new Error("attachment not found"); }); Object.defineProperty(window, "agentGUIRuntime", { configurable: true, value: { readSessionAttachment } as Partial }); render( ); expect( await screen.findByText("agentHost.agentGui.imageLoadFailed") ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "agentHost.agentGui.retryImage" }) ).toBeInTheDocument(); delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; }); it("does not offer retry when the prompt image belongs to another VM", async () => { const readSessionAttachment = vi.fn(async () => { throw Object.assign(new Error("attachment is on another VM"), { code: "agent_session_attachment_vm_unavailable" }); }); Object.defineProperty(window, "agentGUIRuntime", { configurable: true, value: { readSessionAttachment } as Partial }); render( ); expect( await screen.findByText("agentHost.agentGui.imageTemporarilyUnavailable") ).toBeInTheDocument(); expect( screen.queryByRole("button", { name: "agentHost.agentGui.retryImage" }) ).toBeNull(); delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; }); it("keeps a loaded optimistic image mounted when its durable attachment arrives", async () => { const readPromptAsset = vi.fn(async () => ({ attachmentId: "prompt-asset-1", data: "b3B0aW1pc3RpYw==", mimeType: "image/png" as const, name: "screen.png", path: "/prompt-assets/screen.png" })); const readSessionAttachment = vi.fn(async () => ({ attachmentId: "attachment-1", data: "ZHVyYWJsZQ==", mimeType: "image/png" as const, name: "screen.png" })); Object.defineProperty(window, "agentGUIRuntime", { configurable: true, value: { readPromptAsset, readSessionAttachment } as Partial }); const stableImageId = "client-submit:user:submit-1:image:0"; const optimisticMessage: AgentMessageContentVM = { kind: "message-content", id: "client-submit:user:submit-1:images:0", turnId: "turn-1", body: "", presentationKind: "content", contentKind: "image-grid", images: [ { id: stableImageId, workspaceId: "room-1", agentSessionId: "session-1", mimeType: "image/png", name: "screen.png", path: "/prompt-assets/screen.png" } ], occurredAtUnixMs: 1 }; const durableMessage: AgentMessageContentVM = { ...optimisticMessage, images: [ { id: stableImageId, workspaceId: "room-1", agentSessionId: "session-1", attachmentId: "attachment-1", mimeType: "image/png", name: "screen.png" } ], occurredAtUnixMs: 2 }; const { rerender } = render( ); const optimisticImage = await screen.findByRole("img", { name: "screen.png" }); expect(optimisticImage).toHaveAttribute( "src", "data:image/png;base64,b3B0aW1pc3RpYw==" ); fireEvent.load(optimisticImage); await waitFor(() => { expect( screen.queryByTestId("agent-gui-message-image-loading") ).toBeNull(); }); rerender( ); expect(screen.getByRole("img", { name: "screen.png" })).toBe( optimisticImage ); expect(screen.queryByTestId("agent-gui-message-image-loading")).toBeNull(); expect(readPromptAsset).toHaveBeenCalledTimes(1); expect(readSessionAttachment).not.toHaveBeenCalled(); delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; }); it("reserves a loading slot until a remote HTTPS image loads", async () => { document.head.append(agentActivityStyleElement); const readPromptAsset = vi.fn(); Object.defineProperty(window, "agentGUIRuntime", { configurable: true, value: { readPromptAsset } as Partial }); render( ); const image = screen.getByRole("img", { name: "screen.png" }); expect(image).toHaveAttribute( "src", "https://objects.example.test/signed/screen.png" ); expect( screen.getByTestId("agent-gui-message-image-loading") ).toBeInTheDocument(); expect(image.closest("[data-image-state]")).toHaveAttribute( "data-image-state", "loading" ); const imageSlot = image.closest("[data-image-state]"); expect(imageSlot).not.toBeNull(); expect(window.getComputedStyle(imageSlot as Element).height).toBe("80px"); fireEvent.load(image); await waitFor(() => { expect( screen.queryByTestId("agent-gui-message-image-loading") ).toBeNull(); expect(image.closest("[data-image-state]")).toHaveAttribute( "data-image-state", "loaded" ); expect(window.getComputedStyle(imageSlot as Element).height).toBe("80px"); }); expect(readPromptAsset).not.toHaveBeenCalled(); delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; }); it("shows a failure placeholder and retries a remote image with the same URL", async () => { const signedUrl = "https://objects.example.test/signed/screen.png"; render( ); const image = screen.getByRole("img", { name: "screen.png" }); expect( screen.getByTestId("agent-gui-message-image-loading") ).toBeInTheDocument(); fireEvent.error(image); expect( await screen.findByTestId("agent-gui-message-image-failed") ).toBeInTheDocument(); expect( screen.getByText("agentHost.agentGui.imageLoadFailed") ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "agentHost.agentGui.retryImage" }) ).toBeInTheDocument(); fireEvent.click( screen.getByRole("button", { name: "agentHost.agentGui.retryImage" }) ); const retriedImage = await screen.findByRole("img", { name: "screen.png" }); expect(retriedImage).not.toBe(image); expect(retriedImage).toHaveAttribute("src", signedUrl); expect( screen.getByTestId("agent-gui-message-image-loading") ).toBeInTheDocument(); fireEvent.load(retriedImage); expect(screen.queryByTestId("agent-gui-message-image-failed")).toBeNull(); expect(screen.queryByTestId("agent-gui-message-image-loading")).toBeNull(); }); it("does not offer retry when a prompt image is unavailable in this host", async () => { delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; render( ); expect( await screen.findByTestId("agent-gui-message-image-failed") ).toBeInTheDocument(); expect( screen.getByText("agentHost.agentGui.imageTemporarilyUnavailable") ).toBeInTheDocument(); expect( screen.queryByRole("button", { name: "agentHost.agentGui.retryImage" }) ).toBeNull(); }); it("starts a fresh load when a failed remote image URL changes", async () => { const firstUrl = "https://objects.example.test/signed/screen-v1.png"; const secondUrl = "https://objects.example.test/signed/screen-v2.png"; const renderMessage = (url: string) => ( ); const { rerender } = render(renderMessage(firstUrl)); const firstImage = screen.getByRole("img", { name: "screen.png" }); fireEvent.error(firstImage); expect( await screen.findByTestId("agent-gui-message-image-failed") ).toBeInTheDocument(); rerender(renderMessage(secondUrl)); const refreshedImage = await screen.findByRole("img", { name: "screen.png" }); expect(refreshedImage).not.toBe(firstImage); expect(refreshedImage).toHaveAttribute("src", secondUrl); expect(screen.queryByTestId("agent-gui-message-image-failed")).toBeNull(); expect( screen.getByTestId("agent-gui-message-image-loading") ).toBeInTheDocument(); fireEvent.load(refreshedImage); expect(screen.queryByTestId("agent-gui-message-image-loading")).toBeNull(); }); it("shows a loading spinner while a user prompt image is being read", async () => { const readController: { resolve: (value: { attachmentId: string; data: string; mimeType: "image/png"; name: string; }) => void; } = { resolve: () => { throw new Error("readSessionAttachment was not called"); } }; const readSessionAttachment = vi.fn( () => new Promise<{ attachmentId: string; data: string; mimeType: "image/png"; name: string; }>((resolvePromise) => { readController.resolve = resolvePromise; }) ); Object.defineProperty(window, "agentGUIRuntime", { configurable: true, value: { readSessionAttachment } as Partial }); render( ); expect( await screen.findByTestId("agent-gui-message-image-loading") ).toBeTruthy(); expect(screen.queryByRole("img", { name: "screen.png" })).toBeNull(); readController.resolve({ attachmentId: "attachment-loading", data: "aW1hZ2U=", mimeType: "image/png", name: "screen.png" }); const image = await screen.findByRole("img", { name: "screen.png" }); expect(image).toHaveAttribute("src", "data:image/png;base64,aW1hZ2U="); expect( screen.getByTestId("agent-gui-message-image-loading") ).toBeInTheDocument(); fireEvent.load(image); await waitFor(() => { expect( screen.queryByTestId("agent-gui-message-image-loading") ).toBeNull(); }); delete (window as { agentGUIRuntime?: unknown }).agentGUIRuntime; }); it("shows local agent sign-in guidance for auth errors", () => { const onOpenAgentEnvPanel = vi.fn(); const { getByText } = render( ); expect( getByText("agentHost.agentGui.visibleErrorAuthRequired") ).toBeTruthy(); expect( getByText("agentHost.agentGui.visibleErrorAuthRequiredLocalAgentHint") ).toBeTruthy(); fireEvent.click(getByText("agentHost.agentGui.visibleErrorActionRelogin")); expect(onOpenAgentEnvPanel).toHaveBeenCalledWith({ provider: "claude-code", focus: "auth" }); }); it("renders transport retry notice details without the generic title", () => { const { getByText, queryByText } = render( ); expect(getByText("Reconnecting... 1/5")).toBeTruthy(); const notice = getByText("Reconnecting... 1/5"); expect(notice.tagName).toBe("DIV"); expect(queryByText("agentHost.agentGui.visibleErrorDetails")).toBeNull(); expect( queryByText("Codex connection interrupted. Reconnecting...") ).toBeNull(); expect( queryByText("agentHost.agentGui.systemNoticeTransportRetry") ).toBeNull(); }); it.each([ [ "plan_implementation_pending_confirmation", "agentHost.agentGui.systemNoticePlanImplementationPendingConfirmation" ], [ "plan_implementation_completed", "agentHost.agentGui.systemNoticePlanImplementationCompleted" ] ])( "renders semantic plan notice %s without transport text", (noticeKind, key) => { const { getByText } = render( ); expect(getByText(key)).toBeTruthy(); } ); it("renders transport fallback notices with the localized label", () => { const { getByRole, getByText } = render( ); expect(getByRole("status")).toBeTruthy(); expect( getByText("agentHost.agentGui.systemNoticeTransportFallback") ).toBeTruthy(); }); it("renders fallback warning notices with their title", () => { const { getByRole, getByText } = render( ); expect(getByRole("status")).toBeTruthy(); expect( getByText("Falling back from WebSockets to HTTPS transport.") ).toBeTruthy(); }); it("renders context compaction notices as an inline divider", () => { const { getByRole, getByText, queryByText } = render( ); const notice = getByRole("status"); expect(notice.tagName).toBe("DIV"); const dividers = notice.querySelectorAll('span[aria-hidden="true"]'); expect(dividers).toHaveLength(2); expect( getByText("agentHost.agentGui.contextCompactionCompleted") ).toBeTruthy(); expect(queryByText("agentHost.agentGui.systemNoticeDefault")).toBeNull(); }); it("renders in-progress compaction notices as a divider with a ticking timer", () => { vi.useFakeTimers(); try { vi.setSystemTime(66_000); const { getByRole, getByText } = render( ); const notice = getByRole("status"); const dividers = notice.querySelectorAll('span[aria-hidden="true"]'); expect(dividers).toHaveLength(2); expect( getByText(/agentHost\.agentGui\.contextCompactionInProgress/) ).toBeTruthy(); expect(getByText(/· 5s/)).toBeTruthy(); act(() => { vi.advanceTimersByTime(3_000); }); expect(getByText(/· 8s/)).toBeTruthy(); } finally { vi.useRealTimers(); } }); it("renders interrupted compaction notices with a wrapping detail below the divider", () => { const { getByRole, getByText } = render( ); const notice = getByRole("status"); expect(notice.querySelectorAll('span[aria-hidden="true"]')).toHaveLength(2); expect(notice.textContent).toContain( "agentHost.agentGui.contextCompactionInterrupted" ); expect(notice.textContent).toContain("Not enough messages to compact."); const detail = getByText("Not enough messages to compact."); expect(detail.className).toContain("break-words"); expect(detail.className).not.toContain("whitespace-nowrap"); }); it("renders context overflow as an error with new-conversation handoff guidance", () => { const { getByRole } = render( ); const notice = getByRole("alert"); expect(notice.textContent).toContain( "agentHost.agentGui.contextHandoffRequired" ); expect(notice.textContent).toContain( "agentHost.agentGui.contextHandoffRequiredDetail" ); fireEvent.click( screen.getByRole("button", { name: "agentHost.agentGui.visibleErrorDetails" }) ); expect(screen.getByText("Maximum context length exceeded.")).toBeTruthy(); }); it("does not let a legacy title override canonical compact status", () => { const { getByText, queryByText } = render( ); expect( getByText("agentHost.agentGui.contextCompactionInterrupted") ).toBeTruthy(); expect( queryByText("agentHost.agentGui.contextCompactionCompleted") ).toBeNull(); }); it("renders plan-tagged assistant messages as a dedicated plan card", () => { const { getByTestId } = render( ); expect(getByTestId("agent-plan-card")).toBeTruthy(); expect(getByTestId("agent-plan-card-title").textContent).toBe( "agentHost.agentGui.planCardTitle" ); expect(getByTestId("agent-plan-card").textContent).toContain( "Repo health plan" ); }); }); function transcriptLabels() { return { thinkingLabel: "Thought process", toolCallsLabel: (count: number) => `Tool calls (${count})`, processing: "Planning next moves", turnSummary: "Changed files" }; } type TestAgentMessageContentVM = Omit< AgentMessageContentVM, "presentationKind" > & { presentationKind?: AgentMessageContentVM["presentationKind"]; }; function assistantMessageRow( message: TestAgentMessageContentVM = { kind: "message-content", id: "assistant-1", turnId: "turn-1", body: "Assistant answer with [README](/workspace/demo/README.md)", occurredAtUnixMs: 1 } ): AgentMessageRowVM { return { kind: "message", id: "message-row-1", turnId: "turn-1", speaker: "assistant", messages: [withDefaultPresentationKind(message)], thinking: [], occurredAtUnixMs: 1 }; } function userMessageRow( message?: TestAgentMessageContentVM ): AgentMessageRowVM { return { kind: "message", id: "message-row-user-1", turnId: "turn-1", speaker: "user", messages: [ withDefaultPresentationKind( message ?? { kind: "message-content", id: "user-1", turnId: "turn-1", body: "User asks for a fix", copyText: "User asks for a fix", occurredAtUnixMs: 1 } ) ], thinking: [], occurredAtUnixMs: 1 }; } function withDefaultPresentationKind( message: TestAgentMessageContentVM ): AgentMessageContentVM { return { ...message, presentationKind: message.presentationKind ?? "content" }; } function installAgentHostClipboard( writeText: (text: string) => Promise ): void { ( window as unknown as { agentHostApi?: unknown; } ).agentHostApi = { clipboard: { writeText }, filesystem: {}, workspace: {} }; } function claudeCodeAuthErrorMessage(): AgentMessageContentVM { return { kind: "message-content", id: "assistant-auth-1", turnId: "turn-1", body: "Claude Code needs authentication.", presentationKind: "content", occurredAtUnixMs: 1, visibleError: { code: "auth_required", phase: "start", provider: "claude-code", detail: "Failed to authenticate.", retryable: false } }; } function toolGroupRow(): AgentToolGroupRowVM { const call = toolCall(); return { kind: "tool-group", id: "tools-1", turnId: "turn-1", grouped: false, calls: [call], entries: [ { kind: "tool-call", call } ], occurredAtUnixMs: 1 }; } function toolCall(): AgentToolCallVM { return { kind: "tool-call", id: "call-1", turnId: "turn-1", name: "Read File", toolName: "read_file", callType: "tool", status: "Completed", statusKind: "completed", summary: "/workspace/demo/README.md", compactSummary: "/workspace/demo/README.md", payload: null, input: null, output: null, error: null, metadata: null, locations: null, rendererKind: "read", approval: null, planMode: null, askUserQuestion: null, task: null, occurredAtUnixMs: 1 }; }