import { act, fireEvent, render, screen } 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 { AgentExpandedToolContent } from "./AgentExpandedToolContent"; import { AgentSubAgentCard } from "../AgentSubAgentCards"; describe("AgentExpandedToolContent", () => { afterEach(async () => { vi.useRealTimers(); setAgentGuiI18nTestLocale("zh-CN"); }); it("does not render inline approval options without a file preview payload", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Approval options")).toBeNull(); expect(screen.queryByText("Allow once")).toBeNull(); expect(screen.queryByText("Only for this call")).toBeNull(); }); it("renders direct approval input details when no nested preview is supplied", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Input")).toBeTruthy(); expect(screen.getByText(/Command[\s\S]*node relay\.js/)).toBeTruthy(); expect( screen.getByText(/Path[\s\S]*\/workspace\/scripts\/relay\.js/) ).toBeTruthy(); }); it("renders approval file previews from nested toolCall edit payloads", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Approval options")).toBeNull(); expect(screen.getByText("note.txt")).toBeTruthy(); expect(screen.getByText("hello from approval preview")).toBeTruthy(); expect(screen.queryByText("Allow once")).toBeNull(); }); it("renders approval file previews from nested toolCall direct input payloads", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Approval options")).toBeNull(); expect(screen.getByText("generated.md")).toBeTruthy(); expect(screen.getByText("const ready = true")).toBeTruthy(); expect(screen.queryByText("Allow once")).toBeNull(); }); it("renders web fetch URL details in a Claude Code web-read approval dialog", async () => { setAgentGuiI18nTestLocale("en"); // Claude Code's approval payload has no top-level `kind` field — the nested // toolCall is identified only by `title`/`toolName` ("WebFetch"), unlike // Codex's ACP `kind: "fetch"`. Both must resolve to the web-fetch // renderer instead of the bare summary fallback. render( ); expect(screen.getByText("URL")).toBeTruthy(); expect(screen.getByText("docs.example.com")).toBeTruthy(); expect(screen.getByText("https://docs.example.com/guide")).toBeTruthy(); }); it("renders web fetch URL details in an ACP (Codex) web-read approval dialog", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("URL")).toBeTruthy(); expect(screen.getByText("docs.example.com")).toBeTruthy(); }); it("renders ask-user options and selected answer from the typed ask-user vm", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Questions")).toBeTruthy(); expect( screen.getByText("Which implementation path should we take?") ).toBeTruthy(); expect(screen.getByText("Keep the shared renderer")).toBeTruthy(); expect( screen.getByText("Add typed tool bodies on top of the existing shell.") ).toBeTruthy(); expect(screen.getByText("Answer: Keep the shared renderer")).toBeTruthy(); }); it("renders an ask-user answer from the provider response payload", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Answer: Code editing")).toBeTruthy(); expect(screen.getByText("Your questions have been answered.")).toBeTruthy(); expect(screen.queryByText("Waiting for answer…")).toBeNull(); }); it("renders task steps through the new task renderer path", async () => { setAgentGuiI18nTestLocale("en"); render( ); const stepButton = screen.getByRole("button", { name: /Read.*Completed/i }); fireEvent.click(stepButton); await flushCollapsibleRevealFrames(); const pathLabel = screen.getByTitle("/workspace/src/agent.ts"); expect( pathLabel.querySelector(".agent-path-tail-label__directory")?.textContent ).toBe("/workspace/src/"); expect( pathLabel.querySelector(".agent-path-tail-label__file")?.textContent ).toBe("agent.ts"); expect( screen.getAllByText("Loaded agent conversation contract.").length ).toBeGreaterThan(0); }); it("renders ask-user task steps through the shared renderer matrix", async () => { setAgentGuiI18nTestLocale("en"); render( ); const stepButton = screen.getByRole("button", { name: /Ask User Question.*Waiting/i }); fireEvent.click(stepButton); await flushCollapsibleRevealFrames(); expect(screen.getByText("Questions")).toBeTruthy(); expect(screen.getByText("Which renderer should we use?")).toBeTruthy(); expect(screen.getByText("Shared renderer")).toBeTruthy(); expect(screen.getByText("Answer: Shared renderer")).toBeTruthy(); }); it("renders read tool body from typed tool vm", async () => { setAgentGuiI18nTestLocale("en"); render( ); const pathLabel = screen.getByTitle("/workspace/src/app.ts"); expect( pathLabel.querySelector(".agent-path-tail-label__directory")?.textContent ).toBe("/workspace/src/"); expect( pathLabel.querySelector(".agent-path-tail-label__file")?.textContent ).toBe("app.ts"); expect(screen.getByText(/typescript/i)).toBeTruthy(); expect(screen.getByText(/1 lines/i)).toBeTruthy(); expect(screen.getByText("const app = true")).toBeTruthy(); }); it("renders structured read metadata without dumping full file content by default", async () => { setAgentGuiI18nTestLocale("en"); render( ); const pathLabel = screen.getByTitle("/workspace/src/app.ts"); expect( pathLabel.querySelector(".agent-path-tail-label__directory")?.textContent ).toBe("/workspace/src/"); expect( pathLabel.querySelector(".agent-path-tail-label__file")?.textContent ).toBe("app.ts"); expect(screen.getByText(/L2-4/i)).toBeTruthy(); expect(screen.getByText(/10 lines/i)).toBeTruthy(); expect(screen.queryByText("const app = true")).toBeNull(); }); it("renders write tool content as a simplified code block", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("export const routes = []")).toBeTruthy(); }); it("shows unwrapped failure text above proposed write content", async () => { setAgentGuiI18nTestLocale("en"); render( File has not been read yet. Read it first before writing to it." } } }) )} /> ); expect(screen.getByText("Error")).toBeTruthy(); expect( screen.getByText( "File has not been read yet. Read it first before writing to it." ) ).toBeTruthy(); expect(screen.queryByText(/tool_use_error/i)).toBeNull(); expect(screen.getByText("{}")).toBeTruthy(); }); it("renders an (empty) placeholder when write content is empty", async () => { setAgentGuiI18nTestLocale("en"); const { container } = render( ); expect( container.querySelector('[data-agent-code-empty="true"]') ).toBeTruthy(); expect(screen.getByText("(empty)")).toBeTruthy(); }); it("renders a write-created file only once when both content and synthetic diff are present", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getAllByText("First line")).toHaveLength(1); }); it("renders multiple write patches as separate file cards", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Content")).toBeNull(); expect(screen.getByText("app.ts")).toBeTruthy(); expect(screen.getByText("routes.ts")).toBeTruthy(); expect(screen.getByText("export const routes = []")).toBeTruthy(); expect(screen.getByText("const ready = true")).toBeTruthy(); }); it("renders edit unified diff output through the patch viewer", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Patch")).toBeNull(); expect(screen.getByText("a.ts")).toBeTruthy(); expect(screen.getByText("const ready = false")).toBeTruthy(); expect(screen.getByText("const ready = true")).toBeTruthy(); }); it("renders edit old/new string pairs through the patch viewer path", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.queryByText("Patch")).toBeNull(); expect(screen.getByText("app.ts")).toBeTruthy(); expect(screen.getByText("const ready = false")).toBeTruthy(); expect(screen.getByText("const ready = true")).toBeTruthy(); }); it("renders canonical bash calls through the bash renderer without raw JSON fallback", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("pwd")).toBeTruthy(); expect(screen.getByText(/\/workspace\/app/)).toBeTruthy(); expect(screen.getByText(/permission denied/)).toBeTruthy(); expect(screen.queryByText("Path")).toBeNull(); expect(screen.queryByText("Output")).toBeNull(); expect(screen.queryByText("exit 127")).toBeNull(); expect(screen.queryByText("42 ms")).toBeNull(); expect(screen.queryByText("failed")).toBeNull(); expect(screen.queryByText(/call-pwd|command/)).toBeNull(); }); it("renders canonical failed search calls through the search renderer without raw JSON fallback", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Error")).toBeTruthy(); expect(screen.getByText(/missing-tool: not found/)).toBeTruthy(); expect(screen.queryByText(/call-list/)).toBeNull(); }); it("renders structured search file lists and no-match states", async () => { setAgentGuiI18nTestLocale("en"); const { rerender } = render( ); expect(screen.getByText("Results")).toBeTruthy(); expect(screen.getByText("src/app.ts")).toBeTruthy(); expect(screen.getByText("src/routes.ts")).toBeTruthy(); expect(screen.queryByText("Raw payload")).toBeNull(); rerender( ); expect(screen.getByText("No matches")).toBeTruthy(); }); it("renders structured search content and count modes", async () => { setAgentGuiI18nTestLocale("en"); const { rerender } = render( ); expect(screen.getByText("src/app.ts:42: const ready = true")).toBeTruthy(); rerender( ); expect(screen.getByText("3")).toBeTruthy(); }); it("renders web search results from stdout link payloads", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("agent renderer parity")).toBeTruthy(); expect(screen.getByText(/example\.com/)).toBeTruthy(); expect(screen.getByText("Renderer guide")).toBeTruthy(); expect(screen.getByText(/Summary/)).toBeTruthy(); }); it("renders canonical multi-query web search inputs from search_query", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText(/today top news/)).toBeTruthy(); expect(screen.getByText(/agent renderer parity/)).toBeTruthy(); expect(screen.queryByText("Results")).toBeNull(); }); it("renders the query when web search carries no results payload", async () => { setAgentGuiI18nTestLocale("en"); // Mirrors the Codex daemon path (codex_appserver_events.go): web search // events carry only the query under input, never an output/results payload. render( ); expect(screen.getByText("agent renderer parity")).toBeTruthy(); }); it("does not mirror a web search query into results or output", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Query")).toBeTruthy(); expect(screen.getByText("agent renderer parity")).toBeTruthy(); expect(screen.queryByText("Results")).toBeNull(); expect(screen.queryByText("Output")).toBeNull(); }); it("does not render an empty body when web search has no results", async () => { setAgentGuiI18nTestLocale("en"); const { container } = render( ); expect( container.querySelector( ".workspace-agents-status-panel__detail-tool-body" ) ).toBeNull(); }); it("dedupes repeated default tool input and output sections", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Input")).toBeTruthy(); expect(screen.getByText("same content")).toBeTruthy(); expect(screen.queryByText("Output")).toBeNull(); }); it("renders web fetch content with truncation notice", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("docs.example.com")).toBeTruthy(); expect(screen.getByText("Content truncated")).toBeTruthy(); }); it("does not mirror a web fetch domain summary into content", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("URL")).toBeTruthy(); expect( screen.getAllByText(/forecast\.weather\.gov/).length ).toBeGreaterThan(0); expect(screen.queryByText("Content")).toBeNull(); }); it("falls back to structured error content when a failed typed renderer has no body", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Error")).toBeTruthy(); expect(screen.getByText("network request failed")).toBeTruthy(); }); it("keeps real web fetch page content", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Content")).toBeTruthy(); expect(screen.getByText(/Detailed Forecast/)).toBeTruthy(); }); it("renders todo items with semantic statuses instead of raw json", async () => { setAgentGuiI18nTestLocale("en"); render( ); expect(screen.getByText("Ship renderer parity")).toBeTruthy(); expect(screen.getByText("Run smoke test")).toBeTruthy(); expect(screen.getByText("Polish fallback UI")).toBeTruthy(); expect(screen.queryByText(/"todos"|rawInput/)).toBeNull(); }); it("renders tool search, skill, and plan parity details", async () => { setAgentGuiI18nTestLocale("en"); const { rerender } = render( ); expect(screen.getAllByText("read_file").length).toBeGreaterThan(0); expect(screen.getByText("direct")).toBeTruthy(); expect(screen.getByText("1 loaded · 12 available")).toBeTruthy(); rerender( ); expect(screen.getByText("openai-docs")).toBeTruthy(); expect(screen.getByText("Failed to load skill")).toBeTruthy(); rerender( ); expect(screen.getByText("renderer.md")).toBeTruthy(); expect(screen.getByText(/Normalize payloads/)).toBeTruthy(); }); it("renders MCP results from structured objects, json strings, text blocks, and stdout fallback", async () => { setAgentGuiI18nTestLocale("en"); const { rerender } = render( ); expect(screen.getByText("ENG-42")).toBeTruthy(); expect(screen.getByText("Fix renderer parity")).toBeTruthy(); rerender( ); expect(screen.getByText("Renderer docs")).toBeTruthy(); expect(screen.getByText("Detailed guide")).toBeTruthy(); rerender( ); expect(screen.getByText("Loaded docs chunk")).toBeTruthy(); rerender( ); expect(screen.getByText("Plain stdout fallback")).toBeTruthy(); rerender( ); expect(screen.getByText("Nested output fallback")).toBeTruthy(); }); it("moves unknown structured payloads behind an explicit raw payload section", async () => { setAgentGuiI18nTestLocale("en"); render( ); const rawPayloadToggle = screen.getByRole("button", { name: "Raw payload" }); expect(rawPayloadToggle).toHaveAttribute("aria-expanded", "false"); expect(rawPayloadToggle.firstElementChild).toHaveTextContent("Raw payload"); expect(screen.queryByText(/"noisy": true/)).toBeNull(); fireEvent.click(rawPayloadToggle); await flushCollapsibleRevealFrames(); expect(rawPayloadToggle).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText(/"noisy": true/).closest("pre")).toHaveClass( "workspace-agents-status-panel__detail-scroll-region" ); expect(screen.queryByText("Input")).toBeNull(); expect(screen.queryByText("Output")).toBeNull(); }); it("routes nested task steps through the mcp renderer when metadata identifies an mcp tool", async () => { setAgentGuiI18nTestLocale("en"); render( ); const stepButton = screen.getByRole("button", { name: /Custom Tool.*Completed/i }); fireEvent.click(stepButton); await flushCollapsibleRevealFrames(); expect(screen.getByText("MCP")).toBeTruthy(); expect(screen.getByText(/Server: Atlassian/i)).toBeTruthy(); expect(screen.getByText("Found issue TSH-123")).toBeTruthy(); }); it("routes nested task steps through the mcp renderer when callType is explicitly mcp", async () => { setAgentGuiI18nTestLocale("en"); render( ); const stepButton = screen.getByRole("button", { name: /Custom Tool.*Completed/i }); fireEvent.click(stepButton); await flushCollapsibleRevealFrames(); expect(screen.getByText("MCP")).toBeTruthy(); expect(screen.getByText("Found issue TSH-456")).toBeTruthy(); }); it("renders a named standalone sub-agent card with an activity log", async () => { setAgentGuiI18nTestLocale("en"); vi.useFakeTimers(); vi.setSystemTime(101_000); render( ); // Identity comes from the sub-agent's own thread name, never the tool name. expect(screen.getByText("Repo smell analyst")).toBeTruthy(); expect(screen.queryByText("spawnAgent")).toBeNull(); expect(screen.getByText(/1m 40s · Running/)).toBeTruthy(); // Bash-block layout: task strip on top, latest progress line below - no // section labels. expect(screen.getByText("inspect the repository")).toBeTruthy(); expect(screen.getByText("Run command")).toBeTruthy(); expect(screen.queryByText("TASK")).toBeNull(); expect(screen.queryByText("PROGRESS")).toBeNull(); expect(screen.queryByText("Scanning layout")).toBeNull(); expect(screen.queryByText("3 earlier steps omitted")).toBeNull(); }); it("titles an unnamed sub-agent with the localized fallback and starting label", async () => { setAgentGuiI18nTestLocale("en"); render( ); // Tool-row-aligned header: label + per-lane index as the name slot. expect(screen.getByText("Sub-agent")).toBeTruthy(); expect(screen.getByText("#2")).toBeTruthy(); expect(screen.getByText("Starting…")).toBeTruthy(); }); }); 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: "Use tool", toolName: null, callType: "tool", status: "Completed", statusKind: "completed", summary: "", payload: null, ...overrides }; }