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
};
}