diff --git a/src/app/(dashboard)/dashboard/agents/page.tsx b/src/app/(dashboard)/dashboard/acp-agents/page.tsx similarity index 61% rename from src/app/(dashboard)/dashboard/agents/page.tsx rename to src/app/(dashboard)/dashboard/acp-agents/page.tsx index ac1c417b15..79d4d90f34 100644 --- a/src/app/(dashboard)/dashboard/agents/page.tsx +++ b/src/app/(dashboard)/dashboard/acp-agents/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { Card, Button, Input } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; +import { CliConceptCard, CliComparisonCard } from "@/shared/components/cli"; import { useTranslations } from "next-intl"; interface AgentInfo { @@ -65,7 +66,7 @@ export default function AgentsPage() { versionCommand: "", spawnArgs: "", }); - const t = useTranslations("agents"); + const t = useTranslations("acpAgents"); const fetchAgents = useCallback(async () => { try { @@ -160,174 +161,8 @@ export default function AgentsPage() { - -
-
-
-

{t("architectureTitle")}

-

{t("architectureDescription")}

-
- - open_in_new - {t("cliToolsRedirectCta")} - -
-
- - {t("flowOmniRoute")} - - - arrow_forward - - - {t("flowSpawn")} - - - arrow_forward - - - {t("flowLocalBinary")} - - - arrow_forward - - - {t("flowExecute")} - -
-
-
-
-
- - devices - -
-

{t("flowDiagramClient")}

-

{t("flowDiagramClientDesc")}

-
-
- - arrow_forward - -
-
-
- hub -
-

{t("flowDiagramOmniRoute")}

-

- {t("flowDiagramOmniRouteDesc")} -

-
-
- - arrow_forward - -
-
-
- - launch - -
-

- {t("flowDiagramSpawn")} -

-

{t("flowDiagramSpawnDesc")}

-
-
- - arrow_forward - -
-
-
- - terminal - -
-

- {t("flowDiagramCli")} -

-

{t("flowDiagramCliDesc")}

-
-
-
-
- {t("cliToolsRedirectTitle")}{" "} - {t("cliToolsRedirectDesc")}{" "} - - {t("openCliTools")} - - . -
-
-
- - -
-
-
- -
-

{t("comparisonTitle")}

-
- -
-
-
- - arrow_forward - -

- {t("comparisonCliToolsLabel")} -

-
-

- {t("comparisonCliToolsTitle")} -

-

{t("comparisonCliToolsDesc")}

-
- IDE - arrow_forward - OmniRoute - arrow_forward - Provider API -
-
- -
-
- - arrow_back - -

- {t("comparisonAgentsLabel")} -

-
-

- {t("comparisonAgentsTitle")} -

-

{t("comparisonAgentsDesc")}

-
- Client - arrow_forward - OmniRoute - arrow_forward - CLI Binary -
-
-
- -

{t("comparisonSummary")}

-
-
+ + {/* Summary Cards */} {summary && ( @@ -363,10 +198,10 @@ export default function AgentsPage() {

{t("setupGuideTitle")}

- {t("openCliTools")} + {t("cliCodeRedirectCta")}
diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index 251bd0d1d6..f6cf0eb4dc 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -14,7 +14,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [ "context-combos", // OmniProxy > Tools "cli-tools", - "agents", + "acp-agents", "cloud-agents", // OmniProxy > Integrations "api-endpoints", @@ -236,10 +236,10 @@ const TOOLS_GROUP: SidebarItemGroup = { icon: "terminal", }, { - id: "agents", - href: "/dashboard/agents", - i18nKey: "agents", - subtitleKey: "agentsSubtitle", + id: "acp-agents", + href: "/dashboard/acp-agents", + i18nKey: "acpAgents", + subtitleKey: "acpAgentsSubtitle", icon: "smart_toy", }, { @@ -802,7 +802,7 @@ const DEVELOPER_SHOWN: ReadonlySet = new Set([ "context-rtk", "context-combos", "cli-tools", - "agents", + "acp-agents", "api-endpoints", "analytics", "analytics-combo-health", diff --git a/tests/unit/ui/AcpAgentsPage.test.tsx b/tests/unit/ui/AcpAgentsPage.test.tsx new file mode 100644 index 0000000000..c823701f0c --- /dev/null +++ b/tests/unit/ui/AcpAgentsPage.test.tsx @@ -0,0 +1,195 @@ +// @vitest-environment jsdom +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// ── Mocks ───────────────────────────────────────────────────────────────────── + +let capturedTranslationsNamespace = ""; + +vi.mock("next/link", () => ({ + default: ({ + href, + children, + ...props + }: React.AnchorHTMLAttributes & { href: string }) => ( + + {children} + + ), +})); + +vi.mock("next-intl", () => ({ + useTranslations: (ns: string) => { + capturedTranslationsNamespace = ns; + return (key: string) => key; + }, +})); + +vi.mock("@/shared/components", () => ({ + Card: ({ children, ...props }: React.HTMLAttributes) => ( +
+ {children} +
+ ), + Button: ({ + children, + onClick, + loading, + ...props + }: React.ButtonHTMLAttributes & { loading?: boolean }) => ( + + ), + Input: ({ + label, + ...props + }: React.InputHTMLAttributes & { label?: string }) => ( + + ), +})); + +vi.mock("@/shared/components/ProviderIcon", () => ({ + default: ({ providerId }: { providerId: string; size?: number; type?: string }) => ( + + ), +})); + +vi.mock("@/shared/components/cli", () => ({ + CliConceptCard: ({ currentType }: { currentType: string }) => ( +
+ ), + CliComparisonCard: ({ currentType }: { currentType: string }) => ( +
+ ), +})); + +// ── Fetch mock ──────────────────────────────────────────────────────────────── + +const mockAgents = [ + { + id: "claude-code", + name: "Claude Code", + binary: "claude", + version: "1.2.3", + installed: true, + protocol: "stdio", + isCustom: false, + }, + { + id: "codex", + name: "Codex", + binary: "codex", + version: null, + installed: false, + protocol: "stdio", + isCustom: false, + }, +]; + +const mockSummary = { + total: 2, + installed: 1, + notFound: 1, + builtIn: 2, + custom: 0, +}; + +const mockFetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ agents: mockAgents, summary: mockSummary }), +}); + +(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = mockFetch; + +// ── Import after mocks ──────────────────────────────────────────────────────── + +const { default: AcpAgentsPage } = await import( + "@/app/(dashboard)/dashboard/acp-agents/page" +); + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +const containers: HTMLElement[] = []; + +async function renderPage(): Promise { + const container = document.createElement("div"); + document.body.appendChild(container); + containers.push(container); + + const root = createRoot(container); + await act(async () => { + root.render(); + }); + // Allow data-fetching effects to resolve + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + return container; +} + +// ── Lifecycle ───────────────────────────────────────────────────────────────── + +beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + capturedTranslationsNamespace = ""; + mockFetch.mockClear(); +}); + +afterEach(() => { + while (containers.length > 0) { + containers.pop()?.remove(); + } + document.body.innerHTML = ""; +}); + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe("AcpAgentsPage", () => { + it("smoke: renders without crashing", async () => { + const container = await renderPage(); + expect(container).toBeTruthy(); + expect(container.innerHTML.length).toBeGreaterThan(0); + }); + + it("calls useTranslations with 'acpAgents' namespace", async () => { + await renderPage(); + expect(capturedTranslationsNamespace).toBe("acpAgents"); + }); + + it("renders ", async () => { + const container = await renderPage(); + const card = container.querySelector("[data-testid='cli-concept-card']"); + expect(card).not.toBeNull(); + expect(card?.getAttribute("data-current-type")).toBe("acp"); + }); + + it("renders ", async () => { + const container = await renderPage(); + const card = container.querySelector("[data-testid='cli-comparison-card']"); + expect(card).not.toBeNull(); + expect(card?.getAttribute("data-current-type")).toBe("acp"); + }); + + it("cross-link points to /dashboard/cli-code (not /dashboard/cli-tools)", async () => { + const container = await renderPage(); + const links = container.querySelectorAll("a"); + const hrefs = Array.from(links).map((a) => a.getAttribute("href")); + const cliCodeLinks = hrefs.filter((h) => h === "/dashboard/cli-code"); + const cliToolsLinks = hrefs.filter((h) => h === "/dashboard/cli-tools"); + expect(cliCodeLinks.length).toBeGreaterThan(0); + expect(cliToolsLinks).toHaveLength(0); + }); + + it("agent grid renders with mocked /api/acp/agents response", async () => { + const container = await renderPage(); + expect(mockFetch).toHaveBeenCalledWith("/api/acp/agents"); + // Agent names from mock should appear somewhere in the rendered output + expect(container.textContent).toContain("Claude Code"); + expect(container.textContent).toContain("Codex"); + }); +});