feat(mcp): add preset setup and capability mentions
This commit is contained in:
@@ -2,7 +2,7 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
|
||||
import type { CliAppInfo, UIMessage } from "@/lib/types";
|
||||
import type { CliAppInfo, McpPresetInfo, UIMessage } from "@/lib/types";
|
||||
|
||||
const BLENDER_CLI_APP: CliAppInfo = {
|
||||
name: "blender",
|
||||
@@ -21,6 +21,26 @@ const BLENDER_CLI_APP: CliAppInfo = {
|
||||
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 activityMessages(extraReasoning = "", extraTool?: UIMessage): UIMessage[] {
|
||||
const rows: UIMessage[] = [
|
||||
{
|
||||
@@ -120,7 +140,6 @@ describe("AgentActivityCluster", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /working/i }));
|
||||
const scrollport = screen.getByTestId("agent-activity-scroll");
|
||||
setScrollGeometry(scrollport, {
|
||||
scrollHeight: 1000,
|
||||
@@ -149,7 +168,6 @@ describe("AgentActivityCluster", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /working/i }));
|
||||
const scrollport = screen.getByTestId("agent-activity-scroll");
|
||||
setScrollGeometry(scrollport, {
|
||||
scrollHeight: 1000,
|
||||
@@ -201,7 +219,6 @@ describe("AgentActivityCluster", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /working/i }));
|
||||
const scrollport = screen.getByTestId("agent-activity-scroll");
|
||||
setScrollGeometry(scrollport, {
|
||||
scrollHeight: 1000,
|
||||
@@ -238,6 +255,44 @@ describe("AgentActivityCluster", () => {
|
||||
}
|
||||
});
|
||||
|
||||
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(
|
||||
<AgentActivityCluster
|
||||
messages={[liveReasoning]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("activity-reasoning-marker")).toHaveAttribute("data-state", "thinking");
|
||||
|
||||
rerender(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
...liveReasoning,
|
||||
reasoningStreaming: false,
|
||||
isStreaming: false,
|
||||
}]}
|
||||
isTurnStreaming={false}
|
||||
hasBodyBelow
|
||||
/>,
|
||||
);
|
||||
|
||||
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("renders file edit totals and a compact expanded file list", async () => {
|
||||
const restoreMotion = installReducedMotion();
|
||||
try {
|
||||
@@ -306,16 +361,68 @@ describe("AgentActivityCluster", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /running cli @blender/i }));
|
||||
|
||||
const cliRuns = screen.getByTestId("activity-cli-runs");
|
||||
expect(cliRuns).toHaveTextContent("Running CLI");
|
||||
expect(cliRuns).toHaveTextContent("Using");
|
||||
expect(cliRuns).toHaveTextContent("@blender");
|
||||
expect(cliRuns).toHaveTextContent("--json --background scene.blend");
|
||||
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(
|
||||
<AgentActivityCluster
|
||||
messages={[
|
||||
{
|
||||
id: "t-search",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: 'web_search({"query":"nanobot architecture"})',
|
||||
traces: ['web_search({"query":"nanobot architecture"})'],
|
||||
createdAt: 1,
|
||||
},
|
||||
{
|
||||
id: "t-cli",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: cliLine,
|
||||
traces: [cliLine],
|
||||
toolEvents: [{
|
||||
phase: "end",
|
||||
call_id: "call-blender",
|
||||
name: "run_cli_app",
|
||||
arguments: cliArgs,
|
||||
}],
|
||||
createdAt: 2,
|
||||
},
|
||||
{
|
||||
id: "t-fetch",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: 'web_fetch({"url":"https://example.com/diagram"})',
|
||||
traces: ['web_fetch({"url":"https://example.com/diagram"})'],
|
||||
createdAt: 3,
|
||||
},
|
||||
]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow={false}
|
||||
cliApps={[BLENDER_CLI_APP]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const searchRow = screen.getByText("Searching").closest("li");
|
||||
const cliRow = screen.getByText("@blender").closest("li");
|
||||
const fetchRow = screen.getByText("Reading").closest("li");
|
||||
|
||||
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("labels rejected CLI app calls as failed instead of ran", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
@@ -341,14 +448,143 @@ describe("AgentActivityCluster", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /cli failed @github/i }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /failed @github/i }));
|
||||
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("CLI failed");
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("Failed");
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("@github");
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("Error: CLI app 'github' not found");
|
||||
expect(screen.queryByText("Ran CLI")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders MCP preset tool calls as branded activity rows", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-mcp",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: "mcp_browserbase_browser_navigate()",
|
||||
traces: ["mcp_browserbase_browser_navigate({\"url\":\"https://example.com\"})"],
|
||||
toolEvents: [
|
||||
{
|
||||
phase: "start",
|
||||
call_id: "call-browserbase",
|
||||
name: "mcp_browserbase_browser_navigate",
|
||||
arguments: { url: "https://example.com" },
|
||||
},
|
||||
],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow={false}
|
||||
mcpPresets={[BROWSERBASE_MCP]}
|
||||
/>,
|
||||
);
|
||||
|
||||
const mcpRuns = screen.getByTestId("activity-mcp-runs");
|
||||
expect(mcpRuns).toHaveTextContent("Using");
|
||||
expect(mcpRuns).toHaveTextContent("Browserbase");
|
||||
expect(mcpRuns).toHaveTextContent("browser_navigate");
|
||||
expect(mcpRuns).toHaveTextContent("url: https://example.com");
|
||||
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(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-web-fetch",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: 'web_fetch({"url":"https://auth0.com/blog/jwt-security-best-practices"})',
|
||||
traces: ['web_fetch({"url":"https://auth0.com/blog/jwt-security-best-practices"})'],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const favicon = screen.getByTestId("activity-web-favicon-auth0.com");
|
||||
expect(favicon.querySelector("img")?.getAttribute("src")).toContain("auth0.com");
|
||||
expect(screen.getByText("Reading")).toBeInTheDocument();
|
||||
expect(screen.getByText("auth0.com/blog/jwt-security-best-practices")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders plain-text fetch progress with the site favicon", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-web-fetch-text",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: "Fetching https://auth0.com/blog/jwt-security-best-practices",
|
||||
traces: ["Fetching https://auth0.com/blog/jwt-security-best-practices"],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("activity-web-favicon-auth0.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reading")).toBeInTheDocument();
|
||||
expect(screen.getByText("auth0.com/blog/jwt-security-best-practices")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not request favicons for private web fetch targets", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-web-fetch-local",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: 'web_fetch({"url":"http://localhost:3000/dashboard"})',
|
||||
traces: ['web_fetch({"url":"http://localhost:3000/dashboard"})'],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("activity-web-favicon-localhost")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("url: http://localhost:3000/dashboard")).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(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-shell",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: line,
|
||||
traces: [line],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming={false}
|
||||
hasBodyBelow
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Shell")).toBeInTheDocument();
|
||||
expect(screen.getByText(/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("does not render zero diff counters for completed edits", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
@@ -440,7 +676,6 @@ describe("AgentActivityCluster", () => {
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: /preparing edit/i })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: /preparing edit/i }));
|
||||
expect(screen.getByText("Preparing file edit…")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user