import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster"; import { DEFAULT_LOCAL_PREFS, writeLocalPreferences } from "@/lib/local-preferences"; import type { CliAppInfo, McpPresetInfo, UIMessage } from "@/lib/types"; const BLENDER_CLI_APP: CliAppInfo = { name: "blender", display_name: "Blender", category: "3d", description: "3D creation", requires: "", source: "harness", entry_point: "cli-anything-blender", install_supported: true, installed: true, available: true, status: "installed", logo_url: "https://example.invalid/blender.svg", brand_color: "#E87D0D", skill_installed: true, }; const BROWSERBASE_MCP: McpPresetInfo = { name: "browserbase", display_name: "Browserbase", category: "browser", description: "Cloud browser automation", docs_url: "https://docs.browserbase.com", transport: "streamableHttp", requires: "Browserbase API key", note: "", install_supported: true, installed: true, configured: true, available: true, status: "configured", logo_url: "https://example.invalid/browserbase.svg", brand_color: "#111827", required_fields: [], connection_summary: "https://mcp.browserbase.com/mcp", }; function unifiedFileDiff(lines: string[], truncated = false) { return { format: "unified" as const, context: 3, truncated, text: lines.join("\n"), }; } function activityMessages(extraReasoning = "", extraTool?: UIMessage): UIMessage[] { const rows: UIMessage[] = [ { id: "r1", role: "assistant", content: "", reasoning: `thinking${extraReasoning}`, reasoningStreaming: true, isStreaming: true, createdAt: 1, }, { id: "t1", role: "tool", kind: "trace", content: "search()", traces: ["search()"], createdAt: 2, }, ]; if (extraTool) rows.push(extraTool); return rows; } function installAnimationFrameQueue() { const originalRequest = window.requestAnimationFrame; const originalCancel = window.cancelAnimationFrame; const callbacks = new Map(); let nextId = 1; window.requestAnimationFrame = ((callback: FrameRequestCallback) => { const id = nextId; nextId += 1; callbacks.set(id, callback); return id; }) as typeof window.requestAnimationFrame; window.cancelAnimationFrame = ((id: number) => { callbacks.delete(id); }) as typeof window.cancelAnimationFrame; return { flush() { const pending = Array.from(callbacks.entries()); callbacks.clear(); for (const [, callback] of pending) callback(0); }, restore() { window.requestAnimationFrame = originalRequest; window.cancelAnimationFrame = originalCancel; }, }; } function setScrollGeometry( element: HTMLElement, geometry: { scrollHeight: number; clientHeight: number; scrollTop?: number }, ) { Object.defineProperties(element, { scrollHeight: { configurable: true, value: geometry.scrollHeight }, clientHeight: { configurable: true, value: geometry.clientHeight }, scrollTop: { configurable: true, value: geometry.scrollTop ?? element.scrollTop, writable: true, }, }); } function installReducedMotion() { const original = window.matchMedia; Object.defineProperty(window, "matchMedia", { configurable: true, value: () => ({ matches: true, media: "(prefers-reduced-motion: reduce)", addEventListener: () => {}, removeEventListener: () => {}, }), }); return () => { Object.defineProperty(window, "matchMedia", { configurable: true, value: original, }); }; } describe("AgentActivityCluster", () => { it("jumps to the latest activity when opened", () => { const raf = installAnimationFrameQueue(); try { render( , ); const scrollport = screen.getByTestId("agent-activity-scroll"); setScrollGeometry(scrollport, { scrollHeight: 1000, clientHeight: 120, scrollTop: 0, }); act(() => { raf.flush(); }); expect(scrollport.scrollTop).toBe(880); } finally { raf.restore(); } }); it("follows new reasoning and tool activity while the user is at the bottom", () => { const raf = installAnimationFrameQueue(); try { const { rerender } = render( , ); const scrollport = screen.getByTestId("agent-activity-scroll"); setScrollGeometry(scrollport, { scrollHeight: 1000, clientHeight: 120, scrollTop: 0, }); act(() => { raf.flush(); }); rerender( , ); setScrollGeometry(scrollport, { scrollHeight: 1500, clientHeight: 120, scrollTop: scrollport.scrollTop, }); act(() => { raf.flush(); }); expect(scrollport.scrollTop).toBe(1380); } finally { raf.restore(); } }); it("does not pull the user down after they scroll up inside the activity pane", () => { const raf = installAnimationFrameQueue(); try { const { rerender } = render( , ); const scrollport = screen.getByTestId("agent-activity-scroll"); setScrollGeometry(scrollport, { scrollHeight: 1000, clientHeight: 120, scrollTop: 0, }); act(() => { raf.flush(); }); scrollport.scrollTop = 100; fireEvent.scroll(scrollport); rerender( , ); setScrollGeometry(scrollport, { scrollHeight: 1500, clientHeight: 120, scrollTop: scrollport.scrollTop, }); act(() => { raf.flush(); }); expect(scrollport.scrollTop).toBe(100); } finally { raf.restore(); } }); it("turns the live reasoning marker into an animated check when thinking completes", async () => { const liveReasoning: UIMessage = { id: "r-check", role: "assistant", content: "", reasoning: "checking a source", reasoningStreaming: true, isStreaming: true, createdAt: 1, }; const { rerender } = render( , ); expect(screen.getByTestId("activity-reasoning-marker")).toHaveAttribute("data-state", "thinking"); rerender( , ); const marker = screen.getByTestId("activity-reasoning-marker"); expect(marker).toHaveAttribute("data-state", "done"); expect(marker.querySelector("svg")).toBeInTheDocument(); await waitFor(() => expect(marker).toHaveClass("animate-in")); }); it("briefly shows completed activity, then auto-collapses before the answer", () => { vi.useFakeTimers(); const liveReasoning: UIMessage = { id: "r-collapse", role: "assistant", content: "", reasoning: "checking files", reasoningStreaming: true, isStreaming: true, createdAt: 1, }; try { const { rerender } = render( , ); expect(screen.getByTestId("agent-activity-scroll")).toBeInTheDocument(); rerender( , ); expect(screen.getByTestId("agent-activity-scroll")).toBeInTheDocument(); act(() => { vi.advanceTimersByTime(901); }); expect(screen.queryByTestId("agent-activity-scroll")).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "Thought" })).toHaveAttribute( "aria-expanded", "false", ); } finally { vi.useRealTimers(); } }); it("keeps chevron color feedback faster than the drawer rotation", () => { render( , ); const button = screen.getByRole("button", { name: "Thought" }); const chevron = button.querySelector("svg"); expect(chevron).toBeInTheDocument(); expect(chevron).toHaveClass("transition-colors", "duration-200"); expect(chevron?.parentElement).toHaveClass( "transition-transform", "[transition-duration:600ms]", ); }); it("uses persisted turn latency for completed history instead of replay timestamps", () => { render( , ); expect(screen.getByText("Thought for 12s")).toBeInTheDocument(); }); it("labels mixed tool activity as work instead of thought", () => { render( , ); expect(screen.getByText("Worked for 12s")).toBeInTheDocument(); expect(screen.queryByText("Thought for 12s")).not.toBeInTheDocument(); }); it("omits the duration when completed history has no reliable timing", () => { render( , ); expect(screen.getByText("Thought")).toBeInTheDocument(); expect(screen.queryByText("Thought for 0s")).not.toBeInTheDocument(); }); it("renders file edits as one-line activity rows", async () => { const restoreMotion = installReducedMotion(); try { render( , ); expect(screen.queryByText("Edited files")).not.toBeInTheDocument(); const fileRef = screen.getByTestId("activity-file-reference"); expect(fileRef).toHaveTextContent("src/app.tsx"); expect(fileRef).toHaveAttribute("aria-label", "src/app.tsx"); expect(screen.queryByTestId("activity-header-file-reference")).not.toBeInTheDocument(); expect(screen.queryByTestId("file-edit-diff")).not.toBeInTheDocument(); for (const diffPair of screen.getAllByTestId("activity-diff-pair")) { expect(diffPair).toHaveClass("items-baseline"); expect(diffPair).toHaveClass("leading-[inherit]"); expect(diffPair.className).not.toContain("translate-y"); } expect(screen.getByText("+12")).toBeInTheDocument(); expect(screen.getByText("-3")).toBeInTheDocument(); } finally { restoreMotion(); } }); it("renders file edit diffs and responds to preference changes", () => { localStorage.setItem( "nanobot-webui.settings-preferences", JSON.stringify({ fileEditDisplayMode: "diff" }), ); try { render( ;", "+ return ;", ]), }], createdAt: 3, }]} isTurnStreaming={false} hasBodyBelow={false} />, ); expect(screen.getByTestId("file-edit-diff")).toBeInTheDocument(); expect(screen.getByText("return ;")).toBeInTheDocument(); expect(screen.getByText("return ;")).toBeInTheDocument(); expect(screen.getByTestId("activity-file-reference")).toHaveTextContent("src/app.tsx"); expect(screen.getAllByTestId("activity-diff-pair")).toHaveLength(1); act(() => { writeLocalPreferences({ ...DEFAULT_LOCAL_PREFS, fileEditDisplayMode: "summary" }); }); expect(screen.queryByTestId("file-edit-diff")).not.toBeInTheDocument(); act(() => { writeLocalPreferences({ ...DEFAULT_LOCAL_PREFS, fileEditDisplayMode: "diff" }); }); expect(screen.getByTestId("file-edit-diff")).toBeInTheDocument(); } finally { localStorage.removeItem("nanobot-webui.settings-preferences"); } }); it("renders folded separators between separated file edit hunks", () => { localStorage.setItem( "nanobot-webui.settings-preferences", JSON.stringify({ fileEditDisplayMode: "diff" }), ); try { render( , ); expect(screen.getByTestId("file-edit-diff-hunk-gap")).toHaveTextContent( "21 unchanged lines hidden", ); expect(screen.queryByText("@@ -25,3 +25,3 @@")).not.toBeInTheDocument(); expect(screen.getByText("return newSecond;")).toBeInTheDocument(); expect(screen.getByTestId("activity-file-reference")).toHaveTextContent("src/app.tsx"); } finally { localStorage.removeItem("nanobot-webui.settings-preferences"); } }); it("keeps long file edit diffs collapsed until opened", () => { localStorage.setItem( "nanobot-webui.settings-preferences", JSON.stringify({ fileEditDisplayMode: "diff" }), ); const lines = Array.from({ length: 165 }, (_, index) => `line-${index + 1}`); try { render( `+${line}`), ]), }], createdAt: 3, }]} isTurnStreaming={false} hasBodyBelow={false} />, ); const toggle = screen.getByTestId("file-edit-diff-toggle"); expect(toggle).toHaveAttribute("aria-expanded", "false"); expect(toggle).toHaveTextContent("View large diff"); expect(toggle).toHaveTextContent("165 lines"); expect(screen.queryByTestId("file-edit-diff")).not.toBeInTheDocument(); expect(screen.queryByText("line-1")).not.toBeInTheDocument(); fireEvent.click(toggle); expect(toggle).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText("line-160")).toBeInTheDocument(); expect(screen.queryByText("line-161")).not.toBeInTheDocument(); expect(screen.getByTestId("file-edit-diff-expand-lines")).toHaveTextContent( "Show 5 more lines", ); fireEvent.click(screen.getByTestId("file-edit-diff-expand-lines")); expect(screen.getByText("line-165")).toBeInTheDocument(); expect(screen.getByTestId("file-edit-diff-collapse-lines")).toHaveTextContent( "Show fewer lines", ); fireEvent.click(screen.getByTestId("file-edit-diff-collapse-lines")); expect(screen.queryByText("line-165")).not.toBeInTheDocument(); expect(screen.getByTestId("file-edit-diff-expand-lines")).toHaveTextContent( "Show 5 more lines", ); fireEvent.click(toggle); expect(toggle).toHaveAttribute("aria-expanded", "false"); expect(screen.queryByTestId("file-edit-diff")).not.toBeInTheDocument(); } finally { localStorage.removeItem("nanobot-webui.settings-preferences"); } }); it("does not mount collapsed file edit diffs until opened", () => { localStorage.setItem( "nanobot-webui.settings-preferences", JSON.stringify({ fileEditDisplayMode: "collapsed_diff" }), ); try { render( ;", "+ return ;", ]), }], createdAt: 3, }]} isTurnStreaming={false} hasBodyBelow={false} />, ); const toggle = screen.getByTestId("file-edit-diff-toggle"); expect(toggle).toHaveAttribute("aria-expanded", "false"); expect(toggle).toHaveTextContent("View diff"); expect(toggle).toHaveTextContent("3 lines"); expect(screen.queryByTestId("file-edit-diff")).not.toBeInTheDocument(); expect(screen.queryByText("return ;")).not.toBeInTheDocument(); fireEvent.click(toggle); expect(toggle).toHaveAttribute("aria-expanded", "true"); expect(screen.getByTestId("file-edit-diff")).toBeInTheDocument(); expect(screen.getByText("return ;")).toBeInTheDocument(); } finally { localStorage.removeItem("nanobot-webui.settings-preferences"); } }); it("offers the file preview entry point when a diff payload is truncated", () => { localStorage.setItem( "nanobot-webui.settings-preferences", JSON.stringify({ fileEditDisplayMode: "diff" }), ); const onOpenFilePreview = vi.fn(); try { render( , ); const toggle = screen.getByTestId("file-edit-diff-toggle"); expect(toggle).toHaveAttribute("aria-expanded", "false"); expect(toggle).toHaveTextContent("View large diff"); expect(screen.queryByTestId("file-edit-diff-truncated")).not.toBeInTheDocument(); fireEvent.click(toggle); expect(screen.getByTestId("file-edit-diff-truncated")).toHaveTextContent("Diff truncated"); fireEvent.click(screen.getByTestId("file-edit-diff-open-file")); expect(onOpenFilePreview).toHaveBeenCalledWith("/repo/src/app.tsx"); } finally { localStorage.removeItem("nanobot-webui.settings-preferences"); } }); it("labels whole-file deletes as deleted instead of edited", () => { render( , ); expect(screen.getByText("Deleted")).toBeInTheDocument(); expect(screen.queryByText("Edited")).not.toBeInTheDocument(); }); it("renders file-only edits without a redundant disclosure", () => { render( , ); expect(screen.queryByRole("button", { name: /edited app\.tsx/i })).not.toBeInTheDocument(); expect(screen.queryByTestId("agent-activity-scroll")).not.toBeInTheDocument(); expect(screen.getByText("Edited")).toBeInTheDocument(); expect(screen.queryByTestId("activity-header-file-reference")).not.toBeInTheDocument(); expect(screen.getByTestId("activity-file-reference")).toHaveTextContent("src/app.tsx"); expect(screen.getByText("+12")).toBeInTheDocument(); expect(screen.getByText("-3")).toBeInTheDocument(); }); it("renders every file from one apply_patch call", () => { render( , ); const fileRefs = screen.getAllByTestId("activity-file-reference"); expect(fileRefs).toHaveLength(2); expect(fileRefs[0]).toHaveTextContent("USER.md"); expect(fileRefs[1]).toHaveTextContent("MEMORY.md"); }); it("renders CLI app runs as dedicated activity rows", () => { const line = 'run_cli_app({"name":"blender","args":["--background","scene.blend"],"json":true})'; render( , ); expect(screen.getByText("Using Blender · --json --background scene.blend")).toBeInTheDocument(); expect(screen.getByTestId("activity-cli-logo-blender")).toBeInTheDocument(); expect(screen.queryByText(/run_cli_app/)).not.toBeInTheDocument(); }); it("keeps CLI rows in chronological trace order", () => { const cliArgs = { name: "blender", args: ["project", "new"], json: true }; const cliLine = `run_cli_app(${JSON.stringify(cliArgs)})`; render( , ); const searchRow = screen.getByText("Searched nanobot architecture").closest('[data-testid="activity-step"]'); const cliRow = screen.getByText("Used Blender · --json project new").closest('[data-testid="activity-step"]'); const fetchRow = screen.getByText("example.com/diagram").closest('[data-testid="activity-step"]'); expect(searchRow).not.toBeNull(); expect(cliRow).not.toBeNull(); expect(fetchRow).not.toBeNull(); expect(searchRow!.compareDocumentPosition(cliRow!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(cliRow!.compareDocumentPosition(fetchRow!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); it("renders web search results as lightweight branded source rows", () => { const line = 'web_search({"query":"agent frameworks"})'; render( , ); expect(screen.getByText("Searched agent frameworks")).toBeInTheDocument(); expect(screen.queryByText("2 sources")).not.toBeInTheDocument(); const openAiLink = screen.getByText("OpenAI Agents SDK").closest("a"); const anthropicLink = screen.getByText("Building effective agents").closest("a"); expect(openAiLink).toHaveAttribute( "href", "https://openai.com/index/new-tools-for-building-agents/", ); expect(openAiLink).not.toHaveAttribute("title"); expect(anthropicLink).toHaveAttribute( "href", "https://www.anthropic.com/engineering/building-effective-agents", ); expect(screen.getByText("openai.com/index/new-tools-for-building-agents")).toBeInTheDocument(); expect(screen.getByText("anthropic.com/engineering/building-effective-agents")).toBeInTheDocument(); expect(screen.getByTestId("activity-web-favicon-openai.com")).toBeInTheDocument(); expect(screen.getByTestId("activity-web-favicon-anthropic.com")).toBeInTheDocument(); expect(screen.queryByText("Internal dashboard")).not.toBeInTheDocument(); expect(screen.queryByText("Build and deploy agentic applications.")).not.toBeInTheDocument(); const searchStep = screen.getByText("Searched agent frameworks").closest( '[data-testid="activity-step"]', ); const openAiStep = openAiLink!.closest('[data-testid="activity-step"]'); const anthropicStep = anthropicLink!.closest('[data-testid="activity-step"]'); expect(openAiStep).toContainElement( screen.getByText("openai.com/index/new-tools-for-building-agents"), ); expect(anthropicStep).toContainElement( screen.getByText("anthropic.com/engineering/building-effective-agents"), ); expect(searchStep?.parentElement).toBe(openAiStep?.parentElement); expect(searchStep?.parentElement).toBe(anthropicStep?.parentElement); expect(searchStep?.parentElement?.querySelector("ul, li, section")).toBeNull(); expect(screen.getAllByTestId("activity-step")).toHaveLength(3); }); it("renders hosted X search as an explicit search activity", () => { const line = 'x_search({"query":"nanobot oauth"})'; render( , ); expect(screen.getByText("Searched X · nanobot oauth")).toBeInTheDocument(); expect(screen.queryByText(/Completed X search/i)).not.toBeInTheDocument(); expect(screen.getAllByTestId("activity-step")).toHaveLength(1); }); it("redacts credentials from web search queries, titles, and links", () => { const query = "release notes access_token=signed-secret"; const line = `web_search(${JSON.stringify({ query })})`; render( , ); expect(screen.queryByText(/signed-secret|secret1234|url-secret/)).not.toBeInTheDocument(); expect(screen.getByText("Searched release notes access_token=")).toBeInTheDocument(); expect(screen.getByText("Release ")).toBeInTheDocument(); expect(screen.getByText("Release ").closest("a")).toHaveAttribute( "href", "https://example.com/release", ); }); it("renders persisted search progress as one human-readable action", () => { const line = 'web_search({"query":"site:linkedin.com/company Evomap startup"})'; render( , ); expect(screen.getAllByTestId("activity-step")).toHaveLength(1); expect(screen.getByText("Could not search LinkedIn · Evomap startup")).toBeInTheDocument(); expect(screen.queryByText(/site:linkedin/i)).not.toBeInTheDocument(); expect(screen.queryByText("Web research")).not.toBeInTheDocument(); }); it("renders reasoning as a single flat activity row", () => { render( , ); expect(screen.getByText("Planning a focused search for official sources")).toBeInTheDocument(); expect(screen.queryByText("Thinking…")).not.toBeInTheDocument(); expect(screen.queryByText("Thinking")).not.toBeInTheDocument(); }); it("labels rejected CLI app calls as failed instead of ran", () => { render( , ); fireEvent.click(screen.getByRole("button", { name: "Worked" })); const row = screen.getByText("Could not use GitHub · --json repo view").closest( '[data-testid="activity-step"]', ); expect(row).toBeInTheDocument(); expect(row).not.toHaveAttribute("title"); expect(screen.queryByText("Error: CLI app 'github' not found")).not.toBeInTheDocument(); expect(screen.queryByText("Ran CLI")).not.toBeInTheDocument(); }); it("renders MCP preset tool calls as branded activity rows", () => { render( , ); expect(screen.getByText("Opening example.com · Browserbase")).toBeInTheDocument(); expect(screen.queryByText("Using")).not.toBeInTheDocument(); expect(screen.queryByText(/browser_navigate/)).not.toBeInTheDocument(); expect(screen.getByTestId("activity-mcp-logo-browserbase")).toBeInTheDocument(); expect(screen.queryByText(/mcp_browserbase_browser_navigate/)).not.toBeInTheDocument(); }); it("renders public web fetch traces with the site favicon", () => { render( , ); const favicon = screen.getByTestId("activity-web-favicon-auth0.com"); expect(favicon).toHaveAttribute("src", expect.stringContaining("auth0.com")); const row = screen.getByText("auth0.com/blog/jwt-security-best-practices").closest( '[data-testid="activity-step"]', ); expect(row).toHaveTextContent("Reading"); }); it("renders plain-text fetch progress with the site favicon", () => { render( , ); expect(screen.getByTestId("activity-web-favicon-auth0.com")).toBeInTheDocument(); const row = screen.getByText("auth0.com/blog/jwt-security-best-practices").closest( '[data-testid="activity-step"]', ); expect(row).toHaveTextContent("Reading"); }); it("renders a completed fetch as one linked title and URL row", () => { const line = 'web_fetch({"url":"https://example.com/docs"})'; render( , ); const title = screen.getByText("Example documentation"); const url = screen.getByText("example.com/docs"); const row = title.closest('[data-testid="activity-step"]'); expect(row).toContainElement(url); expect(title.closest("a")).toHaveAttribute("href", "https://example.com/docs"); expect(screen.getAllByTestId("activity-step")).toHaveLength(1); }); it("does not request favicons for private web fetch targets", () => { render( , ); expect(screen.queryByTestId("activity-web-favicon-localhost")).not.toBeInTheDocument(); expect(screen.getByText("Reading Private address")).toBeInTheDocument(); expect(screen.queryByText("http://localhost:3000/dashboard")).not.toBeInTheDocument(); }); it("presents generic tool traces as one-line semantic actions", () => { render( , ); expect(screen.getByText("Found files *.tsx")).toBeInTheDocument(); expect(screen.getByText("Listed files memory")).toBeInTheDocument(); expect(screen.getByText("Searching files “dream_cursor”")).toBeInTheDocument(); expect(screen.queryByText("Technical details")).not.toBeInTheDocument(); expect(document.querySelector("details")).not.toBeInTheDocument(); }); it("groups repeated searches over internal tool results without exposing raw paths", () => { const pattern = "Jul (1[0-7]), 2026|July (1[0-7]), 2026|2026-07-(1[0-7])"; const secondPattern = "Anthropic|OpenAI|DeepMind"; const firstPath = "/Users/test/.nanobot/workspace/.nanobot/tool-results/websocket_session/call_first-result.txt"; const secondPath = "/Users/test/.nanobot/workspace/.nanobot/tool-results/websocket_session/call_second-result.txt"; const traces = [ `grep(${JSON.stringify({ pattern, path: firstPath })})`, `grep(${JSON.stringify({ pattern: secondPattern, path: secondPath })})`, ]; render( , ); const run = screen.getByText(/Reviewed sources.*2 files/).closest('[data-testid="activity-step"]'); expect(run).toBeInTheDocument(); expect(screen.queryByText(firstPath)).not.toBeInTheDocument(); expect(screen.queryByText(secondPath)).not.toBeInTheDocument(); expect(screen.queryByText(pattern)).not.toBeInTheDocument(); expect(screen.queryByText(secondPattern)).not.toBeInTheDocument(); expect(screen.queryByText("call_first-result.txt")).not.toBeInTheDocument(); expect(screen.queryByText("call_second-result.txt")).not.toBeInTheDocument(); }); it("surfaces generic tool failures without dumping their arguments", () => { const args = { pattern: "needle", path: "workspace/file.txt" }; const line = `grep(${JSON.stringify(args)})`; render( , ); const row = screen.getByText("Could not search files “needle”").closest( '[data-testid="activity-step"]', ); expect(row).toBeInTheDocument(); expect(row).not.toHaveAttribute("title"); expect(screen.queryByText(/Permission denied/)).not.toBeInTheDocument(); expect(screen.queryByText(/super-secret/)).not.toBeInTheDocument(); expect(screen.queryByText(/sk-live-secret/)).not.toBeInTheDocument(); expect(screen.queryByText(/Authorization/)).not.toBeInTheDocument(); }); it("redacts credentials from generic tool URL details", () => { const line = 'download_asset({"url":"https://user:password@example.com/file?access_token=signed-secret&format=png"})'; render( , ); expect(screen.queryByText(/password|signed-secret/)).not.toBeInTheDocument(); expect(screen.getByText("Completed Download asset")).toBeInTheDocument(); expect(screen.queryByText(/example\.com/)).not.toBeInTheDocument(); }); it("summarizes long shell traces instead of dumping scripts", () => { const command = [ "cat << 'EOF' | bash", "SECRET_TOKEN=sk-test", "for id in m1 m2 m3; do", " echo done $id", "done", "EOF", ].join("\n"); const line = `exec(${JSON.stringify({ command })})`; render( , ); fireEvent.click(screen.getByRole("button", { name: "Worked" })); expect(screen.getByText("Ran command cat << 'EOF' | bash · script, 6 lines")).toBeInTheDocument(); expect(screen.queryByText(/SECRET_TOKEN/)).not.toBeInTheDocument(); expect(screen.queryByText(/for id in/)).not.toBeInTheDocument(); expect(screen.queryByText(/^Done$/)).not.toBeInTheDocument(); }); it("presents time checks as an intent instead of a raw command", () => { const line = `exec(${JSON.stringify({ command: "date '+%Y-%m-%d %H:%M:%S %Z'" })})`; render( , ); expect(screen.getByText("Checking current time")).toBeInTheDocument(); expect(screen.queryByText(/%Y-%m-%d/)).not.toBeInTheDocument(); expect(screen.queryByText("Web")).not.toBeInTheDocument(); }); it("does not render zero diff counters for completed edits", () => { render( , ); expect(screen.getByText("Edited")).toBeInTheDocument(); expect(screen.queryByText("+0")).not.toBeInTheDocument(); expect(screen.queryByText("-0")).not.toBeInTheDocument(); }); it("drops stale pathless pending edits after the turn completes", () => { render( , ); expect(screen.queryByRole("button", { name: /preparing edit/i })).not.toBeInTheDocument(); expect(screen.queryByText("+98")).not.toBeInTheDocument(); expect(screen.queryByText("0 tool calls")).not.toBeInTheDocument(); }); it("renders pending file edit placeholders before the path is known", () => { render( , ); expect(screen.getByText("Preparing file edit…")).toBeInTheDocument(); }); it("shows the reason when a file edit fails", () => { render( , ); const row = screen.getByText("Could not edit").closest('[data-testid="activity-step"]'); expect(row).toBeInTheDocument(); expect(row).not.toHaveAttribute("title"); expect(screen.queryByText("Target text was not found in angry-birds.html.")).not.toBeInTheDocument(); }); it("keeps permission errors readable for failed file edits", () => { render( , ); const row = screen.getByText("Could not edit").closest('[data-testid="activity-step"]'); expect(row).toBeInTheDocument(); expect(row).not.toHaveAttribute("title"); expect(screen.queryByText("No permission to change this location.")).not.toBeInTheDocument(); expect(screen.queryByText(/\[Errno 13\]/)).not.toBeInTheDocument(); }); it("keeps repeated edits for the same path as separate actions", () => { localStorage.setItem( "nanobot-webui.settings-preferences", JSON.stringify({ fileEditDisplayMode: "diff" }), ); try { render( ", "+ ", ]), }, { call_id: "call-edit-2", tool: "edit_file", path: "minecraft-fps/index.html", phase: "error", added: 0, deleted: 0, approximate: false, status: "error", error: "patch failed", }, { call_id: "call-edit-3", tool: "edit_file", path: "minecraft-fps/index.html", phase: "end", added: 6, deleted: 6, approximate: false, status: "done", diff: unifiedFileDiff([ "--- minecraft-fps/index.html", "+++ minecraft-fps/index.html", "@@ -8,2 +8,2 @@", "-const fps = 30;", "+const fps = 60;", " start();", ]), }, ], createdAt: 3, })} isTurnStreaming={false} hasBodyBelow={false} />, ); const fileRefs = screen.getAllByTestId("activity-file-reference"); expect(fileRefs).toHaveLength(3); expect(fileRefs.every((ref) => ref.textContent?.includes("minecraft-fps/index.html"))).toBe(true); const failedRow = screen.getByText("Could not edit").closest( '[data-testid="activity-step"]', ); expect(failedRow).toBeInTheDocument(); expect(failedRow).not.toHaveAttribute("title"); expect(screen.queryByText("patch failed")).not.toBeInTheDocument(); expect(screen.getAllByTestId("file-edit-diff")).toHaveLength(2); expect(screen.getByText("")).toBeInTheDocument(); expect(screen.getByText("const fps = 60;")).toBeInTheDocument(); expect(screen.getAllByText("+2").length).toBeGreaterThan(0); expect(screen.getAllByText("-1").length).toBeGreaterThan(0); expect(screen.getAllByText("+6").length).toBeGreaterThan(0); expect(screen.getAllByText("-6").length).toBeGreaterThan(0); } finally { localStorage.removeItem("nanobot-webui.settings-preferences"); } }); it("keeps the latest failed attempt visible after an earlier edit succeeded", () => { render( , ); expect(screen.getByText("Edited")).toBeInTheDocument(); expect(screen.getByText("Could not edit")).toBeInTheDocument(); expect(screen.getAllByTestId("activity-file-reference")).toHaveLength(2); }); it("keeps tool event embeds out of the flat activity list", () => { render( , ); expect(screen.queryByText("Web")).not.toBeInTheDocument(); const row = screen.getByText("example.com").closest('[data-testid="activity-step"]'); expect(row).toHaveTextContent("Read"); expect(screen.queryByTestId("activity-evidence-preview")).not.toBeInTheDocument(); expect(screen.queryByText(/Found image/i)).not.toBeInTheDocument(); expect(screen.queryByRole("img", { name: "Homepage screenshot" })).not.toBeInTheDocument(); }); it("keeps image generation status to one activity line", () => { const message: UIMessage = { id: "image-run", role: "tool", kind: "trace", content: 'generate_image({"prompt":"an orange nanobot on a desk","aspect_ratio":"4:3"})', traces: ['generate_image({"prompt":"an orange nanobot on a desk","aspect_ratio":"4:3"})'], toolEvents: [{ phase: "start", call_id: "image-call", name: "generate_image", arguments: { prompt: "an orange nanobot on a desk", aspect_ratio: "4:3" }, }], createdAt: 1, }; const { rerender } = render( , ); expect(screen.getByText("Generating image")).toBeInTheDocument(); rerender( , ); expect(screen.getByText("Generated image")).toBeInTheDocument(); expect(screen.queryByRole("img", { name: "generated.png" })).not.toBeInTheDocument(); }); it("keeps image-generation failures visible and actionable", () => { render( , ); expect(screen.getByText("Could not generate image")).toBeInTheDocument(); const row = screen.getByText("Could not generate image").closest( '[data-testid="activity-step"]', ); expect(row).toBeInTheDocument(); expect(row).not.toHaveAttribute("title"); expect(screen.queryByText("Image provider quota exceeded")).not.toBeInTheDocument(); }); it("does not add a secondary evidence row when evidence is missing", () => { render( , ); expect(screen.queryByText("Vision")).not.toBeInTheDocument(); expect(screen.getByText("Captured screenshot")).toBeInTheDocument(); expect(screen.queryByTestId("activity-evidence-preview")).not.toBeInTheDocument(); expect(screen.queryByText("missing.png")).not.toBeInTheDocument(); }); it("keeps every default activity action on one structural line", () => { render( , ); const steps = screen.getAllByTestId("activity-step"); expect(steps.length).toBeGreaterThanOrEqual(3); for (const step of steps) { expect(step).toHaveClass("grid-cols-[1.125rem_minmax(0,1fr)]"); const line = step.children[1]?.firstElementChild; expect(line).toHaveClass("overflow-hidden"); expect(line).toHaveClass("whitespace-nowrap"); expect(step.querySelector("br")).not.toBeInTheDocument(); expect(step.querySelector('[data-testid="activity-evidence-preview"]')).not.toBeInTheDocument(); } expect(document.querySelector("details")).not.toBeInTheDocument(); expect(document.querySelector("ul, li, section")).not.toBeInTheDocument(); }); it("does not expose tool inputs or credentials in the activity surface", () => { const cliLine = 'run_cli_app({"name":"blender","args":["--token","xoxb-1234567890-secret","render"],"json":true})'; const mcpLine = 'mcp_browserbase_browser_fill({"element":"Password","text":"mcp-private-value"})'; const genericLine = 'third_party_sync({"token":"sk-proj-1234567890-secret","payload":"private-payload"})'; const { container } = render( , ); expect(container.innerHTML).not.toContain("xoxb-1234567890-secret"); expect(container.innerHTML).not.toContain("mcp-private-value"); expect(container.innerHTML).not.toContain("sk-proj-1234567890-secret"); expect(container.innerHTML).not.toContain("private-payload"); expect(container.textContent).not.toMatch(/run_cli_app\(|browser_fill\(|third_party_sync\(/); expect(container.textContent).toMatch(/|••••/); }); });