import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { setAgentGuiI18nTestLocale } from "../../../i18n/testUtils"; import type { WorkspaceAgentSessionDetailToolCall } from "../../workspaceAgentSessionDetailViewModel"; import { projectAgentToolCall } from "../projection/agentToolProjection"; import { AgentAskUserQuestionCard } from "./AgentAskUserQuestionCard"; import { AgentEnterPlanModeCard } from "./AgentEnterPlanModeCard"; import { AgentExitPlanModeCard } from "./AgentExitPlanModeCard"; import { AgentTaskCallCard } from "./AgentTaskCallCard"; import { AgentToolCallCard } from "./AgentToolCallCard"; describe("Agent specialized tool cards", () => { afterEach(async () => { setAgentGuiI18nTestLocale("zh-CN"); }); it("expands waiting ask-user cards by default", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Questions")).toBeTruthy(); expect(screen.getByText("Which scope should we use?")).toBeTruthy(); expect(screen.queryByRole("button", { expanded: true })).toBeNull(); }); it("renders enter-plan cards as always-visible detail blocks", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect( screen.getByText("Exploring codebase and planning the renderer rewrite.") ).toBeTruthy(); expect(screen.queryByRole("button", { expanded: true })).toBeNull(); }); it("renders exit-plan cards as always-visible detail blocks", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText(/Finalize the projection layer/)).toBeTruthy(); expect(screen.queryByRole("button", { expanded: true })).toBeNull(); }); it("renders running task cards as always-visible detail blocks", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Steps")).toBeTruthy(); expect(screen.queryByRole("button", { expanded: true })).toBeNull(); }); it("shows failed task status and fallback error when provider omits details", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect( screen.getByRole("button", { name: /Agent Failed Generate exactly/i }) ).toBeTruthy(); expect(screen.getAllByText(/Failed/i).length).toBeGreaterThan(0); expect(screen.getByText("Error")).toBeTruthy(); expect( screen.getByText("The provider reported failure without details.") ).toBeTruthy(); expect(screen.queryByText("Output")).toBeNull(); }); it("keeps task step output hidden until the step expands", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Loaded docs chunk")).toBeNull(); fireEvent.click(screen.getByRole("button", { name: /Read Completed/i })); expect(screen.getByText("Loaded docs chunk")).toBeTruthy(); }); it("shows diff stats in the tool call header for structured edit patches", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("+1")).toBeTruthy(); expect(screen.getByText("-1")).toBeTruthy(); }); it("does not show edit diff stats on approval tool headers", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("+1")).toBeNull(); expect(screen.queryByText("-1")).toBeNull(); }); it("uses explicit MCP target fields as the approval-wrapped tool title", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("playwright / browser_close")).toBeTruthy(); expect(screen.queryByText("Approval")).toBeNull(); }); it("renders the image generation prompt without duplicating artifact output", async () => { setAgentGuiI18nTestLocale("en"); const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tool-image-preview") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect(screen.getByText(/Revised prompt:/)).toBeTruthy(); expect( screen.queryByRole("img", { name: "Image generation preview" }) ).toBeNull(); expect(screen.queryByText("Output")).toBeNull(); expect(readFile).not.toHaveBeenCalled(); }); it("does not duplicate historical Codex image output from savedPath", async () => { setAgentGuiI18nTestLocale("en"); const savedPath = "/Users/demo/.tutti/agent/runs/session/codex-home/generated_images/thread/ig_legacy.png"; const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:legacy-tool-image-preview") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect( screen.queryByRole("img", { name: "Image generation preview" }) ).toBeNull(); expect(readFile).not.toHaveBeenCalled(); }); it("does not show an output loading placeholder for active image generation cards", async () => { setAgentGuiI18nTestLocale("en"); const readFile = vi .fn() .mockRejectedValue(new Error("ENOENT: generated image not ready")); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; render( ); expect(screen.getByText(/Revised prompt:/)).toBeTruthy(); expect(screen.queryByText("Loading preview...")).toBeNull(); expect( screen.queryByRole("img", { name: "Image generation preview" }) ).toBeNull(); expect(readFile).not.toHaveBeenCalled(); }); it("does not render image-only output inside the tool call card", async () => { setAgentGuiI18nTestLocale("en"); const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:image-only-preview") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect( screen.queryByRole("img", { name: "Image generation preview" }) ).toBeNull(); expect(screen.queryByText("Input")).toBeNull(); expect(screen.queryByText("Output")).toBeNull(); expect(readFile).not.toHaveBeenCalled(); }); it("does not allow expanding web fetch cards without displayable content", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Fetch webpage")).toBeTruthy(); expect(screen.getByText("running")).toBeTruthy(); expect(screen.queryByRole("button", { expanded: false })).toBeNull(); expect(screen.queryByRole("button", { name: /Fetch webpage/ })).toBeNull(); expect( document.querySelector(".workspace-agents-status-panel__detail-tool-body") ).not.toBeInTheDocument(); }); it("does not allow expanding edit cards without displayable file content", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Edit file")).toBeTruthy(); expect(screen.getByText("completed")).toBeTruthy(); expect(screen.queryByRole("button", { expanded: false })).toBeNull(); expect( document.querySelector(".workspace-agents-status-panel__detail-tool-body") ).not.toBeInTheDocument(); }); it("shows added-line stats for write content without a diff patch", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("+2")).toBeTruthy(); }); it("hides diff stats for failed writes that still carry proposed content", async () => { setAgentGuiI18nTestLocale("en"); render( File has not been read yet. Read it first before writing to it." } } }) )} /> ); expect(screen.queryByText("+1")).toBeNull(); expect(screen.getByText("failed")).toBeTruthy(); }); it("keeps edit cards collapsed by default and expands detail on demand", async () => { setAgentGuiI18nTestLocale("en"); render( ); const toggle = screen.getByRole("button", { expanded: false }); expect(toggle).toHaveAttribute("aria-expanded", "false"); expect( document.querySelector(".workspace-agents-status-panel__detail-tool-body") ).not.toBeInTheDocument(); fireEvent.click(toggle); await flushCollapsibleRevealFrames(); expect(toggle).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText("app.ts")).toBeTruthy(); const changedLine = screen.getByText("const ready = true"); expect(changedLine).toBeTruthy(); await waitFor(() => expect(changedLine.closest(".agent-collapsible-reveal")).toHaveAttribute( "data-expanded", "true" ) ); }); it("keeps collapsed tool card details unmounted while idle", async () => { setAgentGuiI18nTestLocale("en"); let idleCallback: IdleRequestCallback | null = null; vi.stubGlobal("requestIdleCallback", (callback: IdleRequestCallback) => { idleCallback = callback; return 1; }); vi.stubGlobal("cancelIdleCallback", () => undefined); render( ); expect(screen.queryByText("@@ -1 +1 @@")).toBeNull(); act(() => { idleCallback?.({ didTimeout: false, timeRemaining: () => 50 }); }); expect( document.querySelector(".workspace-agents-status-panel__detail-tool-body") ).not.toBeInTheDocument(); }); }); async function flushCollapsibleRevealFrames(): Promise { await flushAnimationFrame(); await flushAnimationFrame(); } async function flushAnimationFrame(): Promise { await act(async () => { await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); }); } function toolCall( overrides: Partial = {} ): WorkspaceAgentSessionDetailToolCall { return { id: "call:1", name: "Tool", toolName: null, callType: "tool", status: "Completed", statusKind: "completed", summary: "", payload: null, ...overrides }; }