// @vitest-environment jsdom // // Regression test: when the Claude CLI is not detected locally (typical of // remote OmniRoute deployments where the CLI lives on the user's laptop, not // on the server), the card must still surface a "Manual Config" button so the // user can copy the settings.json snippet and paste it into the CLI on their // local machine. Before this fix the Manual Config button only rendered when // `cliReady === true`, which made the card useless for remote deployments // (upstream report: decolua/9router#589). import React from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // ── Mocks ───────────────────────────────────────────────────────────────────── vi.mock("next-intl", () => ({ useTranslations: () => (key: string, values?: Record) => { const messages: Record = { cliNotInstalled: "{tool} CLI not detected locally", cliNotRunnable: "{tool} CLI installed but not runnable", installCliPrompt: "Manual configuration is still available if OmniRoute is deployed on a remote server.", cliFoundFailedHealthcheck: "{tool} CLI was found but failed runtime healthcheck{reason}.", howToInstall: "How to Install", hide: "Hide", manualConfig: "Manual Config", installationGuide: "Installation Guide", platforms: "Platforms", afterInstallationRun: "After installation run", toVerify: "to verify.", checkingCli: "Checking {tool}...", claudeManualConfiguration: "Claude Manual Configuration", }; const raw = messages[key] ?? key; if (!values) return raw; return Object.entries(values).reduce( (acc, [k, v]) => acc.replaceAll(`{${k}}`, String(v ?? "")), raw ); }, useLocale: () => "en", })); vi.mock("@/shared/components/ProviderIcon", () => ({ default: () => , })); vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({ default: () => , })); vi.mock("@/shared/services/claudeCliConfig", () => ({ getStoredClaudeAuthValue: () => null, normalizeClaudeBaseUrl: (u: string) => u, })); // Surface ManualConfigModal as a marker so we can assert it gets rendered with // isOpen=true after clicking the new Manual Config button. vi.mock("@/shared/components", async () => { const React = await import("react"); return { Card: ({ children }: { children: React.ReactNode }) =>
{children}
, Button: ({ children, onClick, }: { children: React.ReactNode; onClick?: () => void; }) => ( ), ModelSelectModal: () => null, ManualConfigModal: ({ isOpen, title }: { isOpen: boolean; title?: string }) => isOpen ?
{title}
: null, }; }); // ── Fetch stub ──────────────────────────────────────────────────────────────── beforeEach(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; // Return: CLI not installed (the broken-on-remote case from upstream #589). globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => { const url = typeof input === "string" ? input : input.toString(); if (url.includes("/api/cli-tools/claude-settings")) { return new Response(JSON.stringify({ installed: false }), { status: 200 }); } if (url.includes("/api/models/alias")) { return new Response(JSON.stringify({ aliases: {} }), { status: 200 }); } if (url.includes("/api/cli-tools/backups")) { return new Response(JSON.stringify({ backups: [] }), { status: 200 }); } return new Response("{}", { status: 200 }); }) as unknown as typeof fetch; }); const containers: HTMLElement[] = []; afterEach(() => { while (containers.length > 0) { containers.pop()?.remove(); } document.body.innerHTML = ""; vi.restoreAllMocks(); }); // ── Import under test (after mocks) ─────────────────────────────────────────── const { default: ClaudeToolCard } = await import( "@/app/(dashboard)/dashboard/cli-code/components/ClaudeToolCard" ); async function renderExpanded() { const container = document.createElement("div"); document.body.appendChild(container); containers.push(container); const root = createRoot(container); await act(async () => { root.render( {}} activeProviders={[]} modelMappings={{}} onModelMappingChange={() => {}} baseUrl="http://localhost:20128" hasActiveProviders={false} apiKeys={[]} cloudEnabled={false} batchStatus={null} lastConfiguredAt={null} /> ); }); // Allow microtasks for the fetch() promise + state update to flush. await act(async () => { await Promise.resolve(); await Promise.resolve(); }); return container; } describe("ClaudeToolCard — manual-config CTA when CLI is not detected", () => { it("renders the Manual Config button alongside How to Install when CLI is not installed", async () => { const container = await renderExpanded(); const buttons = Array.from(container.querySelectorAll("button")); const labels = buttons.map((b) => b.textContent ?? ""); expect(labels.some((l) => l.includes("Manual Config"))).toBe(true); expect(labels.some((l) => l.includes("How to Install"))).toBe(true); }); it("opens the ManualConfigModal when the Manual Config button is clicked", async () => { const container = await renderExpanded(); const manualBtn = Array.from(container.querySelectorAll("button")).find((b) => (b.textContent ?? "").includes("Manual Config") ); expect(manualBtn).toBeTruthy(); await act(async () => { manualBtn!.click(); }); expect(container.querySelector("[data-testid='manual-config-modal']")).not.toBeNull(); }); });