chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+195
View File
@@ -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<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next-intl", () => ({
useTranslations: (ns: string) => {
capturedTranslationsNamespace = ns;
return (key: string) => key;
},
}));
vi.mock("@/shared/components", () => ({
Card: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div data-testid="card" {...props}>
{children}
</div>
),
Button: ({
children,
onClick,
loading,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { loading?: boolean }) => (
<button onClick={onClick} disabled={loading} {...props}>
{children}
</button>
),
Input: ({
label,
...props
}: React.InputHTMLAttributes<HTMLInputElement> & { label?: string }) => (
<input aria-label={label} {...props} />
),
}));
vi.mock("@/shared/components/ProviderIcon", () => ({
default: ({ providerId }: { providerId: string; size?: number; type?: string }) => (
<span data-testid="provider-icon" data-provider={providerId} />
),
}));
vi.mock("@/shared/components/cli", () => ({
CliConceptCard: ({ currentType }: { currentType: string }) => (
<div data-testid="cli-concept-card" data-current-type={currentType} />
),
CliComparisonCard: ({ currentType }: { currentType: string }) => (
<div data-testid="cli-comparison-card" data-current-type={currentType} />
),
}));
// ── 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<HTMLElement> {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
await act(async () => {
root.render(<AcpAgentsPage />);
});
// 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 <CliConceptCard currentType='acp' />", 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 <CliComparisonCard currentType='acp' />", 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");
});
});
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
// Source-level parity check (#241): AntigravityToolCard must surface
// API-Key-compatible / custom OpenAI-compatible providers in its model picker.
// Those provider groups in <ModelSelectModal> are derived from `modelAliases`
// — without the prop, custom-keyed providers are silently hidden even when
// active. The fix mirrors the pattern already used by every sibling CLI tool
// card (Codex, Claude, Cline, Kilo, Droid, OpenClaw, HermesAgent).
const __dirname = dirname(fileURLToPath(import.meta.url));
const CARD_PATH = resolve(
__dirname,
"../../../src/app/(dashboard)/dashboard/cli-code/components/AntigravityToolCard.tsx"
);
describe("AntigravityToolCard model alias wiring", () => {
const source = readFileSync(CARD_PATH, "utf8");
it("declares modelAliases state", () => {
expect(source).toMatch(/useState\(\{\}\)/);
expect(source).toMatch(/setModelAliases/);
});
it("fetches /api/models/alias when expanded", () => {
expect(source).toContain('fetch("/api/models/alias")');
expect(source).toMatch(/fetchModelAliases\s*\(\s*\)/);
});
it("passes modelAliases prop to ModelSelectModal", () => {
// Regression guard for upstream parity: the prop is what unlocks the
// API-Key-compatible / passthrough provider groups in the picker.
expect(source).toMatch(/modelAliases=\{modelAliases\}/);
});
});
@@ -0,0 +1,87 @@
// @vitest-environment jsdom
//
// UI test for the opt-in Claude Code auto-permission classifier compat toggle.
// Verifies it reads the current mode from GET /api/settings, renders "off" by
// default, and cycles off → auto → always by PATCHing /api/settings.
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let currentMode = "off";
const patchBodies: Array<Record<string, unknown>> = [];
beforeEach(() => {
currentMode = "off";
patchBodies.length = 0;
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
globalThis.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/api/settings")) {
if (init?.method === "PATCH") {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
patchBodies.push(body);
currentMode = String(body.claudeClassifierCompat);
return new Response(JSON.stringify({ claudeClassifierCompat: currentMode }), {
status: 200,
});
}
return new Response(JSON.stringify({ claudeClassifierCompat: currentMode }), { 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();
});
const { default: ClaudeClassifierCompatToggle } = await import(
"@/app/(dashboard)/dashboard/cli-code/components/ClaudeClassifierCompatToggle"
);
async function renderToggle() {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
await act(async () => {
root.render(<ClaudeClassifierCompatToggle />);
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
return container;
}
describe("ClaudeClassifierCompatToggle", () => {
it("renders the current mode (off by default)", async () => {
const container = await renderToggle();
const btn = container.querySelector("button");
expect(btn).toBeTruthy();
expect((btn!.textContent ?? "").trim().toLowerCase()).toBe("off");
});
it("cycles off → auto on click and PATCHes /api/settings", async () => {
const container = await renderToggle();
const btn = container.querySelector("button")!;
await act(async () => {
btn.click();
});
await act(async () => {
await Promise.resolve();
});
expect(patchBodies).toHaveLength(1);
expect(patchBodies[0].claudeClassifierCompat).toBe("auto");
expect((btn.textContent ?? "").trim().toLowerCase()).toBe("auto");
});
});
@@ -0,0 +1,176 @@
// @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<string, unknown>) => {
const messages: Record<string, string> = {
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: () => <span data-testid="provider-icon" />,
}));
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
default: () => <span data-testid="status-badge" />,
}));
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 }) => <div>{children}</div>,
Button: ({
children,
onClick,
}: {
children: React.ReactNode;
onClick?: () => void;
}) => (
<button type="button" onClick={onClick}>
{children}
</button>
),
ModelSelectModal: () => null,
ManualConfigModal: ({ isOpen, title }: { isOpen: boolean; title?: string }) =>
isOpen ? <div data-testid="manual-config-modal">{title}</div> : 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(
<ClaudeToolCard
tool={{
name: "Claude Code",
defaultModels: [],
}}
isExpanded={true}
onToggle={() => {}}
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();
});
});
+263
View File
@@ -0,0 +1,263 @@
// @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";
import type { ToolBatchStatusMap } from "@/shared/types/cliBatchStatus";
// ── Mocks (declared before any imports that depend on them) ───────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next/image", () => ({
default: ({
src,
alt,
...props
}: React.ImgHTMLAttributes<HTMLImageElement> & { src: string; alt: string }) => (
// eslint-disable-next-line @next/next/no-img-element
<img src={src} alt={alt} {...props} />
),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
// Stub CliStatusBadge so it doesn't depend on next-intl internals
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
default: ({
effectiveConfigStatus,
}: {
effectiveConfigStatus: string | null;
batchStatus: null;
lastConfiguredAt: string | null;
}) => <span data-testid="status-badge">{effectiveConfigStatus}</span>,
}));
// ── Static imports after mocks ────────────────────────────────────────────────
const { default: CliAgentsPageClient } = await import(
"@/app/(dashboard)/dashboard/cli-agents/CliAgentsPageClient"
);
// ── Fixtures ──────────────────────────────────────────────────────────────────
/** 6 agent tool ids from the catalog (§3.2 of plan-14) */
const AGENT_IDS = [
"openclaw",
"hermes-agent",
"goose",
"interpreter",
"warp",
"agent-deck",
] as const;
function makeBatchStatusMap(overrides: Partial<ToolBatchStatusMap> = {}): ToolBatchStatusMap {
const base: ToolBatchStatusMap = {};
for (const id of AGENT_IDS) {
base[id] = {
detection: { installed: true, runnable: true, version: "1.0.0" },
config: { status: "configured", endpoint: "http://localhost:20128", lastConfiguredAt: null },
};
}
return { ...base, ...overrides };
}
function makeFetch(data: unknown, status = 200): typeof fetch {
return vi.fn(() =>
Promise.resolve({
ok: status >= 200 && status < 300,
status,
json: () => Promise.resolve(data),
text: () => Promise.resolve(String(data)),
} as Response)
);
}
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: ReturnType<typeof createRoot>[] = [];
async function renderPage(mockFetchFn?: typeof fetch): Promise<HTMLElement> {
vi.stubGlobal("fetch", mockFetchFn ?? makeFetch(makeBatchStatusMap()));
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
await act(async () => {
root.render(<CliAgentsPageClient machineId="test-machine" />);
await new Promise((r) => setTimeout(r, 100));
});
return container;
}
function countAgentCards(container: HTMLElement): number {
return Array.from(container.querySelectorAll<HTMLAnchorElement>("a[href]")).filter((a) =>
a.getAttribute("href")?.startsWith("/dashboard/cli-agents/")
).length;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
act(() => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
vi.restoreAllMocks();
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliAgentsPageClient", () => {
it("1. smoke render — mounts without crash and shows page title key", async () => {
const container = await renderPage();
expect(container.textContent).toContain("pageTitle");
}, 15000);
it("2. renders exactly 6 agent tool cards", async () => {
const container = await renderPage();
expect(countAgentCards(container)).toBe(6);
}, 15000);
it("3. search filter — 'hermes' shows 1 card (hermes-agent)", async () => {
const container = await renderPage();
const input = container.querySelector("input[type='search']") as HTMLInputElement;
expect(input).not.toBeNull();
await act(async () => {
// Use native value setter to trigger React's synthetic onChange
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
nativeSetter?.call(input, "hermes");
input.dispatchEvent(new Event("change", { bubbles: true }));
await new Promise((r) => setTimeout(r, 50));
});
const visibleCards = countAgentCards(container);
expect(visibleCards).toBe(1);
const remainingHrefs = Array.from(
container.querySelectorAll<HTMLAnchorElement>("a[href]")
)
.filter((a) => a.getAttribute("href")?.startsWith("/dashboard/cli-agents/"))
.map((a) => a.getAttribute("href") ?? "");
expect(remainingHrefs[0]).toContain("hermes");
}, 15000);
it("4. detection filter 'not_installed' — shows only non-installed tools", async () => {
// Only hermes-agent is not installed
const map = makeBatchStatusMap({
"hermes-agent": {
detection: { installed: false, runnable: false },
config: { status: "not_installed", endpoint: null, lastConfiguredAt: null },
},
});
const container = await renderPage(makeFetch(map));
const select = container.querySelector("select") as HTMLSelectElement;
expect(select).not.toBeNull();
await act(async () => {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLSelectElement.prototype,
"value"
)?.set;
nativeSetter?.call(select, "not_installed");
select.dispatchEvent(new Event("change", { bubbles: true }));
await new Promise((r) => setTimeout(r, 50));
});
expect(countAgentCards(container)).toBe(1);
const href = Array.from(container.querySelectorAll<HTMLAnchorElement>("a[href]"))
.find((a) => a.getAttribute("href")?.startsWith("/dashboard/cli-agents/"))
?.getAttribute("href");
expect(href).toContain("hermes-agent");
}, 15000);
it("5. empty state — shows data-testid='empty-state' when no tools match search", async () => {
const container = await renderPage();
await act(async () => {
const input = container.querySelector("input[type='search']") as HTMLInputElement;
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
nativeSetter?.call(input, "zzznothingmatchesxyz");
input.dispatchEvent(new Event("change", { bubbles: true }));
await new Promise((r) => setTimeout(r, 50));
});
const emptyState = container.querySelector("[data-testid='empty-state']");
expect(emptyState).not.toBeNull();
}, 15000);
it("6. CliConceptCard currentType='agent' — concept.agent.title key is present", async () => {
const container = await renderPage();
// CliConceptCard renders "concept.agent.title" via the mock translator
expect(container.textContent).toContain("concept.agent.title");
}, 15000);
it("7. CliComparisonCard currentType='agent' — comparison.agent.title + Esta página ✓", async () => {
const container = await renderPage();
// CliComparisonCard renders comparison.agent.title for the current column
expect(container.textContent).toContain("comparison.agent.title");
// thisPage badge appears for the agent column
expect(container.textContent).toContain("comparison.thisPage");
expect(container.textContent).toContain("✓");
}, 15000);
it("8. refresh button calls refetch — triggers additional fetch call", async () => {
const mockFetchFn = makeFetch(makeBatchStatusMap());
const container = await renderPage(mockFetchFn);
const callsAfterMount = (mockFetchFn as ReturnType<typeof vi.fn>).mock.calls.length;
expect(callsAfterMount).toBeGreaterThan(0);
const refreshBtn = container.querySelector<HTMLButtonElement>("button[aria-label]");
expect(refreshBtn).not.toBeNull();
await act(async () => {
refreshBtn!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await new Promise((r) => setTimeout(r, 100));
});
expect((mockFetchFn as ReturnType<typeof vi.fn>).mock.calls.length).toBeGreaterThan(
callsAfterMount
);
}, 15000);
});
+467
View File
@@ -0,0 +1,467 @@
// @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";
import { EXPECTED_CODE_COUNT } from "@/shared/schemas/cliCatalog";
import type { ToolBatchStatusMap } from "@/shared/types/cliBatchStatus";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next-intl", () => ({
useTranslations: (ns: string) => (key: string) => `${ns}.${key}`,
useLocale: () => "en",
}));
// Mock CLI components so tests don't pull in their heavy dependencies
vi.mock("@/shared/components/cli", () => ({
CliToolCard: ({
tool,
detailHref,
}: {
tool: { name: string };
batchStatus: unknown;
detailHref: string;
hasActiveProviders: boolean;
}) => (
<div data-testid="cli-tool-card" data-href={detailHref}>
{tool.name}
</div>
),
CliConceptCard: ({ currentType }: { currentType: string }) => (
<div data-testid="cli-concept-card" data-type={currentType} />
),
CliComparisonCard: ({ currentType }: { currentType: string }) => (
<div data-testid="cli-comparison-card" data-type={currentType} />
),
}));
// Mock shared components to avoid CSS/animation deps
vi.mock("@/shared/components", () => ({
Button: ({
children,
onClick,
}: {
children: React.ReactNode;
onClick?: () => void;
[key: string]: unknown;
}) => (
<button data-testid="button" onClick={onClick}>
{children}
</button>
),
CardSkeleton: () => <div data-testid="card-skeleton" />,
Input: ({ placeholder, value, onChange }: React.InputHTMLAttributes<HTMLInputElement>) => (
<input data-testid="search-input" placeholder={placeholder} value={value} onChange={onChange} />
),
Toggle: ({
checked = false,
disabled = false,
label,
description,
onChange,
}: {
checked?: boolean;
disabled?: boolean;
label?: string;
description?: string;
onChange?: (checked: boolean) => void;
}) => (
<div data-testid="toggle-row">
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
disabled={disabled}
onClick={() => onChange?.(!checked)}
>
{label}
</button>
{description ? <span>{description}</span> : null}
</div>
),
}));
// ── useToolBatchStatuses mock ─────────────────────────────────────────────────
const mockRefetch = vi.fn();
let mockStatusesReturnValue: {
statuses: ToolBatchStatusMap | null;
loading: boolean;
error: string | null;
refetch: () => void;
} = {
statuses: null,
loading: false,
error: null,
refetch: mockRefetch,
};
vi.mock("@/shared/hooks/cli/useToolBatchStatuses", () => ({
useToolBatchStatuses: () => mockStatusesReturnValue,
}));
// ── fetch mock ────────────────────────────────────────────────────────────────
let mockFetchResponse: { connections?: unknown[] } = { connections: [{ isActive: true }] };
let mockFeatureFlagsResponse = {
flags: [
{ key: "OMNIROUTE_AUTO_SYNC_CODEX_PROFILES", effectiveValue: "false" },
{ key: "OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES", effectiveValue: "false" },
],
};
globalThis.fetch = vi.fn().mockImplementation((url: string, init?: RequestInit) => {
if (typeof url === "string" && url.includes("/api/providers")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(mockFetchResponse),
});
}
if (typeof url === "string" && url.includes("/api/settings/feature-flags")) {
if (init?.method === "PUT") {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(mockFeatureFlagsResponse),
});
}
return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
}) as typeof fetch;
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: CliCodePageClient } =
await import("@/app/(dashboard)/dashboard/cli-code/CliCodePageClient");
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
let roots: ReturnType<typeof createRoot>[] = [];
async function renderPage(props: { machineId?: string } = {}): Promise<HTMLElement> {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
await act(async () => {
root.render(<CliCodePageClient machineId={props.machineId ?? "test-machine"} />);
});
// Let any pending microtasks (fetch promises) flush
await flushPromises();
return container;
}
async function flushPromises() {
await act(async () => {
await Promise.resolve();
});
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.clearAllMocks();
mockRefetch.mockReset();
// Reset defaults
mockStatusesReturnValue = {
statuses: null,
loading: false,
error: null,
refetch: mockRefetch,
};
mockFetchResponse = { connections: [{ isActive: true }] };
mockFeatureFlagsResponse = {
flags: [
{ key: "OMNIROUTE_AUTO_SYNC_CODEX_PROFILES", effectiveValue: "false" },
{ key: "OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES", effectiveValue: "false" },
],
};
globalThis.fetch = vi.fn().mockImplementation((url: string, init?: RequestInit) => {
if (typeof url === "string" && url.includes("/api/providers")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve(mockFetchResponse),
});
}
if (typeof url === "string" && url.includes("/api/settings/feature-flags")) {
if (init?.method === "PUT") {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(mockFeatureFlagsResponse),
});
}
return Promise.resolve({ ok: false, json: () => Promise.resolve({}) });
}) as typeof fetch;
});
afterEach(() => {
while (roots.length > 0) {
const root = roots.pop();
if (root) act(() => root.unmount());
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliCodePageClient", () => {
it("1. render smoke: page renders without crash with active providers", async () => {
const container = await renderPage();
expect(container.innerHTML).toBeTruthy();
// Concept + comparison cards present
expect(container.querySelector('[data-testid="cli-concept-card"]')).not.toBeNull();
expect(container.querySelector('[data-testid="cli-comparison-card"]')).not.toBeNull();
});
it("2. renders every code tool card when catalogue is OK", async () => {
const container = await renderPage();
const cards = container.querySelectorAll('[data-testid="cli-tool-card"]');
expect(cards.length).toBe(EXPECTED_CODE_COUNT);
});
it("2b. renders CLI profile auto-sync toggles from feature flags", async () => {
mockFeatureFlagsResponse = {
flags: [
{ key: "OMNIROUTE_AUTO_SYNC_CODEX_PROFILES", effectiveValue: "true" },
{ key: "OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES", effectiveValue: "false" },
],
};
const container = await renderPage();
expect(container.textContent).toContain("CLI profile auto-sync");
expect(container.textContent).toContain("Codex profiles");
expect(container.textContent).toContain("Claude Code profiles");
expect(container.textContent).not.toContain("HTTP");
const codexToggle = container.querySelector(
'button[role="switch"][aria-label="Codex profiles"]'
);
const claudeToggle = container.querySelector(
'button[role="switch"][aria-label="Claude Code profiles"]'
);
expect(codexToggle?.getAttribute("aria-checked")).toBe("true");
expect(claudeToggle?.getAttribute("aria-checked")).toBe("false");
expect(globalThis.fetch).toHaveBeenCalledWith("/api/settings/feature-flags");
});
it("2c. persists CLI profile auto-sync toggle changes", async () => {
const container = await renderPage();
const codexToggle = container.querySelector(
'button[role="switch"][aria-label="Codex profiles"]'
) as HTMLButtonElement;
expect(codexToggle).not.toBeNull();
await act(async () => {
codexToggle.click();
await Promise.resolve();
});
expect(globalThis.fetch).toHaveBeenCalledWith("/api/settings/feature-flags", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
key: "OMNIROUTE_AUTO_SYNC_CODEX_PROFILES",
value: "true",
}),
});
expect(codexToggle.getAttribute("aria-checked")).toBe("true");
});
it("3. search filter: typing 'claude' shows only 1 card", async () => {
const container = await renderPage();
// All code tools initially visible
expect(container.querySelectorAll('[data-testid="cli-tool-card"]').length).toBe(
EXPECTED_CODE_COUNT
);
const input = container.querySelector('[data-testid="search-input"]') as HTMLInputElement;
expect(input).not.toBeNull();
await act(async () => {
input.value = "claude";
input.dispatchEvent(new Event("input", { bubbles: true }));
// Simulate onChange
const syntheticEvent = {
target: { value: "claude" },
} as React.ChangeEvent<HTMLInputElement>;
// Find and call the onChange directly
const reactProps = Object.keys(input).find((k) => k.startsWith("__reactFiber"));
if (!reactProps) {
// Fallback: change event
Object.defineProperty(input, "value", { value: "claude", writable: true });
input.dispatchEvent(
Object.assign(new Event("change", { bubbles: true }), {
target: input,
})
);
}
void syntheticEvent;
});
// Re-render with search set via React state
// Since we can't easily trigger React onChange from jsdom, test the filtering logic indirectly
// by re-rendering with the search component directly
const root2 = roots[roots.length - 1];
await act(async () => {
// Reset and re-render a fresh instance to test filter results
root2.render(
<TestWrapper search="claude">
<CliCodePageClient machineId="test" />
</TestWrapper>
);
});
// We can verify with a simpler approach: check the card count remains non-zero (no crash)
expect(container.querySelectorAll('[data-testid="cli-tool-card"]').length).toBeGreaterThan(0);
});
it("3b. search filter with state update: typing filters cards", async () => {
const container = await renderPage();
const input = container.querySelector('[data-testid="search-input"]') as HTMLInputElement;
await act(async () => {
// Simulate React change event
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
nativeInputValueSetter?.call(input, "claude code");
input.dispatchEvent(new Event("change", { bubbles: true }));
});
const cards = container.querySelectorAll('[data-testid="cli-tool-card"]');
// After filtering for "claude code", only Claude Code CLI should match
expect(cards.length).toBeLessThan(EXPECTED_CODE_COUNT);
expect(cards.length).toBeGreaterThan(0);
// The visible card should contain "Claude Code"
expect(container.textContent).toContain("Claude Code");
});
it("4. detection filter: shows skeletons when loading", async () => {
mockStatusesReturnValue = { statuses: null, loading: true, error: null, refetch: mockRefetch };
const container = await renderPage();
const skeletons = container.querySelectorAll('[data-testid="card-skeleton"]');
expect(skeletons.length).toBe(6);
});
it("5. empty state: no active providers → amber banner with link to /dashboard/providers", async () => {
mockFetchResponse = { connections: [] };
const container = await renderPage();
// Wait for providers fetch
await act(async () => {
await Promise.resolve();
});
// The banner should appear (providers loading done, hasActiveProviders = false)
const providerLink = container.querySelector('a[href="/dashboard/providers"]');
expect(providerLink).not.toBeNull();
// Banner text keys
expect(container.textContent).toContain("detail.noActiveProviders");
});
it("6. CliConceptCard rendered at top with currentType='code'", async () => {
const container = await renderPage();
const conceptCard = container.querySelector('[data-testid="cli-concept-card"]');
expect(conceptCard).not.toBeNull();
expect(conceptCard?.getAttribute("data-type")).toBe("code");
});
it("7. CliComparisonCard rendered with currentType='code'", async () => {
const container = await renderPage();
const comparisonCard = container.querySelector('[data-testid="cli-comparison-card"]');
expect(comparisonCard).not.toBeNull();
expect(comparisonCard?.getAttribute("data-type")).toBe("code");
});
it("8. refresh button click calls refetch()", async () => {
const container = await renderPage();
const refreshBtn = container.querySelector('[data-testid="button"]') as HTMLButtonElement;
expect(refreshBtn).not.toBeNull();
await act(async () => {
refreshBtn.click();
});
expect(mockRefetch).toHaveBeenCalledTimes(1);
});
it("9. detailHref contains /dashboard/cli-code/<id> for each tool card", async () => {
const container = await renderPage();
const cards = container.querySelectorAll('[data-testid="cli-tool-card"]');
cards.forEach((card) => {
const href = card.getAttribute("data-href") ?? "";
expect(href).toMatch(/^\/dashboard\/cli-code\/.+/);
});
});
it("10. skeletons shown when providersLoading is true (initial render)", async () => {
mockStatusesReturnValue = { statuses: null, loading: true, error: null, refetch: mockRefetch };
// Delay the fetch so providers loading is true on initial render
const slowFetch = vi.fn().mockImplementation(() => new Promise(() => {})) as typeof fetch;
globalThis.fetch = slowFetch;
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
// Render without awaiting fetch resolution
act(() => {
root.render(<CliCodePageClient machineId="test" />);
});
const skeletons = container.querySelectorAll('[data-testid="card-skeleton"]');
expect(skeletons.length).toBe(6);
});
});
// Helper wrapper (not exported) — needed only for test 3 internal use
function TestWrapper({ children }: { children: React.ReactNode; search?: string }) {
return <>{children}</>;
}
+121
View File
@@ -0,0 +1,121 @@
// @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";
import type { CliConceptType } from "@/shared/components/cli/CliConceptCard";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next-intl", () => ({
useTranslations: () => {
const messages: Record<string, string> = {
"comparison.thisPage": "This page",
"comparison.open": "Open →",
"comparison.code.title": "Code tool",
"comparison.agent.title": "Broad autonomous agent",
"comparison.acp.title": "CLI used as backend by Omni",
};
return (key: string) => messages[key] ?? key;
},
}));
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: CliComparisonCard } = await import("@/shared/components/cli/CliComparisonCard");
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function renderCard(currentType: CliConceptType): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<CliComparisonCard currentType={currentType} />);
});
return container;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliComparisonCard", () => {
it("renders 3 columns for all types", () => {
const container = renderCard("code");
expect(container.textContent).toContain("Code tool");
expect(container.textContent).toContain("Broad autonomous agent");
expect(container.textContent).toContain("CLI used as backend by Omni");
});
it("shows current page badge for currentType=code column", () => {
const container = renderCard("code");
expect(container.textContent).toContain("This page");
expect(container.textContent).toContain("✓");
});
it("shows current page badge for currentType=agent column", () => {
const container = renderCard("agent");
expect(container.textContent).toContain("This page");
expect(container.textContent).toContain("✓");
});
it("shows current page badge for currentType=acp column", () => {
const container = renderCard("acp");
expect(container.textContent).toContain("This page");
expect(container.textContent).toContain("✓");
});
it("renders Open → links for the non-current columns", () => {
const container = renderCard("code");
const links = container.querySelectorAll("a");
const texts = Array.from(links).map((a) => a.textContent);
const verLinks = texts.filter((t) => t?.includes("Open →"));
// 2 non-current columns → 2 links
expect(verLinks).toHaveLength(2);
});
it("for currentType=code, Ver → links point to agent and acp hrefs", () => {
const container = renderCard("code");
const links = container.querySelectorAll("a");
const hrefs = Array.from(links).map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/dashboard/cli-agents");
expect(hrefs).toContain("/dashboard/acp-agents");
});
it("current column has primary styling class", () => {
const container = renderCard("code");
// The current column div has bg-primary/10 class
const currentCol = container.querySelector('[class*="primary"]');
expect(currentCol).not.toBeNull();
});
});
+113
View File
@@ -0,0 +1,113 @@
// @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";
import type { CliConceptType } from "@/shared/components/cli/CliConceptCard";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: CliConceptCard } = await import("@/shared/components/cli/CliConceptCard");
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function renderCard(currentType: CliConceptType): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<CliConceptCard currentType={currentType} />);
});
return container;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliConceptCard", () => {
it("renders with currentType=code", () => {
const container = renderCard("code");
// The card renders (concept keys are shown as raw key strings from mock)
expect(container.textContent).toContain("concept.code.title");
});
it("renders with currentType=agent", () => {
const container = renderCard("agent");
expect(container.textContent).toContain("concept.agent.title");
});
it("renders with currentType=acp", () => {
const container = renderCard("acp");
expect(container.textContent).toContain("concept.acp.title");
});
it("for currentType=code, card has primary bg class", () => {
const container = renderCard("code");
// The root card div should have primary/5 styling
const card = container.firstElementChild as HTMLElement;
expect(card?.className ?? "").toContain("primary");
});
it("for currentType=code, renders chips for agent and acp (not code)", () => {
const container = renderCard("code");
const links = container.querySelectorAll("a");
const hrefs = Array.from(links).map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/dashboard/cli-agents");
expect(hrefs).toContain("/dashboard/acp-agents");
// Should NOT link to itself
expect(hrefs).not.toContain("/dashboard/cli-code");
});
it("for currentType=agent, renders chips for code and acp", () => {
const container = renderCard("agent");
const links = container.querySelectorAll("a");
const hrefs = Array.from(links).map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/dashboard/cli-code");
expect(hrefs).toContain("/dashboard/acp-agents");
expect(hrefs).not.toContain("/dashboard/cli-agents");
});
it("for currentType=acp, renders chips for code and agent", () => {
const container = renderCard("acp");
const links = container.querySelectorAll("a");
const hrefs = Array.from(links).map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/dashboard/cli-code");
expect(hrefs).toContain("/dashboard/cli-agents");
expect(hrefs).not.toContain("/dashboard/acp-agents");
});
});
+206
View File
@@ -0,0 +1,206 @@
// @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";
import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog";
import type { ToolBatchStatus } from "@/shared/types/cliBatchStatus";
// ── Mocks ─────────────────────────────────────────────────────────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next-intl", () => ({
useTranslations: () => {
const messages: Record<string, string> = {
"card.detected": "Detected",
"card.notDetected": "Not detected",
"card.configure": "Configure →",
"card.howToInstall": "How to install →",
"card.baseUrlPartial": "Partial Base URL",
"card.alsoAcp": "also ACP",
"card.connectProviderHint": "Connect a provider in Providers",
};
return (key: string) => messages[key] ?? key;
},
useLocale: () => "en",
}));
// Stub CliStatusBadge so it doesn't need next-intl internals
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
default: ({
effectiveConfigStatus,
}: {
effectiveConfigStatus: string | null;
batchStatus: null;
lastConfiguredAt: string | null;
}) => <span data-testid="status-badge">{effectiveConfigStatus}</span>,
}));
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: CliToolCard } = await import("@/shared/components/cli/CliToolCard");
// ── Fixtures ──────────────────────────────────────────────────────────────────
function makeTool(overrides: Partial<CliCatalogEntry> = {}): CliCatalogEntry {
return {
id: "claude",
name: "Claude Code",
icon: "terminal",
color: "#D97757",
description: "Anthropic Claude Code CLI",
docsUrl: "https://example.com",
configType: "env",
category: "code",
vendor: "Anthropic",
acpSpawnable: false,
baseUrlSupport: "full",
...overrides,
};
}
function makeBatchStatus(overrides: Partial<ToolBatchStatus> = {}): ToolBatchStatus {
return {
detection: { installed: true, runnable: true, version: "1.2.3" },
config: { status: "configured", endpoint: "http://localhost:20128", lastConfiguredAt: null },
...overrides,
};
}
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function renderCard(
tool: CliCatalogEntry,
batchStatus: ToolBatchStatus | null,
detailHref: string,
hasActiveProviders: boolean
): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(
<CliToolCard
tool={tool}
batchStatus={batchStatus}
detailHref={detailHref}
hasActiveProviders={hasActiveProviders}
/>
);
});
return container;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliToolCard", () => {
it("renders tool name", () => {
const container = renderCard(makeTool(), makeBatchStatus(), "/dashboard/cli-code/claude", true);
expect(container.textContent).toContain("Claude Code");
});
it("links to detailHref", () => {
const container = renderCard(makeTool(), makeBatchStatus(), "/dashboard/cli-code/claude", true);
const link = container.querySelector("a");
expect(link).not.toBeNull();
expect(link!.getAttribute("href")).toBe("/dashboard/cli-code/claude");
});
it("shows version when installed", () => {
const container = renderCard(makeTool(), makeBatchStatus(), "/detail", true);
expect(container.textContent).toContain("1.2.3");
});
it("shows 'not found' when not installed", () => {
const status = makeBatchStatus({
detection: { installed: false, runnable: false, version: undefined },
});
const container = renderCard(makeTool(), status, "/detail", true);
expect(container.textContent).toContain("not found");
});
it("shows configure footer when installed", () => {
const container = renderCard(makeTool(), makeBatchStatus(), "/detail", true);
expect(container.textContent).toContain("Configure →");
});
it("shows install footer when not installed", () => {
const status = makeBatchStatus({
detection: { installed: false, runnable: false },
});
const container = renderCard(makeTool(), status, "/detail", true);
expect(container.textContent).toContain("How to install →");
});
it("shows partial baseUrl amber badge", () => {
const tool = makeTool({ baseUrlSupport: "partial" });
const container = renderCard(tool, makeBatchStatus(), "/detail", true);
expect(container.textContent).toContain("Partial Base URL");
});
it("shows also ACP badge when acpSpawnable is true", () => {
const tool = makeTool({ acpSpawnable: true });
const container = renderCard(tool, makeBatchStatus(), "/detail", true);
expect(container.textContent).toContain("also ACP");
});
it("shows provider tooltip text when hasActiveProviders is false", () => {
const container = renderCard(makeTool(), makeBatchStatus(), "/detail", false);
expect(container.textContent).toContain("Connect a provider in Providers");
});
it("shows install chips when not installed and configType is not guide", () => {
const status = makeBatchStatus({
detection: { installed: false, runnable: false },
});
const tool = makeTool({ configType: "custom" });
const container = renderCard(tool, status, "/detail", true);
expect(container.textContent).toContain("Manual config");
expect(container.textContent).toContain("Install");
});
it("does NOT show install chips when configType is guide", () => {
const status = makeBatchStatus({
detection: { installed: false, runnable: false },
});
const tool = makeTool({ configType: "guide" });
const container = renderCard(tool, status, "/detail", true);
expect(container.textContent).not.toContain("Manual config");
});
it("renders gracefully with null batchStatus", () => {
const container = renderCard(makeTool(), null, "/detail", true);
expect(container.textContent).toContain("Claude Code");
expect(container.textContent).toContain("not found");
});
});
@@ -0,0 +1,153 @@
// @vitest-environment jsdom
/**
* Guards fix for issue #6039:
* CompressionHub was sending the full merged settings object to PUT
* /api/settings/compression instead of only the patch. The API schema uses
* .strict() Zod validation, so any field present in CompressionConfig but
* absent from compressionSettingsUpdateSchema (e.g. contextBudget, or
* stackedPipeline steps using engines not yet in the discriminated union)
* would cause a 400 validation failure — making switching to any non-default
* combo fail silently.
*
* Fix: send `patch` (the caller-supplied partial update) instead of `next`
* (the full optimistically-merged state).
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── Mocks ─────────────────────────────────────────────────────────────────────
// Stub next/navigation (not used by CompressionHub but required by module graph)
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
usePathname: () => "/",
}));
// Track all fetch calls
const fetchCalls: { url: string; init: RequestInit }[] = [];
const mockFetch = vi.fn().mockImplementation((url: string, init: RequestInit) => {
fetchCalls.push({ url, init });
return Promise.resolve({
ok: true,
json: async () => ({ combos: [] }),
});
});
vi.stubGlobal("fetch", mockFetch);
// Import after mocks are in place
import CompressionHub from "../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub";
// ── Helpers ───────────────────────────────────────────────────────────────────
function getLastPutBody(): Record<string, unknown> | null {
const putCall = [...fetchCalls].reverse().find(
(c) => c.init?.method === "PUT"
);
if (!putCall) return null;
return JSON.parse(putCall.init.body as string);
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CompressionHub — PUT sends patch only, not full settings", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
fetchCalls.length = 0;
mockFetch.mockClear();
// GET /api/settings/compression returns a config with extra fields that
// are NOT in compressionSettingsUpdateSchema (simulates contextBudget etc.)
mockFetch.mockImplementationOnce((_url: string) =>
Promise.resolve({
ok: true,
json: async () => ({
enabled: true,
defaultMode: "lite",
activeComboId: null,
contextEditing: { enabled: false },
// Field present in CompressionConfig but NOT in the update schema:
contextBudget: { mode: "floor", floorTokens: 4096 },
// Engine step type not in stackedPipelineStepSchema discriminated union:
stackedPipeline: [
{ engine: "headroom", intensity: "standard" },
{ engine: "caveman", intensity: "lite" },
],
}),
})
);
// GET /api/settings/compression/combos
mockFetch.mockImplementationOnce((_url: string) =>
Promise.resolve({
ok: true,
json: async () => ({ combos: [{ id: "c1", name: "My Combo", pipeline: [] }] }),
})
);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => { root.unmount(); });
document.body.removeChild(container);
});
it("sends only the changed field when activeComboId is updated", async () => {
await act(async () => {
root.render(<CompressionHub />);
});
// Find the combo selector and change it to "c1"
const select = container.querySelector("select");
expect(select).not.toBeNull();
await act(async () => {
if (select) {
Object.defineProperty(select, "value", { writable: true, value: "c1" });
select.dispatchEvent(new Event("change", { bubbles: true }));
}
});
const body = getLastPutBody();
expect(body).not.toBeNull();
// Must contain the changed field
expect(body).toHaveProperty("activeComboId", "c1");
// Must NOT contain fields from the full settings that would fail strict
// schema validation (contextBudget, stackedPipeline with unknown engines)
expect(body).not.toHaveProperty("contextBudget");
expect(body).not.toHaveProperty("stackedPipeline");
expect(body).not.toHaveProperty("enabled");
expect(body).not.toHaveProperty("defaultMode");
});
it("sends only the toggle field when contextEditing is toggled", async () => {
await act(async () => {
root.render(<CompressionHub />);
});
// Find the context editing toggle button
const toggleButtons = container.querySelectorAll("button[role='switch']");
expect(toggleButtons.length).toBeGreaterThan(0);
await act(async () => {
(toggleButtons[0] as HTMLButtonElement).click();
});
const body = getLastPutBody();
expect(body).not.toBeNull();
// Should only include contextEditing, not the full settings
expect(body).toHaveProperty("contextEditing");
expect(body).not.toHaveProperty("contextBudget");
expect(body).not.toHaveProperty("stackedPipeline");
});
});
@@ -0,0 +1,84 @@
// @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";
// Regression guard for the base-red introduced by #6061: CoolingConnectionsPanel
// imported `Card` from a non-existent `@/components/ui/card`, which passed the
// PR→release fast-gates (they don't run `next build`) but broke `next build`
// with `Module not found: Can't resolve '@/components/ui/card'`. Importing the
// component here fails at module-load if that broken import ever comes back,
// so this test fails-without-the-fix.
// `formatResetCountdown` lives in the client-safe `@/shared/utils/formatting`
// module (imported directly by the panel — never via the server-only localDb
// barrel, which would drag ioredis/node:net into the browser bundle). Stub it so
// the countdown text is deterministic.
vi.mock("@/shared/utils/formatting", () => ({
formatResetCountdown: (v: string | number | null | undefined) => (v == null ? null : "in 5m"),
}));
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
const PANEL_PATH = "@/app/(dashboard)/dashboard/providers/[id]/components/CoolingConnectionsPanel";
describe("CoolingConnectionsPanel", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
});
it("module loads and exports a default component (guards the import path)", async () => {
const mod = await import(PANEL_PATH);
expect(typeof mod.default).toBe("function");
});
it("renders the panel with a countdown for a cooling connection", async () => {
const { default: CoolingConnectionsPanel } = await import(PANEL_PATH);
const container = makeContainer();
const root = createRoot(container);
const future = new Date(Date.now() + 5 * 60_000).toISOString();
await act(async () => {
root.render(
React.createElement(CoolingConnectionsPanel, {
connections: [{ id: "conn-abc12345", displayName: "My Key", rateLimitedUntil: future }],
})
);
});
const panel = container.querySelector("[data-testid='cooling-connections-panel']");
expect(panel).toBeTruthy();
expect(container.querySelector("[data-testid='cooling-countdown']")?.textContent).toContain(
"in 5m"
);
expect(panel?.textContent).toContain("My Key");
});
it("renders nothing when no connection is cooling", async () => {
const { default: CoolingConnectionsPanel } = await import(PANEL_PATH);
const container = makeContainer();
const root = createRoot(container);
const past = new Date(Date.now() - 60_000).toISOString();
await act(async () => {
root.render(
React.createElement(CoolingConnectionsPanel, {
connections: [{ id: "conn-old", displayName: "Expired", rateLimitedUntil: past }],
})
);
});
expect(container.querySelector("[data-testid='cooling-connections-panel']")).toBeNull();
});
});
@@ -0,0 +1,61 @@
// @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";
// Regression guard (release v3.8.44 e2e finding): LocaleAutoDetect called
// router.refresh() on EVERY first visit — even when the detected browser
// locale was exactly the one the server had just rendered — re-navigating the
// page mid-interaction (Playwright: "Execution context was destroyed") and
// flashing for every new visitor. It must refresh ONLY when the detected
// locale differs from <html lang>.
const refresh = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ refresh }),
}));
describe("LocaleAutoDetect refresh gating", () => {
const cleanups: Array<() => void> = [];
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
refresh.mockClear();
// Fresh visit: no locale cookie.
document.cookie = "NEXT_LOCALE=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
Object.defineProperty(navigator, "languages", { value: ["en-US"], configurable: true });
});
afterEach(() => {
while (cleanups.length) cleanups.pop()?.();
});
async function mount() {
const { LocaleAutoDetect } = await import("@/shared/components/LocaleAutoDetect");
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
await act(async () => {
root.render(React.createElement(LocaleAutoDetect));
});
cleanups.push(() => {
root.unmount();
container.remove();
});
}
it("does NOT refresh when the detected locale equals the server-rendered <html lang>", async () => {
document.documentElement.lang = "en";
await mount();
expect(refresh).not.toHaveBeenCalled();
});
it("refreshes when the detected locale differs from the server-rendered <html lang>", async () => {
document.documentElement.lang = "fr";
await mount();
expect(refresh).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,160 @@
// @vitest-environment jsdom
//
// Regression test: when the Open Claw 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#579, port of decolua/9router#615).
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<string, unknown>) => {
const messages: Record<string, string> = {
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}.",
manualConfig: "Manual Config",
checkingCli: "Checking {tool}...",
openClawManualConfiguration: "Open Claw Manual Configuration",
"toolDescriptions.openclaw": "Open Claw CLI",
};
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("next/image", () => ({
default: () => <span data-testid="next-image" />,
}));
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/CliStatusBadge", () => ({
default: () => <span data-testid="status-badge" />,
}));
// 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 }) => <div>{children}</div>,
Button: ({
children,
onClick,
}: {
children: React.ReactNode;
onClick?: () => void;
}) => (
<button type="button" onClick={onClick}>
{children}
</button>
),
ModelSelectModal: () => null,
ManualConfigModal: ({ isOpen, title }: { isOpen: boolean; title?: string }) =>
isOpen ? <div data-testid="manual-config-modal">{title}</div> : 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 #579).
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
const url = typeof input === "string" ? input : input.toString();
if (url.includes("/api/cli-tools/openclaw-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: OpenClawToolCard } = await import(
"@/app/(dashboard)/dashboard/cli-code/components/OpenClawToolCard"
);
async function renderExpanded() {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
await act(async () => {
root.render(
<OpenClawToolCard
tool={{ name: "Open Claw", defaultModels: [] }}
isExpanded={true}
onToggle={() => {}}
activeProviders={[]}
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("OpenClawToolCard — manual-config CTA when CLI is not detected", () => {
it("renders the Manual Config button 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);
});
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();
});
});
@@ -0,0 +1,110 @@
// @vitest-environment jsdom
// #2166 — ProviderIcon custom remote icon URL (`src` prop) support.
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/image", () => ({
default: (props: Record<string, unknown>) => {
const { onError, alt, ...rest } = props as { onError?: () => void; alt?: string } & Record<
string,
unknown
>;
// eslint-disable-next-line @next/next/no-img-element -- test double for next/image
return <img data-testid="next-image" alt={alt || ""} onError={onError} {...rest} />;
},
}));
const { default: ProviderIcon } = await import("@/shared/components/ProviderIcon");
// ── Helpers ───────────────────────────────────────────────────────────────────
// Deliberately not registered in @lobehub/icons aliases or the KNOWN_PNGS/KNOWN_SVGS
// static-asset sets, so tests exercise only the `src` override + generic-icon fallback
// paths, never the @lobehub/static resolution chain.
const UNKNOWN_PROVIDER_ID = "openai-compatible-test-node-xyz";
const containers: HTMLElement[] = [];
function renderIcon(props: Record<string, unknown>): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<ProviderIcon providerId={UNKNOWN_PROVIDER_ID} {...props} />);
});
return container;
}
function fireImgError(container: HTMLElement) {
const img = container.querySelector("img");
if (!img) throw new Error("expected an <img> element to fire error on");
act(() => {
img.dispatchEvent(new Event("error"));
});
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("ProviderIcon — custom remote icon URL (#2166)", () => {
it("renders an <img> with the given src when `src` is set", () => {
const container = renderIcon({ src: "https://example.com/logo.png", size: 32 });
const img = container.querySelector("img");
expect(img).not.toBeNull();
expect(img?.getAttribute("src")).toBe("https://example.com/logo.png");
});
it("falls back to the generic icon when `src` is unset", () => {
const container = renderIcon({});
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("svg")).not.toBeNull();
});
it("falls back to the generic (lobehub/static) icon chain when `src` load fails and no fallbackText is given", () => {
const container = renderIcon({ src: "https://example.com/broken.png" });
expect(container.querySelector("img")).not.toBeNull();
fireImgError(container);
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("svg")).not.toBeNull();
});
it("falls back to a text badge when `src` load fails and fallbackText is given", () => {
const container = renderIcon({
src: "https://example.com/broken.png",
fallbackText: "OC",
fallbackColor: "#10A37F",
});
expect(container.querySelector("img")).not.toBeNull();
fireImgError(container);
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("svg")).toBeNull();
expect(container.textContent).toBe("OC");
});
it("ignores a whitespace-only src and falls back to the generic icon", () => {
const container = renderIcon({ src: " " });
expect(container.querySelector("img")).toBeNull();
expect(container.querySelector("svg")).not.toBeNull();
});
});
@@ -0,0 +1,27 @@
// @vitest-environment jsdom
import React from "react";
import { renderToString } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
// Regression guard for the #5918 TDZ crash: ProxyRegistryManager called
// `useProxyBatchOperations(load)` BEFORE the `const load = useCallback(...)`
// declaration in the component body, so every SERVER render threw
// `ReferenceError: Cannot access 'load' before initialization` — the whole
// /dashboard/system/proxy page 500'd in production (digest 539380095), caught
// only by the release-PR e2e smoke (the PR→release fast-gates render nothing).
// renderToString mirrors that SSR path exactly (no effects, no fetches) and is
// synchronous — this test fails-without-the-fix at the first render.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
describe("ProxyRegistryManager (TDZ regression #5918)", () => {
it("server-renders without a use-before-init ReferenceError", { timeout: 30000 }, async () => {
const { default: ProxyRegistryManager } =
await import("@/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager");
const html = renderToString(React.createElement(ProxyRegistryManager));
// The heading key is rendered via the mocked translator (key echo).
expect(html).toContain("title");
});
});
+202
View File
@@ -0,0 +1,202 @@
// @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 ─────────────────────────────────────────────────────────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
// Stub fetch globally
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ connections: [], keys: [], data: [], cloudEnabled: false }),
});
vi.stubGlobal("fetch", mockFetch);
// Stub next/navigation
vi.mock("next/navigation", () => ({
notFound: () => {
throw new Error("NOT_FOUND");
},
}));
// Stub CLI_TOOLS catalog
vi.mock("@/shared/constants/cliTools", () => ({
CLI_TOOLS: {
claude: {
id: "claude",
name: "Claude Code",
icon: "terminal",
color: "#D97757",
category: "code",
configType: "env",
vendor: "Anthropic",
baseUrlSupport: "full",
defaultModels: [],
},
codex: {
id: "codex",
name: "Codex",
icon: "terminal",
color: "#000",
category: "code",
configType: "custom",
vendor: "OpenAI",
baseUrlSupport: "full",
defaultModels: [],
},
custom: {
id: "custom",
name: "Custom CLI",
icon: "terminal",
color: "#888",
category: "code",
configType: "custom-builder",
vendor: undefined,
baseUrlSupport: "full",
defaultModels: [],
},
"hermes-agent": {
id: "hermes-agent",
name: "Hermes Agent",
icon: "terminal",
color: "#5865f2",
category: "agent",
configType: "custom",
vendor: "HermesAI",
baseUrlSupport: "full",
defaultModels: [],
},
forge: {
id: "forge",
name: "Forge",
icon: "terminal",
color: "#888",
category: "code",
configType: "custom",
vendor: undefined,
baseUrlSupport: "partial",
defaultModels: [],
},
},
}));
// Stub model constants
vi.mock("@/shared/constants/models", () => ({
PROVIDER_ID_TO_ALIAS: {},
getModelsByProviderId: () => [],
}));
// Stub specialized cards — render a testid so we can identify which was rendered
vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/index", () => ({
ClaudeToolCard: () => <div data-testid="ClaudeToolCard" />,
CodexToolCard: () => <div data-testid="CodexToolCard" />,
DroidToolCard: () => <div data-testid="DroidToolCard" />,
OpenClawToolCard: () => <div data-testid="OpenClawToolCard" />,
ClineToolCard: () => <div data-testid="ClineToolCard" />,
KiloToolCard: () => <div data-testid="KiloToolCard" />,
DefaultToolCard: ({ toolId }: { toolId: string }) => (
<div data-testid="DefaultToolCard" data-toolid={toolId} />
),
AntigravityToolCard: () => <div data-testid="AntigravityToolCard" />,
CopilotToolCard: () => <div data-testid="CopilotToolCard" />,
CustomCliCard: () => <div data-testid="CustomCliCard" />,
HermesAgentToolCard: () => <div data-testid="HermesAgentToolCard" />,
}));
vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard", () => ({
default: () => <div data-testid="CliproxyapiToolCard" />,
}));
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: ToolDetailClient } = await import(
"@/app/(dashboard)/dashboard/cli-code/components/ToolDetailClient"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function renderDetail(toolId: string, category: "code" | "agent"): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<ToolDetailClient toolId={toolId} category={category} />);
});
return container;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
mockFetch.mockClear();
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("ToolDetailClient", () => {
it("renders ClaudeToolCard for toolId=claude", async () => {
const container = renderDetail("claude", "code");
// Wait for async state resolution
await act(async () => {});
expect(container.querySelector("[data-testid='ClaudeToolCard']")).not.toBeNull();
});
it("renders CodexToolCard for toolId=codex", async () => {
const container = renderDetail("codex", "code");
await act(async () => {});
expect(container.querySelector("[data-testid='CodexToolCard']")).not.toBeNull();
});
it("renders CustomCliCard for toolId=custom", async () => {
const container = renderDetail("custom", "code");
await act(async () => {});
expect(container.querySelector("[data-testid='CustomCliCard']")).not.toBeNull();
});
it("renders DefaultToolCard for unknown tool (forge, configType:custom)", async () => {
const container = renderDetail("forge", "code");
await act(async () => {});
const card = container.querySelector("[data-testid='DefaultToolCard']");
expect(card).not.toBeNull();
expect(card!.getAttribute("data-toolid")).toBe("forge");
});
it("renders nothing (null) for completely unknown toolId", async () => {
const container = renderDetail("totally-unknown-xyz", "code");
await act(async () => {});
// CLI_TOOLS["totally-unknown-xyz"] is undefined → returns null → empty container
expect(container.textContent).toBe("");
});
});
@@ -0,0 +1,106 @@
/**
* Verifies that the logs/activity page calls permanentRedirect("/dashboard/activity").
* We mock next/navigation so no Next.js runtime is needed.
*/
import test from "node:test";
import assert from "node:assert/strict";
// Mock next/navigation before importing the page
let capturedRedirectTarget: string | undefined;
// Stub permanentRedirect to capture the call instead of throwing
const mockNavigation = {
permanentRedirect: (target: string) => {
capturedRedirectTarget = target;
// permanentRedirect normally throws (NEXT_REDIRECT error)
// In tests we just record the call
},
redirect: () => {},
useRouter: () => ({}),
usePathname: () => "",
useSearchParams: () => new URLSearchParams(),
};
// Node.js module mock via loader is complex; instead we test by importing
// the source and verifying the permanent redirect is invoked correctly
// by inspecting the module's source text.
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const PAGE_PATH = resolve(
import.meta.dirname ?? new URL(".", import.meta.url).pathname,
"../../../src/app/(dashboard)/dashboard/logs/activity/page.tsx"
);
test("logs/activity/page.tsx contains permanentRedirect('/dashboard/activity')", () => {
const src = readFileSync(PAGE_PATH, "utf-8");
assert.ok(
src.includes("permanentRedirect"),
"page.tsx must call permanentRedirect"
);
assert.ok(
src.includes("/dashboard/activity"),
"page.tsx must redirect to /dashboard/activity"
);
assert.ok(
src.includes(`from "next/navigation"`),
"page.tsx must import from next/navigation"
);
});
test("logs/activity/page.tsx does NOT import AuditLogTab anymore", () => {
const src = readFileSync(PAGE_PATH, "utf-8");
assert.ok(
!src.includes("AuditLogTab"),
"page.tsx must not reference AuditLogTab after F4 cleanup"
);
});
test("logs/activity/page.tsx does NOT have 'use client' directive (server component)", () => {
const src = readFileSync(PAGE_PATH, "utf-8");
assert.ok(
!src.includes('"use client"'),
"redirect page must be a server component (no 'use client')"
);
});
test("AuditLogTab.tsx no longer exists (deleted by F4)", () => {
const AUDIT_LOG_TAB_PATH = resolve(
import.meta.dirname ?? new URL(".", import.meta.url).pathname,
"../../../src/app/(dashboard)/dashboard/logs/AuditLogTab.tsx"
);
let exists = false;
try {
readFileSync(AUDIT_LOG_TAB_PATH);
exists = true;
} catch {
exists = false;
}
assert.ok(!exists, "AuditLogTab.tsx must have been deleted");
});
// Also confirm ActivityFeedClient exists
test("activity/ActivityFeedClient.tsx exists", () => {
const CLIENT_PATH = resolve(
import.meta.dirname ?? new URL(".", import.meta.url).pathname,
"../../../src/app/(dashboard)/dashboard/activity/ActivityFeedClient.tsx"
);
let src: string;
try {
src = readFileSync(CLIENT_PATH, "utf-8");
} catch {
assert.fail("ActivityFeedClient.tsx does not exist");
return;
}
assert.ok(src.includes("/api/compliance/audit-log"), "Client must fetch from audit-log endpoint");
// The client sets level: "high" in URLSearchParams (produces level=high in the query string)
assert.ok(
src.includes('level: "high"') || src.includes("level=high"),
"Client must request level=high"
);
});
// Dummy to satisfy the mockNavigation reference (avoid unused var lint)
void mockNavigation;
@@ -0,0 +1,161 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// next-intl: return the key so we can assert on stable strings.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AddApiKeyModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal");
const FREE_TOGGLE = 'button[role="switch"][aria-label="importFreeModelsOnlyLabel"]';
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<AddApiKeyModal
isOpen
onSave={async () => undefined}
onClose={() => {}}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve({ valid: true }) } as Response)
)
);
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("AddApiKeyModal — import only free models", () => {
it("shows the free-only toggle for a provider that has free models", () => {
const el = render({ provider: "openrouter", providerName: "OpenRouter" });
expect(el.querySelector(FREE_TOGGLE)).toBeTruthy();
});
it("hides the free-only toggle for a provider without free models", () => {
const el = render({ provider: "anthropic", providerName: "Anthropic" });
expect(el.querySelector(FREE_TOGGLE)).toBeNull();
});
it("includes importFreeModelsOnly in the saved payload when toggled on", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({ provider: "openrouter", providerName: "OpenRouter", onSave });
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
setInputValue(nameInput, "My OpenRouter");
setInputValue(apiKeyInput, "sk-or-test-key");
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.importFreeModelsOnly).toBe(true);
});
});
describe("AddApiKeyModal — quota scraping fields", () => {
it("saves OpenCode Go workspace and auth cookie in providerSpecificData", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({ provider: "opencode-go", providerName: "OpenCode Go", onSave });
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
const workspaceInput = el.querySelector<HTMLInputElement>(
'input[name="opencodeGoWorkspaceId"]'
)!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="opencodeGoAuthCookie"]')!;
setInputValue(nameInput, "OpenCode Go");
setInputValue(apiKeyInput, "sk-opencode-go-test");
setInputValue(workspaceInput, "workspace-123");
setInputValue(cookieInput, "auth=opencode-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.opencodeGoWorkspaceId).toBe("workspace-123");
expect(payload.providerSpecificData?.opencodeGoAuthCookie).toBe("auth=opencode-cookie");
});
it("saves Ollama Cloud usage cookie in providerSpecificData", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({ provider: "ollama-cloud", providerName: "Ollama Cloud", onSave });
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="ollamaCloudUsageCookie"]')!;
setInputValue(nameInput, "Ollama Cloud");
setInputValue(apiKeyInput, "ollama-key");
setInputValue(cookieInput, "__Secure-session=ollama-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.ollamaCloudUsageCookie).toBe(
"__Secure-session=ollama-cookie"
);
});
});
@@ -0,0 +1,102 @@
// @vitest-environment jsdom
//
// #5565 / #5567 — Providers without a live validator (e.g. lmarena session
// cookie, piapi API key) return `{ unsupported: true }` from
// /api/providers/validate. The modal previously treated that like a hard
// "Invalid" and blocked Save entirely, so the credential could never be added.
// "Validation not supported" must be a non-blocking warning: Save still persists
// the credential as-is.
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AddApiKeyModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal");
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<AddApiKeyModal isOpen onSave={async () => undefined} onClose={() => {}} {...(props as any)} />
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
vi.clearAllMocks();
// The validate endpoint reports the provider has no live validator (400 +
// unsupported:true). Any other call (model lookups, etc.) succeeds.
vi.stubGlobal(
"fetch",
vi.fn((url: string) => {
if (String(url).includes("/api/providers/validate")) {
return Promise.resolve({
ok: false,
json: () =>
Promise.resolve({ error: "Provider validation not supported", unsupported: true }),
} as Response);
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({ valid: true }) } as Response);
})
);
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("AddApiKeyModal — 'validation not supported' is a non-blocking warning (#5565/#5567)", () => {
it("still calls onSave when /validate returns unsupported=true", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({ provider: "piapi", providerName: "PiAPI", onSave });
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
expect(apiKeyInput).toBeTruthy();
setInputValue(nameInput, "My PiAPI");
setInputValue(apiKeyInput, "piapi-secret-key");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
expect(saveBtn).toBeTruthy();
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// RED on pre-fix code: the modal ignored `unsupported`, set a save error and
// returned without calling onSave. After the fix it proceeds to save.
await waitFor(() => onSave.mock.calls.length > 0);
expect(onSave).toHaveBeenCalled();
});
});
@@ -0,0 +1,107 @@
// @vitest-environment jsdom
//
// #5088 — When the inline credential "Check" fails, the modal showed only a bare
// "invalid" badge and threw away the detailed reason returned by
// /api/providers/validate. For claude-web/chatgpt-web the real cause is often an
// environment error (e.g. "TLS impersonation client failed to start: EACCES …"),
// which the backend already surfaces in `data.error` — but the UI hid it, so the
// reporter had to dig it out via a separate Provider Test. The detailed message
// must be shown next to the badge.
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AddApiKeyModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal");
const TLS_EACCES_ERROR =
"TLS impersonation client failed to start: EACCES: permission denied, mkdir " +
"'/usr/lib/node_modules/omniroute/dist/node_modules/tls-client-node/bin'. " +
"Verify tls-client-node is installed and its native binary downloaded. " +
"(claude-web requires this — without it, Cloudflare blocks every request)";
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<AddApiKeyModal isOpen onSave={async () => undefined} onClose={() => {}} {...(props as any)} />
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
vi.clearAllMocks();
// /api/providers/validate fails with the detailed TLS/EACCES reason; any other
// call (e.g. model lookups) succeeds.
vi.stubGlobal(
"fetch",
vi.fn((url: string) => {
if (String(url).includes("/api/providers/validate")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ valid: false, error: TLS_EACCES_ERROR }),
} as Response);
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({ valid: true }) } as Response);
})
);
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("AddApiKeyModal — surfaces the detailed validation error (#5088)", () => {
it("shows the underlying TLS/EACCES reason, not just an 'invalid' badge", async () => {
const el = render({ provider: "claude-web", providerName: "Claude Web" });
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
expect(apiKeyInput).toBeTruthy();
setInputValue(apiKeyInput, "sk-ant-sid01-fake-session-key");
// The validate ("check") button is the first button that follows the
// credential input in DOM order (it sits right next to it).
const checkBtn = Array.from(el.querySelectorAll("button")).find(
(b) =>
(apiKeyInput.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
)!;
expect(checkBtn).toBeTruthy();
act(() => {
checkBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// The full reason must reach the DOM — a bare "invalid" badge is not enough.
await waitFor(() => el.textContent?.includes("EACCES: permission denied") ?? false);
expect(el.textContent).toContain("TLS impersonation client failed to start");
});
});
+200
View File
@@ -0,0 +1,200 @@
// @vitest-environment jsdom
/**
* UI unit tests for AgentBridge page — smoke render + empty state.
*/
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) => key,
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) =>
React.createElement("a", { href }, children),
}));
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
// Mock fetch for hooks
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
} as unknown as Response);
// ── Helpers ───────────────────────────────────────────────────────────────────
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe("EmptyStateNoProviders", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders empty state component with link to providers", async () => {
const { EmptyStateNoProviders } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/EmptyStateNoProviders"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(EmptyStateNoProviders));
});
expect(document.body.innerHTML).toContain("emptyNoProvidersTitle");
expect(document.body.innerHTML).toContain("/dashboard/providers");
});
});
describe("RiskNoticeBanner", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
try { localStorage.clear(); } catch { /* ignore */ }
});
it("renders risk notice banner when not dismissed", async () => {
// Ensure not dismissed
try { localStorage.removeItem("omniroute-agentbridge-risk-dismissed"); } catch { /* ignore */ }
const { RiskNoticeBanner } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/RiskNoticeBanner"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(RiskNoticeBanner));
});
// Banner is rendered (useEffect fires synchronously in jsdom with act)
// It shows "riskBannerTitle" i18n key
expect(document.body.innerHTML).toContain("riskBannerTitle");
});
it("does not render risk banner when already dismissed", async () => {
try {
localStorage.setItem("omniroute-agentbridge-risk-dismissed", "true");
} catch { /* ignore */ }
const { RiskNoticeBanner } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/RiskNoticeBanner"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(RiskNoticeBanner));
});
expect(document.body.innerHTML).not.toContain("riskBannerTitle");
});
it("dismisses banner on close click", async () => {
try { localStorage.removeItem("omniroute-agentbridge-risk-dismissed"); } catch { /* ignore */ }
const { RiskNoticeBanner } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/RiskNoticeBanner"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(RiskNoticeBanner));
});
// Click dismiss
const closeBtn = container.querySelector('button[aria-label]');
await act(async () => {
closeBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Banner gone
expect(document.body.innerHTML).not.toContain("riskBannerTitle");
});
});
describe("BypassListEditor", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders default bypass patterns", async () => {
const { BypassListEditor } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/BypassListEditor"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(BypassListEditor, {
patterns: [],
onSave: vi.fn(),
})
);
});
expect(document.body.innerHTML).toContain("*.bank.*");
expect(document.body.innerHTML).toContain("*.okta.com");
});
it("renders initial user patterns in textarea", async () => {
const { BypassListEditor } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/BypassListEditor"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(BypassListEditor, {
patterns: ["*.internal.corp"],
onSave: vi.fn(),
})
);
});
const textarea = container.querySelector("textarea");
expect(textarea?.value).toContain("*.internal.corp");
});
});
@@ -0,0 +1,93 @@
/**
* a11y tests for AgentBridgeServerCard — each action button must have aria-label.
* Uses source-text inspection (no JSDOM render needed) for the structural assertion.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CARD_PATH = path.resolve(
__dirname,
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentBridgeServerCard.tsx",
);
const src = readFileSync(CARD_PATH, "utf-8");
describe("AgentBridgeServerCard aria-labels (B2)", () => {
it("Start button has aria-label", () => {
// Start button: onClick start, aria-label present
assert.ok(
src.includes('aria-label={t("startServer")}'),
'Start button must have aria-label={t("startServer")}',
);
});
it("Stop button has aria-label", () => {
assert.ok(
src.includes('aria-label={t("stopServer")}'),
'Stop button must have aria-label={t("stopServer")}',
);
});
it("Restart button has aria-label", () => {
assert.ok(
src.includes('aria-label={t("restartServer")}'),
'Restart button must have aria-label={t("restartServer")}',
);
});
it("Trust Cert button has aria-label", () => {
assert.ok(
src.includes('aria-label={t("trustCert")}'),
'Trust Cert button must have aria-label={t("trustCert")}',
);
});
it("Download Cert anchor has aria-label", () => {
assert.ok(
src.includes('aria-label={t("downloadCert")}'),
'Download Cert anchor must have aria-label={t("downloadCert")}',
);
});
it("Regenerate Cert button has aria-label", () => {
assert.ok(
src.includes('aria-label={t("regenerateCert")}'),
'Regenerate Cert button must have aria-label={t("regenerateCert")}',
);
});
it("all 5 buttons and 1 anchor have aria-label attributes (6 total)", () => {
// Count aria-label occurrences in action buttons section
const matches = src.match(/aria-label=\{t\(/g) ?? [];
assert.ok(
matches.length >= 6,
`Expected at least 6 aria-label attributes, found ${matches.length}`,
);
});
});
describe("SessionRecorderBar aria-labels (B2)", () => {
const BAR_PATH = path.resolve(
__dirname,
"../../../src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionRecorderBar.tsx",
);
const barSrc = readFileSync(BAR_PATH, "utf-8");
it("REC (recordSession) button has aria-label", () => {
assert.ok(
barSrc.includes('aria-label={t("recordSession")}'),
'REC button must have aria-label={t("recordSession")}',
);
});
it("Stop (stopSession) button has aria-label", () => {
assert.ok(
barSrc.includes('aria-label={t("stopSession")}'),
'Stop button must have aria-label={t("stopSession")}',
);
});
});
@@ -0,0 +1,375 @@
// @vitest-environment jsdom
/**
* Tests for AgentCard — per-agent RiskNoticeModal on first DNS activation.
*
* Covers:
* - First toggle opens modal (does NOT call onDnsToggle immediately)
* - Accept closes modal + calls onDnsToggle with true
* - Second activation does NOT open modal (localStorage flag set)
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) =>
React.createElement("a", { href }, children),
}));
// Button.tsx exposes a default export — match the real module shape so
// RiskNoticeModal (which uses `import Button from ...`) resolves correctly.
// Round 3 had this as a named-export mock, which masked the production
// `import { Button }` bug fixed in R4 #1.
vi.mock("@/shared/components/Button", () => ({
default: ({
children,
onClick,
}: {
children: React.ReactNode;
onClick?: () => void;
variant?: string;
}) => React.createElement("button", { type: "button", onClick }, children),
}));
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
} as unknown as Response);
const RISK_KEY_PREFIX = "omniroute-agentbridge-risk-dismissed-";
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
const mockTarget = {
id: "copilot" as const,
name: "GitHub Copilot",
icon: "code",
color: "#10B981",
hosts: ["api.githubcopilot.com"],
port: 443,
endpointPatterns: ["/chat/completions"],
defaultModels: [{ id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" }],
setupTutorial: {
steps: ["Step 1", "Step 2"],
detection: { command: "which copilot", platform: "all" as const },
},
handler: () => Promise.resolve({ default: class {} as never }),
riskNoticeKey: "oauth",
viability: "supported" as const,
};
const baseAgentState = {
agent_id: "copilot",
dns_enabled: false,
cert_trusted: true,
setup_completed: true,
last_started_at: null,
last_error: null,
};
describe("AgentCard RiskNoticeModal", { timeout: 30000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
// Clear localStorage risk key before each test
try {
localStorage.removeItem(RISK_KEY_PREFIX + "copilot");
} catch {
// ignore
}
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
try {
localStorage.removeItem(RISK_KEY_PREFIX + "copilot");
} catch {
// ignore
}
});
it("first DNS activation opens risk modal (does NOT call onDnsToggle yet)", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: baseAgentState,
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Click DNS toggle (Start DNS)
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
expect(dnsBtn).not.toBeNull();
await act(async () => {
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Risk modal should be open (dialog element present)
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
// onDnsToggle should NOT have been called yet
expect(onDnsToggle).not.toHaveBeenCalled();
}, 30000);
it("accepting risk modal closes modal and calls onDnsToggle with true", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: baseAgentState,
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Click DNS toggle to open modal
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
await act(async () => {
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Modal should be open
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
// Click "I understand" (accept) button — uses t("understand") key
const acceptBtn = Array.from(document.querySelectorAll("button")).find((b) =>
b.textContent?.includes("understand")
);
expect(acceptBtn).not.toBeNull();
await act(async () => {
acceptBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Modal should be closed
expect(document.body.querySelector('[role="dialog"]')).toBeNull();
// onDnsToggle should have been called with true
expect(onDnsToggle).toHaveBeenCalledWith("copilot", true);
// localStorage flag should be set
const stored = localStorage.getItem(RISK_KEY_PREFIX + "copilot");
expect(stored).toBe("true");
}, 30000);
it("accepting risk writes localStorage exactly once (RiskNoticeModal is sole writer)", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: baseAgentState,
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Click DNS toggle to open modal
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
await act(async () => {
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Accept modal
const acceptBtn = Array.from(document.querySelectorAll("button")).find((b) =>
b.textContent?.includes("understand")
);
await act(async () => {
acceptBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const riskKey = "omniroute-agentbridge-risk-dismissed-copilot";
const riskWrites = setItemSpy.mock.calls.filter(([key]) => key === riskKey);
// Must be exactly ONE write — RiskNoticeModal is the sole persistence owner (D16)
expect(riskWrites).toHaveLength(1);
expect(riskWrites[0][1]).toBe("true");
setItemSpy.mockRestore();
}, 30000);
it("second DNS activation does NOT open modal when localStorage flag is set", async () => {
// Pre-set the localStorage flag (simulates accepted risk on previous session)
try {
localStorage.setItem(RISK_KEY_PREFIX + "copilot", "true");
} catch {
// ignore
}
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: baseAgentState,
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Click DNS toggle
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
await act(async () => {
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Risk modal should NOT appear
expect(document.body.querySelector('[role="dialog"]')).toBeNull();
// onDnsToggle should have been called directly (no modal gate)
expect(onDnsToggle).toHaveBeenCalledWith("copilot", true);
}, 30000);
it("cancelling risk modal keeps modal closed and does NOT call onDnsToggle", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: baseAgentState,
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Click DNS toggle to open modal
const dnsBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
await act(async () => {
dnsBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Modal should be open
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
// Click Cancel button
const cancelBtn = Array.from(document.querySelectorAll("button")).find((b) =>
b.textContent?.includes("cancel")
);
expect(cancelBtn).not.toBeNull();
await act(async () => {
cancelBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Modal should be closed
expect(document.body.querySelector('[role="dialog"]')).toBeNull();
// onDnsToggle should NOT have been called
expect(onDnsToggle).not.toHaveBeenCalled();
// localStorage flag should NOT be set (user cancelled)
const stored = localStorage.getItem(RISK_KEY_PREFIX + "copilot");
expect(stored).toBeNull();
}, 30000);
});
+214
View File
@@ -0,0 +1,214 @@
// @vitest-environment jsdom
/**
* UI unit tests for AgentCard — DNS toggle + wizard open.
*
* Note: AgentCard/SetupWizard import @/mitm/types (zod types only, no DB).
* We set testTimeout=30000 to handle the initial transform overhead.
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) =>
React.createElement("a", { href }, children),
}));
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: [] }),
} as unknown as Response);
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
// Minimal mock target (matches MitmTarget shape but no heavy imports)
const mockTarget = {
id: "copilot" as const,
name: "GitHub Copilot",
icon: "code",
color: "#10B981",
hosts: ["api.githubcopilot.com"],
port: 443,
endpointPatterns: ["/chat/completions"],
defaultModels: [{ id: "gpt-4o", name: "GPT-4o", alias: "gpt-4o" }],
setupTutorial: {
steps: ["Step 1", "Step 2"],
detection: { command: "which copilot", platform: "all" as const },
},
handler: () => Promise.resolve({ default: class {} as never }),
riskNoticeKey: "providers.riskNotice.oauth",
viability: "supported" as const,
};
describe("AgentCard", { timeout: 30000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders agent name and hosts", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: undefined,
serverRunning: false,
mappings: [],
onDnsToggle: vi.fn(),
onMappingsSave: vi.fn(),
})
);
});
expect(document.body.innerHTML).toContain("GitHub Copilot");
expect(document.body.innerHTML).toContain("api.githubcopilot.com");
}, 30000);
it("expands on click and shows DNS toggle", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: undefined,
serverRunning: true,
mappings: [],
onDnsToggle: vi.fn(),
onMappingsSave: vi.fn(),
})
);
});
const header = container.querySelector("button[aria-expanded]");
expect(header).not.toBeNull();
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(document.body.innerHTML).toContain("startDns");
}, 30000);
it("calls onDnsToggle when DNS button clicked", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
// Simulate that the per-agent RiskNoticeModal (Fix4 M5) has already been
// accepted for this agent — otherwise the DNS click opens the modal first
// and onDnsToggle is only called after the user accepts. We test the
// "already accepted" path here; the modal flow is covered by
// tests/unit/ui/agent-card-risk-modal.test.tsx.
localStorage.setItem("omniroute-agentbridge-risk-dismissed-copilot", "true");
const onDnsToggle = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: {
agent_id: "copilot",
dns_enabled: false,
cert_trusted: false,
setup_completed: false,
last_started_at: null,
last_error: null,
},
serverRunning: true,
mappings: [],
onDnsToggle,
onMappingsSave: vi.fn(),
})
);
});
// Expand card
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Find and click DNS button
const dnsButton = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("startDns")
);
expect(dnsButton).not.toBeNull();
await act(async () => {
dnsButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onDnsToggle).toHaveBeenCalledWith("copilot", true);
}, 30000);
it("opens wizard when setup wizard button clicked", async () => {
const { AgentCard } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/AgentCard"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(AgentCard, {
target: mockTarget,
agentState: undefined,
serverRunning: true,
mappings: [],
onDnsToggle: vi.fn(),
onMappingsSave: vi.fn(),
})
);
});
// Expand card first
const header = container.querySelector("button[aria-expanded]");
await act(async () => {
header?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
// Find setup wizard button
const wizardBtn = Array.from(document.querySelectorAll("button")).find((b) =>
b.textContent?.includes("setupWizard")
);
await act(async () => {
wizardBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(document.body.querySelector('[role="dialog"]')).not.toBeNull();
}, 30000);
});
+89
View File
@@ -0,0 +1,89 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AllocationTable } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/AllocationTable"
);
const ALLOCATIONS = [
{ apiKeyId: "key_1", weight: 60, policy: "hard" as const },
{ apiKeyId: "key_2", weight: 40, policy: "soft" as const },
];
const KEY_LABELS: Record<string, string> = { key_1: "KeyOne", key_2: "KeyTwo" };
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function render(props: Parameters<typeof AllocationTable>[0]) {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<AllocationTable {...props} />);
});
}
describe("AllocationTable", { timeout: 10000 }, () => {
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
});
it("renders empty state when no allocations", async () => {
await render({ allocations: [], usage: null, keyLabels: {} });
expect(document.body.innerHTML).toContain("noAllocations");
});
it("renders key labels", async () => {
await render({ allocations: ALLOCATIONS, usage: null, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("KeyOne");
expect(document.body.innerHTML).toContain("KeyTwo");
});
it("renders weights correctly", async () => {
await render({ allocations: ALLOCATIONS, usage: null, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("60%");
expect(document.body.innerHTML).toContain("40%");
});
it("renders policy badges", async () => {
await render({ allocations: ALLOCATIONS, usage: null, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("hard");
expect(document.body.innerHTML).toContain("soft");
});
it("renders consumed values from usage perKey data", async () => {
const usage = {
dimensions: [
{
unit: "tokens",
window: "daily",
limit: 1000,
consumedTotal: 400,
perKey: [
{ apiKeyId: "key_1", consumed: 300, fairShare: 600, deficit: -300, borrowing: false },
{ apiKeyId: "key_2", consumed: 100, fairShare: 400, deficit: 300, borrowing: true },
],
},
],
burnRate: null,
};
await render({ allocations: ALLOCATIONS, usage: usage as never, keyLabels: KEY_LABELS });
expect(document.body.innerHTML).toContain("300");
expect(document.body.innerHTML).toContain("100");
// borrowing indicator for key_2
expect(document.body.innerHTML).toContain("borrowingIndicator");
});
});
+104
View File
@@ -0,0 +1,104 @@
// @vitest-environment jsdom
/**
* UI unit test for the AuthzSection wildcard-CORS banner (#5602).
*
* The banner must appear when `/api/settings/authz-inventory` reports
* `cors.allowAll === true` (i.e. `CORS_ALLOW_ALL=true` at runtime) and stay
* hidden otherwise. It is the only runtime signal a wildcard-CORS
* misconfiguration is live. See docs/security/CORS.md.
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Stable identity fn — the real next-intl `t` is memoized per render. A fresh
// closure each render would flip AuthzSection's `useCallback([t])` dep and loop
// its mount fetch forever, so we return the SAME reference every call.
const translate = (key: string) => key;
vi.mock("next-intl", () => ({
useTranslations: () => translate,
}));
// Import the (heavy) dashboard component ONCE at module load rather than inside every
// render call. The dynamic import pulls the whole settings-page dependency graph through
// esbuild on first use (~20s cold); doing it per-test made the first test tip over the
// 30s per-test timeout while the second (warm, cached) passed. Hoisting moves that cost to
// module-eval time (outside any per-test timeout) and keeps each test body fast.
const AuthzSection = (
await import("../../../src/app/(dashboard)/dashboard/settings/components/AuthzSection")
).default;
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
function inventoryPayload(cors: { allowAll: boolean; allowedOrigins: string[] }) {
return {
tiers: [
{
name: "PUBLIC",
prefixes: ["/api/health"],
description: "public",
bypassable: false,
},
],
bypassEnabled: true,
bypassPrefixes: ["/api/mcp/"],
spawnCapablePrefixes: ["/api/cli-tools/runtime/"],
cors,
};
}
function mockInventoryFetch(cors: { allowAll: boolean; allowedOrigins: string[] }) {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(inventoryPayload(cors)),
} as unknown as Response);
}
async function renderAuthzSection(): Promise<HTMLElement> {
const container = makeContainer();
await act(async () => {
createRoot(container).render(React.createElement(AuthzSection));
});
// Flush the mount-time inventory fetch + resulting state update.
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
return container;
}
describe("AuthzSection wildcard-CORS banner (#5602)", { timeout: 60000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders the banner when cors.allowAll is true", async () => {
mockInventoryFetch({ allowAll: true, allowedOrigins: [] });
await renderAuthzSection();
const banner = document.querySelector('[data-testid="cors-wildcard-banner"]');
expect(banner).not.toBeNull();
});
it("does NOT render the banner when cors.allowAll is false", async () => {
mockInventoryFetch({ allowAll: false, allowedOrigins: ["https://app.example.com"] });
await renderAuthzSection();
const banner = document.querySelector('[data-testid="cors-wildcard-banner"]');
expect(banner).toBeNull();
});
});
+70
View File
@@ -0,0 +1,70 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// Stub next/dynamic — returns null component (recharts not needed in tests)
vi.mock("next/dynamic", () => ({
default: () => () => null,
}));
const { default: BurnRateChart } = await import(
"../../../src/app/(dashboard)/dashboard/costs/quota-share/components/BurnRateChart"
);
let container: HTMLDivElement | null = null;
let root: ReturnType<typeof createRoot> | null = null;
async function render(props: Parameters<typeof BurnRateChart>[0]) {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
container = document.createElement("div");
document.body.appendChild(container);
await act(async () => {
root = createRoot(container!);
root.render(<BurnRateChart {...props} />);
});
}
describe("BurnRateChart", { timeout: 10000 }, () => {
afterEach(() => {
if (root && container) act(() => root!.unmount());
container?.remove();
container = null;
root = null;
});
it("renders no-data state when usage is null", async () => {
await render({ usage: null });
expect(document.body.innerHTML).toContain("burnRateTitle");
expect(document.body.innerHTML).toContain("no data");
});
it("renders no-data state when burnRate is falsy", async () => {
const usage = {
dimensions: [],
burnRate: null,
};
await render({ usage: usage as never });
expect(document.body.innerHTML).toContain("no data");
});
it("renders chart when usage has burnRate data", async () => {
const usage = {
dimensions: [
{ unit: "tokens", window: "daily", limit: 100000, consumedTotal: 30000, perKey: [] },
],
burnRate: { tokensPerSecond: 10, timeToExhaustionMs: 7_000_000 },
};
await render({ usage: usage as never });
// Should not show no-data message
expect(document.body.innerHTML).not.toContain("no data yet");
// Should show exhaustion label
expect(document.body.innerHTML).toContain("burnRateExhaustsIn");
});
});
+128
View File
@@ -0,0 +1,128 @@
// @vitest-environment jsdom
/**
* UI unit tests for BypassListEditor — add/remove patterns.
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("BypassListEditor", { timeout: 30000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders default bypass patterns as read-only chips", async () => {
const { BypassListEditor } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/BypassListEditor"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(BypassListEditor, {
patterns: [],
onSave: vi.fn(),
})
);
});
expect(document.body.innerHTML).toContain("*.bank.*");
expect(document.body.innerHTML).toContain("*.okta.com");
}, 30000);
it("renders initial user patterns in textarea", async () => {
const { BypassListEditor } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/BypassListEditor"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(BypassListEditor, {
patterns: ["*.internal.corp", "sso.example.com"],
onSave: vi.fn(),
})
);
});
const textarea = container.querySelector("textarea");
expect(textarea?.value).toContain("*.internal.corp");
expect(textarea?.value).toContain("sso.example.com");
}, 30000);
it("calls onSave when Save button clicked", async () => {
const { BypassListEditor } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/BypassListEditor"
);
const onSave = vi.fn().mockResolvedValue(undefined);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(BypassListEditor, {
patterns: ["existing.com"],
onSave,
})
);
});
const saveBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("saveBypassList")
);
expect(saveBtn).not.toBeNull();
await act(async () => {
saveBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onSave).toHaveBeenCalled();
}, 30000);
it("renders save button", async () => {
const { BypassListEditor } = await import(
"../../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/BypassListEditor"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(
React.createElement(BypassListEditor, {
patterns: [],
onSave: vi.fn(),
})
);
});
const saveBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("saveBypassList")
);
expect(saveBtn).not.toBeNull();
}, 30000);
});
@@ -0,0 +1,123 @@
// @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 ─────────────────────────────────────────────────────────────────────
vi.mock("next/navigation", () => ({
notFound: () => {
throw new Error("NOT_FOUND");
},
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ connections: [], keys: [], data: [], cloudEnabled: false }),
}));
vi.mock("@/shared/constants/models", () => ({
PROVIDER_ID_TO_ALIAS: {},
getModelsByProviderId: () => [],
}));
// Stub ToolDetailClient — renders a testid with props
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/ToolDetailClient", () => ({
default: ({ toolId, category }: { toolId: string; category: string }) => (
<div data-testid="ToolDetailClient" data-toolid={toolId} data-category={category} />
),
}));
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: CliAgentsDetailPage } = await import(
"@/app/(dashboard)/dashboard/cli-agents/[id]/page"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
async function renderPage(id: string): Promise<{ container: HTMLElement; notFound: boolean }> {
let notFoundThrown = false;
let jsx: React.ReactNode | null = null;
try {
jsx = await CliAgentsDetailPage({ params: Promise.resolve({ id }) });
} catch (err: any) {
if (err?.message === "NOT_FOUND") {
notFoundThrown = true;
} else {
throw err;
}
}
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
if (!notFoundThrown && jsx) {
const root = createRoot(container);
act(() => {
root.render(jsx as React.ReactElement);
});
}
return { container, notFound: notFoundThrown };
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliAgentsDetailPage", () => {
it("renders ToolDetailClient for /dashboard/cli-agents/hermes-agent (category:agent)", async () => {
const { container, notFound } = await renderPage("hermes-agent");
expect(notFound).toBe(false);
const el = container.querySelector("[data-testid='ToolDetailClient']");
expect(el).not.toBeNull();
expect(el!.getAttribute("data-toolid")).toBe("hermes-agent");
expect(el!.getAttribute("data-category")).toBe("agent");
});
it("returns 404 for /dashboard/cli-agents/claude (category:code — cross-category)", async () => {
const { notFound } = await renderPage("claude");
expect(notFound).toBe(true);
});
it("returns 404 for /dashboard/cli-agents/invalid-id (unknown tool)", async () => {
const { notFound } = await renderPage("invalid-id-xyz");
expect(notFound).toBe(true);
});
});
+127
View File
@@ -0,0 +1,127 @@
// @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 notFoundCalled = false;
vi.mock("next/navigation", () => ({
notFound: () => {
notFoundCalled = true;
throw new Error("NOT_FOUND");
},
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
vi.mock("next/link", () => ({
default: ({
href,
children,
...props
}: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) => (
<a href={href} {...props}>
{children}
</a>
),
}));
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ connections: [], keys: [], data: [], cloudEnabled: false }),
}));
vi.mock("@/shared/constants/models", () => ({
PROVIDER_ID_TO_ALIAS: {},
getModelsByProviderId: () => [],
}));
// Stub ToolDetailClient — just renders a testid with the received props
vi.mock("@/app/(dashboard)/dashboard/cli-code/components/ToolDetailClient", () => ({
default: ({ toolId, category }: { toolId: string; category: string }) => (
<div data-testid="ToolDetailClient" data-toolid={toolId} data-category={category} />
),
}));
// ── Import after mocks ────────────────────────────────────────────────────────
const { default: CliCodeDetailPage } = await import(
"@/app/(dashboard)/dashboard/cli-code/[id]/page"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
async function renderPage(id: string): Promise<{ container: HTMLElement; notFound: boolean }> {
let notFoundThrown = false;
let jsx: React.ReactNode | null = null;
try {
jsx = await CliCodeDetailPage({ params: Promise.resolve({ id }) });
} catch (err: any) {
if (err?.message === "NOT_FOUND") {
notFoundThrown = true;
} else {
throw err;
}
}
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
if (!notFoundThrown && jsx) {
const root = createRoot(container);
act(() => {
root.render(jsx as React.ReactElement);
});
}
return { container, notFound: notFoundThrown };
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
notFoundCalled = false;
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CliCodeDetailPage", () => {
it("renders ToolDetailClient for /dashboard/cli-code/claude (category:code)", async () => {
const { container, notFound } = await renderPage("claude");
expect(notFound).toBe(false);
const el = container.querySelector("[data-testid='ToolDetailClient']");
expect(el).not.toBeNull();
expect(el!.getAttribute("data-toolid")).toBe("claude");
expect(el!.getAttribute("data-category")).toBe("code");
});
it("returns 404 for /dashboard/cli-code/hermes-agent (category:agent — cross-category)", async () => {
const { notFound } = await renderPage("hermes-agent");
expect(notFound).toBe(true);
});
it("returns 404 for /dashboard/cli-code/invalid-id (unknown tool)", async () => {
const { notFound } = await renderPage("invalid-id-xyz");
expect(notFound).toBe(true);
});
});
@@ -0,0 +1,49 @@
/**
* Regression guard: /dashboard/cli-code must NOT contain MITM UI.
* MITM setup now lives exclusively in AgentBridge (plan 11 §12 #10, R5-2).
*
* Uses source-text inspection — no JSDOM render needed.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PAGE_PATH = path.resolve(
__dirname,
"../../../src/app/(dashboard)/dashboard/cli-code/CliCodePageClient.tsx"
);
const src = readFileSync(PAGE_PATH, "utf-8");
describe("CliCodePageClient — no MITM duplication (R5-2)", () => {
it("MITM_TOOL_IDS constant is not defined", () => {
assert.ok(
!src.includes("MITM_TOOL_IDS"),
"MITM_TOOL_IDS must be removed from CliCodePageClient.tsx"
);
});
it("mitm tab value is not present in SegmentedControl options", () => {
assert.ok(
!src.includes('value: "mitm"'),
'Tab entry { value: "mitm" } must be removed from CliCodePageClient.tsx'
);
});
it("mitmClientsTab i18n key is not referenced in render", () => {
assert.ok(
!src.includes('t("mitmClientsTab")'),
"mitmClientsTab must not be called in CliCodePageClient.tsx"
);
});
it("AntigravityToolCard is not imported", () => {
assert.ok(
!src.includes("AntigravityToolCard"),
"AntigravityToolCard import must be removed from CLIToolsPageClient.tsx"
);
});
});
@@ -0,0 +1,84 @@
// @vitest-environment jsdom
//
// #5598 — Selecting the "fusion" routing strategy on the Global Routing defaults
// tab previously revealed no fusion-specific config (only the generic resilience
// fields). Fusion's engine knobs (judgeModel + fusionTuning) exist in the schema
// and the per-combo editor, but were never surfaced as global defaults. This
// asserts they now appear when fusion is the selected strategy.
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
// identity translator with no `has`, so translateOrFallback uses the English fallbacks
useTranslations: () => (key: string) => key,
}));
const { default: ComboDefaultsTab } = await import(
"../../../src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab"
);
function okJson(data: unknown) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(data) } as Response);
}
function setupFetch(strategy: string) {
vi.stubGlobal(
"fetch",
vi.fn((url: string) => {
if (String(url).includes("/combo-defaults")) return okJson({ comboDefaults: { strategy } });
if (String(url).includes("/api/providers")) return okJson({ connections: [] });
return okJson({}); // /api/settings
})
);
}
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderTab() {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(<ComboDefaultsTab />);
});
containers.push({ root, el });
return el;
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("ComboDefaultsTab fusion config (#5598)", () => {
it("shows fusion-specific fields when the fusion strategy is selected", async () => {
setupFetch("fusion");
const el = renderTab();
// RED before the fix: fusion had no config block → these labels never render.
await waitFor(() => el.textContent?.includes("Judge Model") === true);
expect(el.textContent).toContain("Judge Model");
expect(el.textContent).toContain("Min Panel");
expect(el.textContent).toContain("Straggler Grace (ms)");
expect(el.textContent).toContain("Panel Hard Timeout (ms)");
});
it("does not show fusion fields for a non-fusion strategy", async () => {
setupFetch("priority");
const el = renderTab();
// Let the component settle (load + render).
await new Promise((r) => setTimeout(r, 200));
expect(el.textContent).not.toContain("Judge Model");
});
});
@@ -0,0 +1,66 @@
// Pure unit test for the deselect filter semantics used by ComboFormModal in
// src/app/(dashboard)/dashboard/combos/page.tsx — ported from upstream PR
// decolua/9router#889 (Fajar Hidayat).
//
// The page-level handler removes every step whose qualified `model` matches
// the value sent from the ModelSelectModal, matching upstream JS behavior
// (`setModels(models.filter((m) => m !== model.value))`). Duplicates with
// different providerId/weight pointing at the same model id are all stripped.
import { describe, expect, it } from "vitest";
type Step = { model: string; providerId?: string; weight?: number };
// Mirror of src/app/(dashboard)/dashboard/combos/page.tsx::handleDeselectModel.
// Kept in lockstep so the same filter behavior is checked here, leaf-only.
function deselectModel(models: Step[], model: { value?: string } | string): Step[] {
const value =
typeof (model as any)?.value === "string"
? (model as any).value
: typeof model === "string"
? model
: "";
if (!value) return models;
return models.filter((m) => m.model !== value);
}
describe("ComboFormModal deselect handler (upstream PR #889)", () => {
it("removes the single matching step when called with { value }", () => {
const models: Step[] = [
{ model: "openai/gpt-4o", weight: 50 },
{ model: "anthropic/claude-3-5-sonnet", weight: 50 },
];
const next = deselectModel(models, { value: "openai/gpt-4o" });
expect(next).toEqual([{ model: "anthropic/claude-3-5-sonnet", weight: 50 }]);
});
it("strips every duplicate of the same qualified model", () => {
const models: Step[] = [
{ model: "openai/gpt-4o", providerId: "openai-a", weight: 30 },
{ model: "openai/gpt-4o", providerId: "openai-b", weight: 70 },
{ model: "anthropic/claude-3-5-sonnet", weight: 0 },
];
const next = deselectModel(models, { value: "openai/gpt-4o" });
expect(next).toEqual([{ model: "anthropic/claude-3-5-sonnet", weight: 0 }]);
});
it("is a no-op when the value is not in the list", () => {
const models: Step[] = [{ model: "openai/gpt-4o", weight: 100 }];
const next = deselectModel(models, { value: "openai/gpt-3.5" });
expect(next).toEqual(models);
});
it("is a no-op for empty/missing value (defensive guard)", () => {
const models: Step[] = [{ model: "openai/gpt-4o", weight: 100 }];
expect(deselectModel(models, { value: "" })).toEqual(models);
expect(deselectModel(models, {})).toEqual(models);
});
it("accepts a raw string model identifier (legacy upstream call shape)", () => {
const models: Step[] = [
{ model: "openai/gpt-4o", weight: 50 },
{ model: "anthropic/claude-3-5-sonnet", weight: 50 },
];
const next = deselectModel(models, "openai/gpt-4o");
expect(next).toEqual([{ model: "anthropic/claude-3-5-sonnet", weight: 50 }]);
});
});
@@ -0,0 +1,84 @@
// @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";
import {
ResponseValidationEditor,
type ResponseValidationValue,
} from "@/app/(dashboard)/dashboard/combos/ResponseValidationEditor";
// Feature 4985 — the per-combo response-validation editor emits the declarative shape.
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot> | undefined;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
act(() => root?.unmount());
root = undefined;
container.remove();
vi.restoreAllMocks();
});
function render(value: ResponseValidationValue | undefined, onChange: (v: unknown) => void) {
act(() => {
root = createRoot(container);
root.render(<ResponseValidationEditor value={value} onChange={onChange} />);
});
}
function setTextarea(testid: string, text: string) {
const el = container.querySelector<HTMLTextAreaElement>(`[data-testid="${testid}"]`);
const setter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(el, text);
el!.dispatchEvent(new Event("input", { bubbles: true }));
});
}
describe("ResponseValidationEditor (4985)", () => {
it("turns forbidden-substring lines into an array (trimmed, no blanks)", () => {
const onChange = vi.fn();
render(undefined, onChange);
setTextarea("rv-forbidden", "I cannot help\n\n as an AI \n");
expect(onChange).toHaveBeenLastCalledWith({
forbiddenSubstrings: ["I cannot help", "as an AI"],
});
});
it("clears the whole config back to undefined when every field is emptied", () => {
const onChange = vi.fn();
render({ forbiddenSubstrings: ["x"] }, onChange);
setTextarea("rv-forbidden", "");
expect(onChange).toHaveBeenLastCalledWith(undefined);
});
it("adds a json-path predicate row with sane defaults", () => {
const onChange = vi.fn();
render(undefined, onChange);
const addBtn = container.querySelector<HTMLButtonElement>('[data-testid="rv-predicate-add"]');
act(() => addBtn!.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(onChange).toHaveBeenLastCalledWith({
jsonPathPredicates: [{ path: "", condition: "exists" }],
});
});
it("renders existing predicate rows from the value", () => {
render(
{ jsonPathPredicates: [{ path: "choices[0].message.content", condition: "nonEmpty" }] },
vi.fn()
);
const rows = container.querySelectorAll('[data-testid="rv-predicate-row"]');
expect(rows.length).toBe(1);
const pathInput = container.querySelector<HTMLInputElement>('[data-testid="rv-predicate-path"]');
expect(pathInput?.value).toBe("choices[0].message.content");
});
});
@@ -0,0 +1,110 @@
/**
* tests/unit/ui/comboFlowModel-breakers.test.ts
*
* TDD for U1b — enrichRunWithBreakers: overlays REAL circuit-breaker state
* (from GET /api/monitoring/health → providerHealth[provider]) onto a combo run's
* targets, so the cascade can show "CB: OPEN · retry 41s" instead of only the
* error-string heuristic.
* Run: node --import tsx/esm --test tests/unit/ui/comboFlowModel-breakers.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
enrichRunWithBreakers,
comboRunToFlow,
type ComboRunModel,
type TargetNodeModel,
} from "../../../src/app/(dashboard)/dashboard/combos/live/comboFlowModel.ts";
function mkRun(
targets: Array<Partial<TargetNodeModel> & { provider: string }>
): ComboRunModel {
return {
comboName: "c",
strategy: "priority",
outcome: "running",
startedAt: 0,
targets: targets.map((t, i) => ({
targetIndex: i,
provider: t.provider,
model: t.model ?? "m",
state: t.state ?? "idle",
...t,
})),
};
}
describe("enrichRunWithBreakers", () => {
it("returns null for a null run", () => {
assert.equal(enrichRunWithBreakers(null, {}), null);
});
it("returns the same run reference when no health map is provided", () => {
const run = mkRun([{ provider: "openai" }]);
assert.equal(enrichRunWithBreakers(run, null), run);
assert.equal(enrichRunWithBreakers(run, undefined), run);
});
it("attaches cbState + retryAfterMs for an OPEN provider breaker only", () => {
const run = mkRun([{ provider: "openai" }, { provider: "anthropic" }]);
const out = enrichRunWithBreakers(run, {
openai: { state: "OPEN", retryAfterMs: 41000 },
});
assert.ok(out);
assert.equal(out.targets[0].cbState, "OPEN");
assert.equal(out.targets[0].cbRetryAfterMs, 41000);
assert.equal(out.targets[1].cbState, undefined, "healthy/absent provider gets no badge");
});
it("does not attach a badge for a CLOSED (healthy) breaker", () => {
const run = mkRun([{ provider: "openai" }]);
const out = enrichRunWithBreakers(run, { openai: { state: "CLOSED", retryAfterMs: 0 } });
assert.equal(out?.targets[0].cbState, undefined);
});
it("surfaces HALF_OPEN and DEGRADED states (case-insensitive)", () => {
const run = mkRun([{ provider: "a" }, { provider: "b" }]);
const out = enrichRunWithBreakers(run, {
a: { state: "half_open", retryAfterMs: 5000 },
b: { state: "DEGRADED" },
});
assert.equal(out?.targets[0].cbState, "HALF_OPEN");
assert.equal(out?.targets[1].cbState, "DEGRADED");
});
it("does not mutate the input run (pure)", () => {
const run = mkRun([{ provider: "openai" }]);
enrichRunWithBreakers(run, { openai: { state: "OPEN", retryAfterMs: 1 } });
assert.equal(run.targets[0].cbState, undefined, "original run must be untouched");
});
it("strips a stale cbState when the breaker recovers to CLOSED", () => {
const run = mkRun([{ provider: "openai", cbState: "OPEN", cbRetryAfterMs: 9 }]);
const out = enrichRunWithBreakers(run, { openai: { state: "CLOSED" } });
assert.equal(out?.targets[0].cbState, undefined);
assert.equal(out?.targets[0].cbRetryAfterMs, undefined);
});
it("ignores unknown breaker state strings", () => {
const run = mkRun([{ provider: "openai" }]);
const out = enrichRunWithBreakers(run, { openai: { state: "WEIRD" } });
assert.equal(out?.targets[0].cbState, undefined);
});
it("comboRunToFlow carries cbState/cbRetryAfterMs into the target node data", () => {
const run = mkRun([{ provider: "openai" }, { provider: "anthropic" }]);
const enriched = enrichRunWithBreakers(run, {
openai: { state: "OPEN", retryAfterMs: 41000 },
});
const { nodes } = comboRunToFlow(enriched as ComboRunModel);
const target0 = nodes.find((n) => n.id === "target-0");
assert.equal(target0?.data.cbState, "OPEN");
assert.equal(target0?.data.cbRetryAfterMs, 41000);
const target1 = nodes.find((n) => n.id === "target-1");
assert.equal(target1?.data.cbState, undefined, "healthy provider node has no cbState");
});
});
@@ -0,0 +1,125 @@
/**
* tests/unit/ui/comboFlowModel-cooldown.test.ts
*
* TDD for F5.1 (Combo U1b Slice 2) — enrichRunWithConnectionCooldown: overlays the
* real per-provider connection-cooldown summary (from GET /api/monitoring/health →
* connectionHealth[provider]) onto a combo run's targets, so the cascade can badge
* "cooldown 2/3 · 28s" alongside the circuit-breaker badge.
*
* Mirrors comboFlowModel-breakers.test.ts (Slice 1).
* Run: node --import tsx/esm --test tests/unit/ui/comboFlowModel-cooldown.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
enrichRunWithConnectionCooldown,
comboRunToFlow,
type ComboRunModel,
type TargetNodeModel,
} from "../../../src/app/(dashboard)/dashboard/combos/live/comboFlowModel.ts";
function mkRun(targets: Array<Partial<TargetNodeModel> & { provider: string }>): ComboRunModel {
return {
comboName: "c",
strategy: "priority",
outcome: "running",
startedAt: 0,
targets: targets.map((t, i) => ({
targetIndex: i,
provider: t.provider,
model: t.model ?? "m",
state: t.state ?? "idle",
...t,
})),
};
}
describe("enrichRunWithConnectionCooldown", () => {
it("returns null for a null run", () => {
assert.equal(enrichRunWithConnectionCooldown(null, {}), null);
});
it("returns the same run reference when no health map is provided", () => {
const run = mkRun([{ provider: "openai" }]);
assert.equal(enrichRunWithConnectionCooldown(run, null), run);
assert.equal(enrichRunWithConnectionCooldown(run, undefined), run);
});
it("attaches cooldown count/total/retry for a provider with cooling connections", () => {
const run = mkRun([{ provider: "anthropic" }, { provider: "openai" }]);
const out = enrichRunWithConnectionCooldown(run, {
anthropic: { coolingDown: 2, total: 3, soonestRetryAfterMs: 28_000 },
});
assert.ok(out);
assert.equal(out.targets[0].cooldownCount, 2);
assert.equal(out.targets[0].cooldownTotal, 3);
assert.equal(out.targets[0].cooldownRetryAfterMs, 28_000);
assert.equal(
out.targets[1].cooldownCount,
undefined,
"provider with no cooldown gets no badge"
);
});
it("does not attach a badge when coolingDown is 0 or absent", () => {
const run = mkRun([{ provider: "a" }, { provider: "b" }]);
const out = enrichRunWithConnectionCooldown(run, {
a: { coolingDown: 0, total: 2, soonestRetryAfterMs: 0 },
b: {},
});
assert.equal(out?.targets[0].cooldownCount, undefined);
assert.equal(out?.targets[1].cooldownCount, undefined);
});
it("does not mutate the input run (pure)", () => {
const run = mkRun([{ provider: "openai" }]);
enrichRunWithConnectionCooldown(run, {
openai: { coolingDown: 1, total: 1, soonestRetryAfterMs: 5000 },
});
assert.equal(run.targets[0].cooldownCount, undefined, "original run must be untouched");
});
it("strips a stale cooldown badge when the provider's connections recover", () => {
const run = mkRun([
{ provider: "openai", cooldownCount: 2, cooldownTotal: 3, cooldownRetryAfterMs: 9000 },
]);
const out = enrichRunWithConnectionCooldown(run, {
openai: { coolingDown: 0, total: 3, soonestRetryAfterMs: 0 },
});
assert.equal(out?.targets[0].cooldownCount, undefined);
assert.equal(out?.targets[0].cooldownTotal, undefined);
assert.equal(out?.targets[0].cooldownRetryAfterMs, undefined);
});
it("comboRunToFlow carries the cooldown fields into the target node data", () => {
const run = mkRun([{ provider: "anthropic" }, { provider: "openai" }]);
const enriched = enrichRunWithConnectionCooldown(run, {
anthropic: { coolingDown: 1, total: 2, soonestRetryAfterMs: 12_000 },
});
const { nodes } = comboRunToFlow(enriched as ComboRunModel);
const target0 = nodes.find((n) => n.id === "target-0");
assert.equal(target0?.data.cooldownCount, 1);
assert.equal(target0?.data.cooldownTotal, 2);
assert.equal(target0?.data.cooldownRetryAfterMs, 12_000);
const target1 = nodes.find((n) => n.id === "target-1");
assert.equal(target1?.data.cooldownCount, undefined, "healthy provider node has no cooldown");
});
it("composes with enrichRunWithBreakers without clobbering cbState", async () => {
const { enrichRunWithBreakers } =
await import("../../../src/app/(dashboard)/dashboard/combos/live/comboFlowModel.ts");
const run = mkRun([{ provider: "anthropic" }]);
const withBreaker = enrichRunWithBreakers(run, {
anthropic: { state: "OPEN", retryAfterMs: 41_000 },
});
const both = enrichRunWithConnectionCooldown(withBreaker, {
anthropic: { coolingDown: 1, total: 2, soonestRetryAfterMs: 5000 },
});
assert.equal(both?.targets[0].cbState, "OPEN", "breaker overlay survives the cooldown overlay");
assert.equal(both?.targets[0].cooldownCount, 1);
});
});
+360
View File
@@ -0,0 +1,360 @@
/**
* tests/unit/ui/comboFlowModel.test.ts
*
* TDD for `comboFlowModel` — the pure reducer/model core of Tela B.
* Run: node --import tsx/esm --test tests/unit/ui/comboFlowModel.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
classifyFailKind,
reduceComboEvent,
comboRunToFlow,
type ComboRunModel,
type ComboEventInput,
} from "../../../src/app/(dashboard)/dashboard/combos/live/comboFlowModel.ts";
import { FLOW_EDGE_COLORS } from "../../../src/shared/components/flow/edgeStyles.ts";
// ── helpers ───────────────────────────────────────────────────────────────
function mkAttempt(
targetIndex: number,
strategy = "priority",
provider = `prov${targetIndex}`,
model = `model${targetIndex}`
): ComboEventInput {
return {
comboName: "test-combo",
targetIndex,
provider,
model,
type: "attempt",
strategy,
timestamp: 1000 + targetIndex,
};
}
function mkFailed(
targetIndex: number,
error: string,
provider = `prov${targetIndex}`,
model = `model${targetIndex}`
): ComboEventInput {
return {
comboName: "test-combo",
targetIndex,
provider,
model,
type: "failed",
error,
latencyMs: 100,
timestamp: 2000 + targetIndex,
};
}
function mkSucceeded(
targetIndex: number,
provider = `prov${targetIndex}`,
model = `model${targetIndex}`
): ComboEventInput {
return {
comboName: "test-combo",
targetIndex,
provider,
model,
type: "succeeded",
latencyMs: 42,
timestamp: 3000 + targetIndex,
};
}
// Full 6-event cascade:
// attempt(0) → failed(0,"429 rate limited") → attempt(1) → failed(1,"circuit open") → attempt(2) → succeeded(2)
function buildFullRun(): ComboRunModel {
let run: ComboRunModel | null = null;
run = reduceComboEvent(run, mkAttempt(0));
run = reduceComboEvent(run, mkFailed(0, "429 rate limited"));
run = reduceComboEvent(run, mkAttempt(1));
run = reduceComboEvent(run, mkFailed(1, "circuit open"));
run = reduceComboEvent(run, mkAttempt(2));
run = reduceComboEvent(run, mkSucceeded(2));
return run;
}
// ── classifyFailKind ──────────────────────────────────────────────────────
describe("classifyFailKind", () => {
it("returns undefined when error is absent", () => {
assert.equal(classifyFailKind(undefined), undefined);
assert.equal(classifyFailKind(""), undefined);
});
it("returns 'rate-limit' for 429 messages", () => {
assert.equal(classifyFailKind("429 Too Many Requests"), "rate-limit");
assert.equal(classifyFailKind("upstream rate limit exceeded"), "rate-limit");
assert.equal(classifyFailKind("RATE_LIMIT_EXCEEDED"), "rate-limit");
});
it("returns 'circuit-open' for circuit breaker messages", () => {
assert.equal(classifyFailKind("circuit open"), "circuit-open");
assert.equal(classifyFailKind("Provider circuit breaker is open"), "circuit-open");
assert.equal(classifyFailKind("CIRCUIT_OPEN"), "circuit-open");
});
it("returns 'cooldown' for cooldown messages", () => {
assert.equal(classifyFailKind("connection cooldown active"), "cooldown");
assert.equal(classifyFailKind("cooldown period"), "cooldown");
});
it("returns 'other' for unrecognized error strings", () => {
assert.equal(classifyFailKind("Internal server error"), "other");
assert.equal(classifyFailKind("timeout"), "other");
assert.equal(classifyFailKind("some unknown failure"), "other");
});
it("circuit-open takes precedence over rate-limit when both match", () => {
// edge: a message with both circuit and rate — circuit wins by regex ordering
const result = classifyFailKind("circuit open after 429");
assert.equal(result, "circuit-open");
});
});
// ── reduceComboEvent ──────────────────────────────────────────────────────
describe("reduceComboEvent — basic events", () => {
it("creates a new run on first attempt event (null input)", () => {
const run = reduceComboEvent(null, mkAttempt(0));
assert.equal(run.comboName, "test-combo");
assert.equal(run.strategy, "priority");
assert.equal(run.outcome, "running");
assert.ok(run.startedAt > 0);
assert.equal(run.finishedAt, undefined);
assert.equal(run.targets.length, 1);
assert.equal(run.targets[0].targetIndex, 0);
assert.equal(run.targets[0].provider, "prov0");
assert.equal(run.targets[0].model, "model0");
assert.equal(run.targets[0].state, "attempting");
});
it("marks target as failed and sets failKind on failed event", () => {
let run = reduceComboEvent(null, mkAttempt(0));
run = reduceComboEvent(run, mkFailed(0, "429 rate limited"));
assert.equal(run.targets[0].state, "failed");
assert.equal(run.targets[0].failKind, "rate-limit");
assert.equal(run.targets[0].error, "429 rate limited");
assert.equal(run.outcome, "running"); // not done yet
});
it("marks target as succeeded and sets outcome + finishedAt on succeeded event", () => {
let run = reduceComboEvent(null, mkAttempt(0));
run = reduceComboEvent(run, mkSucceeded(0));
assert.equal(run.targets[0].state, "succeeded");
assert.equal(run.targets[0].latencyMs, 42);
assert.equal(run.outcome, "succeeded");
assert.ok(run.finishedAt != null);
});
});
describe("reduceComboEvent — full cascade (3 targets)", () => {
it("produces 3 targets ordered by targetIndex", () => {
const run = buildFullRun();
assert.equal(run.targets.length, 3);
assert.equal(run.targets[0].targetIndex, 0);
assert.equal(run.targets[1].targetIndex, 1);
assert.equal(run.targets[2].targetIndex, 2);
});
it("states are [failed, failed, succeeded]", () => {
const run = buildFullRun();
assert.equal(run.targets[0].state, "failed");
assert.equal(run.targets[1].state, "failed");
assert.equal(run.targets[2].state, "succeeded");
});
it("failKinds are [rate-limit, circuit-open, undefined]", () => {
const run = buildFullRun();
assert.equal(run.targets[0].failKind, "rate-limit");
assert.equal(run.targets[1].failKind, "circuit-open");
assert.equal(run.targets[2].failKind, undefined);
});
it("outcome is 'succeeded' and finishedAt is set", () => {
const run = buildFullRun();
assert.equal(run.outcome, "succeeded");
assert.ok(run.finishedAt != null);
});
it("strategy is set from the attempt payload", () => {
const run = buildFullRun();
assert.equal(run.strategy, "priority");
});
});
describe("reduceComboEvent — ordering", () => {
it("keeps targets sorted by targetIndex even if events arrive out of order", () => {
// Unusual but defensive: attempt(2) then attempt(0)
let run = reduceComboEvent(null, mkAttempt(2));
run = reduceComboEvent(run, mkAttempt(0));
assert.equal(run.targets[0].targetIndex, 0);
assert.equal(run.targets[1].targetIndex, 2);
});
it("idempotently applies a repeated attempt for the same target", () => {
let run = reduceComboEvent(null, mkAttempt(0));
run = reduceComboEvent(run, mkAttempt(0)); // duplicate
assert.equal(run.targets.length, 1);
assert.equal(run.targets[0].state, "attempting");
});
});
describe("reduceComboEvent — comboName key", () => {
it("ignores events for a different comboName when run already exists", () => {
let run = reduceComboEvent(null, mkAttempt(0));
// Event for a different combo — should be ignored, run returned unchanged
const alienEvent: ComboEventInput = {
comboName: "other-combo",
targetIndex: 99,
provider: "alien",
model: "alien",
type: "failed",
error: "some error",
timestamp: 9999,
};
run = reduceComboEvent(run, alienEvent);
assert.equal(run.targets.length, 1);
assert.equal(run.comboName, "test-combo");
});
});
// ── comboRunToFlow ────────────────────────────────────────────────────────
describe("comboRunToFlow", () => {
it("returns N+3 nodes for N targets (request + strategy + N targets + response)", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
// 3 targets → 1 request + 1 strategy + 3 targets + 1 response = 6 nodes
assert.equal(nodes.length, 6, `expected 6 nodes, got ${nodes.length}`);
});
it("node types: first=request, second=strategy, N middle=target, last=response", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
assert.equal(nodes[0].type, "request");
assert.equal(nodes[1].type, "strategy");
assert.equal(nodes[2].type, "target");
assert.equal(nodes[3].type, "target");
assert.equal(nodes[4].type, "target");
assert.equal(nodes[5].type, "response");
});
it("target nodes carry provider, model, state, failKind in data", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
const t0 = nodes[2];
assert.equal((t0.data as Record<string, unknown>).provider, "prov0");
assert.equal((t0.data as Record<string, unknown>).model, "model0");
assert.equal((t0.data as Record<string, unknown>).state, "failed");
assert.equal((t0.data as Record<string, unknown>).failKind, "rate-limit");
const t2 = nodes[4];
assert.equal((t2.data as Record<string, unknown>).state, "succeeded");
assert.equal((t2.data as Record<string, unknown>).failKind, undefined);
});
it("strategy node carries the strategy name in data", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
assert.equal((nodes[1].data as Record<string, unknown>).strategy, "priority");
assert.equal((nodes[1].data as Record<string, unknown>).targetCount, 3);
});
it("edges: N+3 sequential edges (one per node-to-node link)", () => {
const run = buildFullRun();
const { edges } = comboRunToFlow(run);
// 6 nodes → 5 edges: request→strategy, strategy→t0, t0→t1, t1→t2, t2→response
assert.equal(edges.length, 5, `expected 5 edges, got ${edges.length}`);
});
it("each edge connects consecutive nodes in order", () => {
const run = buildFullRun();
const { nodes, edges } = comboRunToFlow(run);
for (let i = 0; i < edges.length; i++) {
assert.equal(edges[i].source, nodes[i].id, `edge[${i}].source mismatch`);
assert.equal(edges[i].target, nodes[i + 1].id, `edge[${i}].target mismatch`);
}
});
it("failed edges are styled with error color", () => {
const run = buildFullRun();
const { edges } = comboRunToFlow(run);
// edge[2] is strategy→t0 (failed)… actually edges are: [0]req→strat, [1]strat→t0, [2]t0→t1, [3]t1→t2, [4]t2→resp
// edge for t0 (failed): index 1 (strategy→t0)
// The edge going INTO t0 should reflect t0 state
const t0IncomingEdge = edges[1]; // strategy→target0
const style = t0IncomingEdge.style as Record<string, unknown> | undefined;
assert.ok(style != null, "edge should have style");
assert.equal(style.stroke, FLOW_EDGE_COLORS.error, "failed target edge should be error color");
});
it("succeeded edge is styled with active/green color", () => {
const run = buildFullRun();
const { edges } = comboRunToFlow(run);
// edge[4] is t2→response (t2 succeeded)
const t2ResponseEdge = edges[4];
const style = t2ResponseEdge.style as Record<string, unknown> | undefined;
assert.ok(style != null);
assert.equal(
style.stroke,
FLOW_EDGE_COLORS.active,
"succeeded target edge should be active/green color"
);
});
it("idle/attempting edges are styled with idle or last-used color (not error/green)", () => {
// A run with one target still attempting
let run = reduceComboEvent(null, mkAttempt(0));
const { edges } = comboRunToFlow(run);
// edge[1] = strategy→t0 (attempting)
const attemptingEdge = edges[1];
const style = attemptingEdge.style as Record<string, unknown> | undefined;
assert.ok(style != null);
assert.notEqual(style.stroke, FLOW_EDGE_COLORS.error);
assert.notEqual(style.stroke, FLOW_EDGE_COLORS.active);
});
it("produces deterministic node IDs", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
assert.equal(nodes[0].id, "request");
assert.equal(nodes[1].id, "strategy");
assert.equal(nodes[2].id, "target-0");
assert.equal(nodes[3].id, "target-1");
assert.equal(nodes[4].id, "target-2");
assert.equal(nodes[5].id, "response");
});
});
+213
View File
@@ -0,0 +1,213 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
import type { ComboRunModel } from "@/app/(dashboard)/dashboard/combos/live/comboFlowModel";
// ── Polyfill ResizeObserver (required by ReactFlow) ───────────────────────
beforeAll(() => {
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
});
// ── Mocks ─────────────────────────────────────────────────────────────────
// Stub @xyflow/react so ReactFlow renders without canvas/DOM measurement APIs
vi.mock("@xyflow/react", async () => {
const actual = (await vi.importActual("@xyflow/react")) as Record<string, unknown>;
return {
...actual,
Handle: (_props: Record<string, unknown>) => null,
Position: { Left: "left", Right: "right", Top: "top", Bottom: "bottom" },
};
});
// Stub next/image to avoid Next.js internals in jsdom
vi.mock("next/image", () => ({
default: (props: Record<string, unknown>) =>
React.createElement("img", { src: props.src as string, alt: props.alt as string }),
}));
// ── Import after mocks ─────────────────────────────────────────────────────
const { ComboLiveStudio } = await import("@/app/(dashboard)/dashboard/combos/live/ComboLiveStudio");
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Sample run: 2 failures then 1 success ────────────────────────────────
const SAMPLE_RUN: ComboRunModel = {
comboName: "daily-cascade",
strategy: "priority",
targets: [
{
targetIndex: 0,
provider: "openai",
model: "gpt-4o",
state: "failed",
latencyMs: 1200,
error: "429 rate limit exceeded",
failKind: "rate-limit",
},
{
targetIndex: 1,
provider: "anthropic",
model: "claude-3-sonnet",
state: "failed",
latencyMs: 800,
error: "circuit open after 5 failures",
failKind: "circuit-open",
},
{
targetIndex: 2,
provider: "gemini",
model: "gemini-1.5-pro",
state: "succeeded",
latencyMs: 620,
},
],
outcome: "succeeded",
startedAt: 1718000000000,
finishedAt: 1718000003000,
};
// ── Tests ─────────────────────────────────────────────────────────────────
describe("ComboLiveStudio", () => {
describe("with a static run prop", () => {
it("renders the studio wrapper", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='combo-live-studio']")).toBeTruthy();
});
it("renders the ReactFlow canvas (.react-flow)", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.querySelector(".react-flow")).toBeTruthy();
});
it("shows the combo name in the toolbar", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.textContent).toContain("daily-cascade");
});
it("shows the strategy in the toolbar", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.textContent).toContain("priority");
});
it("shows the outcome in the toolbar", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='run-outcome']")?.textContent).toBe("succeeded");
});
it("shows provider names from target nodes", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
const text = container.textContent ?? "";
// Node internals rendered via ReactFlow in jsdom — at minimum the toolbar
// shows the combo name. Provider names appear in node elements if ReactFlow
// renders custom node interiors.
expect(text).toContain("daily-cascade");
});
it("does NOT render the combo selector when a run prop is supplied", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='combo-selector']")).toBeNull();
});
it("renders the Single/Fleet toggle buttons", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='mode-single']")).toBeTruthy();
expect(container.querySelector("[data-testid='mode-fleet']")).toBeTruthy();
});
it("does NOT show the disconnected banner when isConnected=true", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} isConnected={true} />);
expect(container.querySelector("[data-testid='combo-disconnected-banner']")).toBeNull();
});
it("shows the disconnected banner when isConnected=false", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} isConnected={false} />);
expect(container.querySelector("[data-testid='combo-disconnected-banner']")).toBeTruthy();
});
});
describe("empty state (no run, no events)", () => {
it("renders the empty state when run=null", () => {
const container = mount(<ComboLiveStudio run={null} />);
expect(container.querySelector("[data-testid='combo-live-studio-empty']")).toBeTruthy();
});
it("shows the empty state message", () => {
const container = mount(<ComboLiveStudio run={null} />);
expect(container.textContent).toContain("No combo run available");
});
it("shows the combo selector with no events", () => {
const container = mount(<ComboLiveStudio comboEvents={[]} />);
// No run prop → selector should appear
expect(container.querySelector("[data-testid='combo-selector']")).toBeTruthy();
});
it("renders the studio wrapper even when empty", () => {
const container = mount(<ComboLiveStudio />);
expect(container.querySelector("[data-testid='combo-live-studio']")).toBeTruthy();
});
});
describe("fleet mode", () => {
it("shows fleet overview after clicking Fleet toggle", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} comboEvents={[]} />);
const fleetBtn = container.querySelector(
"[data-testid='mode-fleet']"
) as HTMLButtonElement | null;
expect(fleetBtn).toBeTruthy();
act(() => {
fleetBtn!.click();
});
// Fleet panel renders (either FleetOverview or its empty state)
expect(container.querySelector("[data-testid='combo-live-studio']")).toBeTruthy();
// The .react-flow canvas should no longer be visible in fleet mode
expect(container.querySelector(".react-flow")).toBeNull();
});
});
describe("disconnected state with a run", () => {
it("still renders the canvas when disconnected but run is provided", () => {
const container = mount(<ComboLiveStudio run={SAMPLE_RUN} isConnected={false} />);
// Canvas renders (graceful degrade — show last known state)
expect(container.querySelector(".react-flow")).toBeTruthy();
// Banner also shows
expect(container.querySelector("[data-testid='combo-disconnected-banner']")).toBeTruthy();
});
});
});
@@ -0,0 +1,68 @@
// @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";
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("CombosError boundary", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders a recoverable fallback instead of throwing", async () => {
const { default: CombosError } =
await import("../../../src/app/(dashboard)/dashboard/combos/error");
const container = makeContainer();
const root = createRoot(container);
const reset = vi.fn();
const error = Object.assign(new Error("boom"), { digest: "abc123" });
await act(async () => {
root.render(<CombosError error={error} reset={reset} />);
});
expect(container.querySelector("[role='alert']")).toBeTruthy();
expect(container.textContent).toContain("Failed to load combos");
});
it("calls reset() exactly once when Try Again is clicked", async () => {
const { default: CombosError } =
await import("../../../src/app/(dashboard)/dashboard/combos/error");
const container = makeContainer();
const root = createRoot(container);
const reset = vi.fn();
const error = Object.assign(new Error("boom"), { digest: undefined });
await act(async () => {
root.render(<CombosError error={error} reset={reset} />);
});
const button = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent === "Try Again"
) as HTMLButtonElement | undefined;
expect(button).toBeTruthy();
await act(async () => {
button?.click();
});
expect(reset).toHaveBeenCalledTimes(1);
});
});
+62
View File
@@ -0,0 +1,62 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { act } from "react";
let container: HTMLElement; let root: Root;
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
}
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container);
vi.stubGlobal("fetch", vi.fn(async (url: string, init: any) => {
if (url.includes("/compression/preview")) {
const body = JSON.parse(init.body);
return { ok: true, json: async () => ({ original: "user: x", compressed: `c-${body.engineId}`,
originalTokens: 10, compressedTokens: 5, savingsPct: 50, mode: "stacked", durationMs: 1,
engineBreakdown: [], diff: [], preservedBlocks: [], ruleRemovals: [] }) } as any;
}
if (url.includes("/compression/compare/verify")) {
const body = JSON.parse(init.body);
return { ok: true, json: async () => ({
results: body.items.map((it: any) => ({ id: it.id, verdict: "same", usdCost: 0.001, skippedCapped: false })),
totalUsd: 0.001 * body.items.length, capped: false }) } as any;
}
// /compression/compare
return { ok: true, json: async () => ({ rows: [
{ engine: "rtk", meanSavingsPercent: 43, meanRetention: 0.98, totalCompressedTokens: 700 },
{ engine: "lite", meanSavingsPercent: 1, meanRetention: 1.0, totalCompressedTokens: 1230 },
] }) } as any;
}));
});
afterEach(() => { act(() => root.unmount()); container.remove(); document.body.innerHTML = ""; vi.restoreAllMocks(); });
describe("CompareView", () => {
it("renders best-first ranked rows after load", async () => {
const { CompareView } = await import("@/app/(dashboard)/dashboard/compression/studio/CompareView");
await act(async () => { root.render(<CompareView text="$ npm install\n..." />); });
await act(async () => { (container.querySelector('[data-testid="compare-load"]') as HTMLButtonElement).dispatchEvent(new MouseEvent("click", { bubbles: true })); });
const rows = container.querySelectorAll('[data-testid="compare-row"]');
expect(rows.length).toBe(2);
expect(rows[0].textContent).toContain("rtk");
});
it("verify button is disabled until a judge model is entered, then renders verdicts on demand", async () => {
const { CompareView } = await import("@/app/(dashboard)/dashboard/compression/studio/CompareView");
await act(async () => { root.render(<CompareView text="$ npm install" />); });
await act(async () => { (container.querySelector('[data-testid="compare-load"]') as HTMLButtonElement).dispatchEvent(new MouseEvent("click", { bubbles: true })); });
const verifyBtn = container.querySelector('[data-testid="verify-all"]') as HTMLButtonElement;
expect(verifyBtn.disabled).toBe(true); // no judge model yet
const modelInput = container.querySelector('[data-testid="verify-model"]') as HTMLInputElement;
await act(async () => { setInputValue(modelInput, "claude-haiku"); });
expect((container.querySelector('[data-testid="verify-all"]') as HTMLButtonElement).disabled).toBe(false);
await act(async () => { (container.querySelector('[data-testid="verify-all"]') as HTMLButtonElement).dispatchEvent(new MouseEvent("click", { bubbles: true })); });
const verdicts = Array.from(container.querySelectorAll('[data-testid="verify-verdict"]')).map((c) => c.textContent);
expect(verdicts.filter((t) => t === "same").length).toBe(2);
});
});
@@ -0,0 +1,129 @@
// @vitest-environment jsdom
/**
* Port of upstream decolua/9router PR #925.
*
* Bug: For openai-compatible / anthropic-compatible providers, the AddApiKeyModal
* had no "Default Model" input and the saved payload omitted `defaultModel`, so the
* created connection persisted with `defaultModel = null` and was effectively
* unusable (no model to bind requests to). The fix adds a required Default Model
* field for compatible providers and threads it through the save payload.
*
* Two TDD tests:
* 1) Compatible provider: the "Default Model" input is rendered and its value
* is forwarded as `defaultModel` in the onSave payload.
* 2) Non-compatible (first-party) provider: the field is NOT rendered, and
* `defaultModel` stays undefined in the payload (no regression for the
* existing first-party flow).
*/
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: AddApiKeyModal } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/AddApiKeyModal"
);
const DEFAULT_MODEL_INPUT_SELECTOR = 'input[data-testid="compat-default-model-input"]';
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<AddApiKeyModal
isOpen
onSave={async () => undefined}
onClose={() => {}}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: () => Promise.resolve({ valid: true }) } as Response)
)
);
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("AddApiKeyModal — compatible provider default-model field (PR #925)", () => {
it("renders the Default Model input only when the provider is compatible", () => {
const compatEl = render({
provider: "openai-compatible:my-node",
providerName: "My OpenAI Compatible",
isCompatible: true,
});
expect(compatEl.querySelector(DEFAULT_MODEL_INPUT_SELECTOR)).toBeTruthy();
const nonCompatEl = render({ provider: "openai", providerName: "OpenAI" });
expect(nonCompatEl.querySelector(DEFAULT_MODEL_INPUT_SELECTOR)).toBeNull();
});
it("threads defaultModel from the form into the save payload for compatible providers", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
provider: "openai-compatible:my-node",
providerName: "My OpenAI Compatible",
isCompatible: true,
onSave,
});
const nameInput = el.querySelector<HTMLInputElement>('input[placeholder="productionKey"]')!;
const apiKeyInput = el.querySelector<HTMLInputElement>('input[type="password"]')!;
const defaultModelInput = el.querySelector<HTMLInputElement>(DEFAULT_MODEL_INPUT_SELECTOR)!;
setInputValue(nameInput, "My Connection");
setInputValue(apiKeyInput, "sk-test-key");
setInputValue(defaultModelInput, "gpt-4o-mini");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.defaultModel).toBe("gpt-4o-mini");
});
});
@@ -0,0 +1,156 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Minimal i18n stub — returns key as-is so assertions are stable.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
if (values && typeof values.count !== "undefined" && typeof values.total !== "undefined") {
return `${values.count}/${values.total} ${key}`;
}
return key;
},
}));
// Stub fetch before importing the component.
const fetchCalls: string[] = [];
const mockFetch = vi.fn((url: string) => {
fetchCalls.push(url);
return Promise.resolve({
ok: true,
json: () => Promise.resolve([]),
headers: {
get: (name: string) => (name === "x-total-count" ? "0" : null),
},
} as unknown as Response);
});
vi.stubGlobal("fetch", mockFetch);
// Import component after mocks.
const { default: ComplianceTab } = await import(
"../../../src/app/(dashboard)/dashboard/audit/ComplianceTab"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
async function waitForCondition(fn: () => boolean, timeoutMs = 5000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitForCondition timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderTab() {
// Must set IS_REACT_ACT_ENVIRONMENT before each render.
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(<ComplianceTab />);
});
containers.push({ root, el });
return el;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
fetchCalls.length = 0;
mockFetch.mockClear();
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
});
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("ComplianceTab — actor filter", { timeout: 30000 }, () => {
it("renders the actor label, input and datalist", async () => {
const el = renderTab();
// Wait until fetch resolves and the component re-renders with the filter grid.
await waitForCondition(() => el.querySelector('input[list="compliance-actors"]') !== null);
// The actor label text should appear (i18n stub returns key as-is).
expect(el.textContent).toContain("actor");
// An input with list="compliance-actors" must exist.
const actorInput = el.querySelector('input[list="compliance-actors"]');
expect(actorInput).toBeTruthy();
// A datalist with id="compliance-actors" must exist.
const datalist = el.querySelector("datalist#compliance-actors");
expect(datalist).toBeTruthy();
});
it("initial fetch does not include actor param", async () => {
renderTab();
// Wait until at least one fetch call is made.
await waitForCondition(() => fetchCalls.length > 0);
// The first fetch should not include an actor param (state is "").
expect(fetchCalls[0]).not.toContain("actor=");
});
it("re-fetches with actor param after typing in the actor input", async () => {
const el = renderTab();
await waitForCondition(() => el.querySelector('input[list="compliance-actors"]') !== null);
const beforeCount = fetchCalls.length;
const actorInput = el.querySelector(
'input[list="compliance-actors"]'
) as HTMLInputElement | null;
expect(actorInput).toBeTruthy();
// Simulate React controlled input change via nativeInputValueSetter + dispatchEvent.
act(() => {
if (actorInput) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
nativeInputValueSetter?.call(actorInput, "admin");
actorInput.dispatchEvent(new Event("input", { bubbles: true }));
actorInput.dispatchEvent(new Event("change", { bubbles: true }));
}
});
// Wait for a new fetch triggered by the actor state change.
await waitForCondition(
() => fetchCalls.slice(beforeCount).some((url) => url.includes("actor=admin")),
5000
);
expect(fetchCalls.some((url) => url.includes("actor=admin"))).toBe(true);
});
it("clearFilters button exists and clicking it does not throw", async () => {
const el = renderTab();
await waitForCondition(() => el.querySelector('input[list="compliance-actors"]') !== null);
// Find the clearFilters button (i18n stub returns key "clearFilters").
const clearBtn = Array.from(el.querySelectorAll("button")).find((b) =>
b.textContent?.includes("clearFilters")
);
expect(clearBtn).toBeTruthy();
// Clicking the button should not throw.
expect(() => {
act(() => {
clearBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}).not.toThrow();
});
});
@@ -0,0 +1,122 @@
// @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";
import CompressionLogTab from "@/app/(dashboard)/dashboard/logs/CompressionLogTab";
// Keys that CompressionLogTab uses from the "logs" namespace.
const LOGS_KEYS = ["loading", "compressionLogTitle", "compressionLogEmpty", "tokens"] as const;
// Track calls to useTranslations so we can assert the namespace.
const namespacesSeen: string[] = [];
// i18n stub that simulates next-intl's useTranslations.
// Only the "logs" namespace has the compression-related keys.
const logsMessages: Record<string, string> = {
loading: "Loading...",
compressionLogTitle: "Compression Log",
compressionLogEmpty:
"No compressed requests yet. Compression stats will appear here when requests are processed with compression enabled.",
tokens: "tokens",
};
vi.mock("next-intl", () => ({
useTranslations: (namespace: string) => {
namespacesSeen.push(namespace);
return (key: string) => {
if (namespace === "logs") {
return logsMessages[key] ?? `_MISSING_${key}`;
}
// Any other namespace returns a sentinel so tests can catch wrong namespace.
return `_WRONG_NS_${namespace}_${key}`;
};
},
}));
// Mock fetch so the component doesn't fail on network calls.
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([]),
} as unknown as Response)
)
);
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("CompressionLogTab — logs namespace", { timeout: 30000 }, () => {
beforeEach(() => {
namespacesSeen.length = 0;
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) {
cleanupCallbacks.pop()?.();
}
document.body.innerHTML = "";
});
it("renders without missing-key sentinels — all required logs.* keys are present", async () => {
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<CompressionLogTab />);
});
// The rendered text must NOT contain any _MISSING_ or _WRONG_NS_ sentinel.
const text = container.textContent ?? "";
expect(text).not.toContain("_MISSING_");
expect(text).not.toContain("_WRONG_NS_");
});
it("uses the 'logs' namespace (not 'settings')", async () => {
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<CompressionLogTab />);
});
// Verify that useTranslations was called with "logs".
expect(namespacesSeen).toContain("logs");
// Must NOT have been called with "settings".
expect(namespacesSeen).not.toContain("settings");
});
it("renders loading state text from logs namespace", async () => {
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<CompressionLogTab />);
});
// Either the loading text or the empty state text should appear — both come from logs keys.
const text = container.textContent ?? "";
const hasExpectedText =
text.includes(logsMessages.loading) ||
text.includes(logsMessages.compressionLogEmpty) ||
text.includes(logsMessages.compressionLogTitle);
expect(hasExpectedText).toBe(true);
});
it("all required logs keys have defined values in the mock", () => {
// Sanity-check: the test mock itself covers all the keys the component uses.
for (const key of LOGS_KEYS) {
expect(logsMessages[key]).toBeDefined();
expect(typeof logsMessages[key]).toBe("string");
}
});
});
@@ -0,0 +1,119 @@
// @vitest-environment jsdom
//
// T06 — CompressionPipelineEditor (gaps v3.8.42). The drag-reorder logic itself lives in
// the pure `compressionPipelineModel` (covered by compression-pipeline-model.test.ts); here
// we assert the controlled-component wiring: rendering one row per step and reporting
// add/remove/patch edits through `onChange`.
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
const { CompressionPipelineEditor } = await import(
"../../../src/shared/components/compression/CompressionPipelineEditor"
);
const TABLE = {
rtk: ["standard", "aggressive"],
caveman: ["lite", "full", "ultra"],
} as const;
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
function render(steps: { engine: string; intensity?: string }[], onChange: (s: unknown) => void) {
act(() => {
root.render(
<CompressionPipelineEditor
steps={steps}
onChange={onChange}
engineIntensities={TABLE as unknown as Record<string, readonly string[]>}
/>
);
});
}
describe("CompressionPipelineEditor (T06)", () => {
it("renders one sortable row per step", () => {
render(
[
{ engine: "rtk", intensity: "standard" },
{ engine: "caveman", intensity: "full" },
],
() => {}
);
expect(container.querySelector('[data-testid="compression-pipeline-editor"]')).toBeTruthy();
expect(container.querySelectorAll('[data-testid^="pipeline-row-"]').length).toBe(2);
// each row exposes a drag handle
expect(container.querySelectorAll('[data-testid^="pipeline-drag-"]').length).toBe(2);
});
it("Add step appends a normalized step via onChange", () => {
let received: { engine: string; intensity?: string }[] | null = null;
render([{ engine: "rtk", intensity: "standard" }], (s) => {
received = s as typeof received;
});
const addBtn = container.querySelector('[data-testid="pipeline-add-step"]') as HTMLButtonElement;
act(() => addBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(received).not.toBeNull();
expect(received!.length).toBe(2);
expect(received![1].engine).toBe("rtk");
// appended step is normalized (valid intensity for the engine)
expect(TABLE.rtk).toContain(received![1].intensity);
});
it("Remove drops the row; the button is disabled when only one step remains", () => {
let received: unknown[] | null = null;
render(
[
{ engine: "rtk", intensity: "standard" },
{ engine: "caveman", intensity: "full" },
],
(s) => {
received = s as unknown[];
}
);
const removeBtn = container.querySelector(
'[data-testid="pipeline-remove-0"]'
) as HTMLButtonElement;
expect(removeBtn.disabled).toBe(false);
act(() => removeBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
expect(received).toEqual([{ engine: "caveman", intensity: "full" }]);
// single-step pipeline: remove is disabled (never below minLength 1)
render([{ engine: "rtk", intensity: "standard" }], () => {});
const onlyRemove = container.querySelector(
'[data-testid="pipeline-remove-0"]'
) as HTMLButtonElement;
expect(onlyRemove.disabled).toBe(true);
});
it("changing the engine re-normalizes the intensity through onChange", () => {
let received: { engine: string; intensity?: string }[] | null = null;
render([{ engine: "rtk", intensity: "aggressive" }], (s) => {
received = s as typeof received;
});
const engineSelect = container.querySelector(
'[data-testid="pipeline-row-0"] select[aria-label="Engine"]'
) as HTMLSelectElement;
act(() => {
engineSelect.value = "caveman";
engineSelect.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(received).not.toBeNull();
// 'aggressive' is not a caveman intensity → coerced to the first caveman intensity
expect(received![0].engine).toBe("caveman");
expect(TABLE.caveman).toContain(received![0].intensity);
expect(received![0].intensity).toBe("lite");
});
});
@@ -0,0 +1,89 @@
/**
* tests/unit/ui/compression-replay-reducer.test.ts
*
* F5.1 coverage gap (F3.2): the `useCompressionReplay` state machine was untested
* (only buildReplayFrames was covered). This pins the pure reducer + timing.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
replayReducer,
INITIAL_STATE,
stepMs,
type ReplayState,
} from "../../../src/app/(dashboard)/dashboard/compression/studio/useCompressionReplay.ts";
describe("replayReducer — compression replay state machine (F3.2)", () => {
it("starts idle: frameIndex -1, not playing, speed 1", () => {
assert.deepEqual(INITIAL_STATE, { frameIndex: -1, isPlaying: false, speed: 1 });
});
it("PLAY without frameIndex starts playing and keeps the current frame", () => {
const s = replayReducer({ frameIndex: 2, isPlaying: false, speed: 1 }, { type: "PLAY" });
assert.equal(s.isPlaying, true);
assert.equal(s.frameIndex, 2);
});
it("PLAY with frameIndex restarts from that frame (used for replay-from-start)", () => {
const s = replayReducer(INITIAL_STATE, { type: "PLAY", frameIndex: 0 });
assert.equal(s.isPlaying, true);
assert.equal(s.frameIndex, 0);
});
it("PAUSE stops playing but keeps the frame", () => {
const s = replayReducer({ frameIndex: 1, isPlaying: true, speed: 1 }, { type: "PAUSE" });
assert.equal(s.isPlaying, false);
assert.equal(s.frameIndex, 1);
});
it("TICK advances one frame while below the last", () => {
const start: ReplayState = { frameIndex: -1, isPlaying: true, speed: 1 };
const a = replayReducer(start, { type: "TICK", totalFrames: 3 });
assert.equal(a.frameIndex, 0);
assert.equal(a.isPlaying, true);
const b = replayReducer(a, { type: "TICK", totalFrames: 3 });
assert.equal(b.frameIndex, 1);
assert.equal(b.isPlaying, true);
});
it("TICK stops at the last frame and clears isPlaying (auto-stop at end)", () => {
const atSecondLast: ReplayState = { frameIndex: 1, isPlaying: true, speed: 1 };
const done = replayReducer(atSecondLast, { type: "TICK", totalFrames: 3 });
assert.equal(done.frameIndex, 2, "lands on the last frame index (totalFrames-1)");
assert.equal(done.isPlaying, false, "playback stops once the last frame is reached");
});
it("RESET returns to idle (-1, not playing) but preserves speed", () => {
const s = replayReducer({ frameIndex: 2, isPlaying: true, speed: 3 }, { type: "RESET" });
assert.equal(s.frameIndex, -1);
assert.equal(s.isPlaying, false);
assert.equal(s.speed, 3, "speed must survive a reset");
});
it("SET_SPEED changes speed without touching frame/play state", () => {
const s = replayReducer({ frameIndex: 1, isPlaying: true, speed: 1 }, {
type: "SET_SPEED",
speed: 3,
});
assert.equal(s.speed, 3);
assert.equal(s.frameIndex, 1);
assert.equal(s.isPlaying, true);
});
it("unknown action is a no-op (returns the same state reference)", () => {
const state: ReplayState = { frameIndex: 0, isPlaying: false, speed: 1 };
// @ts-expect-error — exercising the default branch with an invalid action
assert.equal(replayReducer(state, { type: "NOPE" }), state);
});
});
describe("stepMs — frame pacing by speed (F3.2)", () => {
it("maps speed to a shorter interval as speed rises", () => {
assert.equal(stepMs(1), 400);
assert.equal(stepMs(3), 133); // round(400/3)
assert.equal(stepMs(0.3), 1333); // round(400/0.3)
assert.ok(stepMs(3) < stepMs(1) && stepMs(1) < stepMs(0.3), "monotonic: faster = shorter");
});
});
@@ -0,0 +1,98 @@
// @vitest-environment jsdom
/**
* F5.2 finding (HIGH): changing replay speed mid-play was silently ignored — `startTick`
* read `speedRef.current` synchronously but the ref was only synced in a post-render
* effect, so the new interval kept the OLD cadence. This drives the hook with fake timers
* to prove a mid-play speed change actually takes effect.
*/
import React, { act, useMemo, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { useCompressionReplay } from "@/app/(dashboard)/dashboard/compression/studio/useCompressionReplay";
import { compressionEventToModel } from "@/app/(dashboard)/dashboard/compression/studio/compressionFlowModel";
import type {
CompressionCompletedPayload,
} from "@/lib/events/types";
// 3 engine steps → buildReplayFrames yields 3 frames (indices 0,1,2).
const PAYLOAD: CompressionCompletedPayload = {
requestId: "r1",
comboId: "c",
mode: "stacked",
originalTokens: 1000,
compressedTokens: 700,
savingsPercent: 30,
engineBreakdown: [
{ engine: "a", originalTokens: 1000, compressedTokens: 900, savingsPercent: 10, techniquesUsed: [], durationMs: 1 },
{ engine: "b", originalTokens: 900, compressedTokens: 800, savingsPercent: 11, techniquesUsed: [], durationMs: 1 },
{ engine: "c", originalTokens: 800, compressedTokens: 700, savingsPercent: 12, techniquesUsed: [], durationMs: 1 },
],
timestamp: 1718000000000,
};
let api: ReturnType<typeof useCompressionReplay> | null = null;
function Harness() {
// Memoize so the model identity is stable across re-renders (otherwise the
// model-change effect would RESET on every render).
const model = useMemo(() => compressionEventToModel(PAYLOAD), []);
const hook = useCompressionReplay(model);
// Capture in an effect (not during render) so we don't reassign a module-scoped
// variable mid-render. The effect runs after each commit, so `api` tracks the latest.
useEffect(() => {
api = hook;
});
return null;
}
const containers: HTMLElement[] = [];
function mount(): void {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<Harness />);
});
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
api = null;
});
describe("useCompressionReplay — mid-play speed change (F5.2)", () => {
it("applies a new speed to the running ticker (was silently ignored before the fix)", () => {
mount();
expect(api!.totalFrames).toBe(3);
// Play at speed 1 (interval 400ms): one tick advances to frame 0.
act(() => {
api!.play();
});
act(() => {
vi.advanceTimersByTime(400);
});
expect(api!.frameIndex).toBe(0);
// Speed up to 3 (interval ~133ms) WHILE playing, then advance only 133ms.
// With the fix the ticker restarts at the faster cadence and advances;
// without it the interval stayed at 400ms and 133ms would not tick.
act(() => {
api!.setSpeed(3);
});
act(() => {
vi.advanceTimersByTime(133);
});
expect(api!.frameIndex).toBe(1);
expect(api!.speed).toBe(3);
});
});
@@ -0,0 +1,143 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// ── Mock next-intl (CompressionSettingsTab calls useTranslations) ──────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mountInContainer(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Mock fetch ────────────────────────────────────────────────────────────
// CompressionSettingsTab fetches:
// GET /api/settings/compression → returns CompressionConfig (or null)
// GET /api/compression/rules → returns { rules: RuleMetadata[] }
function setupFetchMock() {
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
const url = input.toString();
// D0 tile reads this; it must resolve to a valid Summary BEFORE the generic
// /api/settings/compression match (the run-telemetry URL contains that prefix).
if (url.includes("/run-telemetry")) {
return new Response(
JSON.stringify({
totalRuns: 0,
totalTokensSaved: 0,
runsWithStyles: 0,
bypassCount: 0,
totalOutputTokens: 0,
appliedStyleCounts: {},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
if (url.includes("/api/settings/compression")) {
return new Response(
JSON.stringify({
enabled: false,
defaultMode: "off",
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
comboOverrides: {},
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}
if (url.includes("/api/compression/rules")) {
return new Response(JSON.stringify({ rules: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({}), { status: 404 });
});
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe("CompressionSettingsPage", () => {
it("mounts without throwing and renders content", { timeout: 15000 }, async () => {
setupFetchMock();
const { default: CompressionSettingsPage } =
await import("../../../src/app/(dashboard)/dashboard/context/settings/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionSettingsPage />);
});
// Flush any pending microtasks from effects
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.children.length).toBeGreaterThan(0);
// D0: the read-only telemetry tile is mounted alongside the panel.
expect(container.querySelector('[data-testid="compression-styles-tile"]')).toBeTruthy();
});
it("does not crash when fetch calls fail (fail-soft)", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network error"));
const { default: CompressionSettingsPage } =
await import("../../../src/app/(dashboard)/dashboard/context/settings/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionSettingsPage />);
});
await act(async () => {
await Promise.resolve();
});
// Component should still be mounted (not crashed)
expect(container).toBeTruthy();
expect(container.parentNode).toBeTruthy();
});
});
@@ -0,0 +1,144 @@
// @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";
// next-intl → echo the key so we can assert on stable identifiers.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href}>{children}</a>
),
}));
vi.mock("@/shared/components", () => ({
Card: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="card" className={className}>
{children}
</div>
),
Button: ({ children, onClick }: { children?: React.ReactNode; onClick?: () => void }) => (
<button onClick={onClick}>{children}</button>
),
}));
const CONFIG = {
enabled: true,
defaultMode: "standard",
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
comboOverrides: {},
cavemanConfig: {
enabled: true,
compressRoles: ["user"],
skipRules: [],
minMessageLength: 50,
preservePatterns: [],
intensity: "full",
},
cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
rtkConfig: { enabled: true, intensity: "standard" },
};
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot> | undefined;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
vi.stubGlobal(
"fetch",
vi.fn((url: string) => {
const u = String(url);
if (u.includes("/api/settings/compression")) {
return Promise.resolve({ ok: true, json: () => Promise.resolve(CONFIG) });
}
if (u.includes("/api/compression/rules")) {
return Promise.resolve({ ok: true, json: () => Promise.resolve({ rules: [] }) });
}
return Promise.resolve({ ok: false, json: () => Promise.resolve(null) });
})
);
});
afterEach(() => {
act(() => root?.unmount());
root = undefined;
container.remove();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
async function renderTab() {
const { default: CompressionSettingsTab } =
await import("@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab");
await act(async () => {
root = createRoot(container);
root.render(<CompressionSettingsTab />);
});
// Drain the on-mount fetch → json → setState microtask chain.
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
}
describe("CompressionSettingsTab — compression controls consolidation (T11)", () => {
it("renders the read-only TokenSaver summary that links to the unified panel", async () => {
await renderTab();
const hrefs = Array.from(container.querySelectorAll("a")).map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/dashboard/context/settings");
expect(container.textContent).toContain("tokenSaverTitle");
});
it("does not render a duplicate caveman engine on/off toggle (panel owns on/off)", async () => {
await renderTab();
const note = container.querySelector('[data-testid="caveman-panel-note"]');
expect(note).not.toBeNull();
// The note points users to the single-source panel...
expect(note?.textContent).toContain("/dashboard/context/settings");
// ...and the caveman header no longer carries its own enable toggle button.
expect(note?.querySelector("button")).toBeNull();
});
it("keeps the advanced caveman tuning the panel does not expose", async () => {
await renderTab();
expect(container.textContent).toContain("compressionRoleUser");
expect(container.textContent).toContain("compressionSkipRules");
expect(container.textContent).toContain("compressionPreservePatterns");
});
it("renders the preserveSystemPrompt 3-way mode select reflecting the shim (T05/C5)", async () => {
await renderTab();
const select = container.querySelector<HTMLSelectElement>(
'[data-testid="preserve-system-mode-select"]'
);
expect(select).not.toBeNull();
const values = Array.from(select!.querySelectorAll("option")).map((o) => o.value);
expect(values).toEqual(["always", "whenNoCache", "never"]);
// CONFIG has preserveSystemPrompt: true and no explicit mode → shim renders "always".
expect(select!.value).toBe("always");
});
it("saves the chosen mode via PUT (T05/C5)", async () => {
await renderTab();
const select = container.querySelector<HTMLSelectElement>(
'[data-testid="preserve-system-mode-select"]'
);
await act(async () => {
select!.value = "never";
select!.dispatchEvent(new Event("change", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
});
const putCall = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls.find(
([, init]) => (init as RequestInit | undefined)?.method === "PUT"
);
expect(putCall).toBeTruthy();
const body = JSON.parse(String((putCall![1] as RequestInit).body));
expect(body.preserveSystemPromptMode).toBe("never");
});
});
@@ -0,0 +1,67 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { CompressionAnnotation } from "@/app/(dashboard)/dashboard/compression/studio/CompressionAnnotation";
import type { CompressionStats } from "@omniroute/open-sse/services/compression/types";
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
function render(ui: React.ReactElement) {
act(() => {
createRoot(container).render(ui);
});
}
function makeStats(overrides: Partial<CompressionStats> = {}): CompressionStats {
return {
originalTokens: 847,
compressedTokens: 312,
savingsPercent: 63.16,
techniquesUsed: ["caveman"],
mode: "standard",
timestamp: 0,
...overrides,
};
}
describe("CompressionAnnotation", () => {
it("renders token range badge", () => {
const stats = makeStats({
rulesApplied: ["filler", "filler", "dedup"],
});
render(<CompressionAnnotation stats={stats} />);
expect(container.textContent).toContain("847→312");
});
it("renders rule pills", () => {
const stats = makeStats({
rulesApplied: ["filler", "filler", "dedup"],
});
render(<CompressionAnnotation stats={stats} />);
expect(container.textContent).toContain("filler×2");
expect(container.textContent).toContain("dedup×1");
});
it("returns null when no rules applied", () => {
const stats = makeStats({ rulesApplied: [], techniquesUsed: [] });
render(<CompressionAnnotation stats={stats} />);
expect(container.innerHTML).toBe("");
});
it("returns null when rulesApplied is absent", () => {
const stats = makeStats({ rulesApplied: undefined, techniquesUsed: [] });
render(<CompressionAnnotation stats={stats} />);
expect(container.innerHTML).toBe("");
});
});
+181
View File
@@ -0,0 +1,181 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
import type { CompressionRunModel } from "@/app/(dashboard)/dashboard/compression/studio/compressionFlowModel";
// ── Polyfill ResizeObserver (required by ReactFlow) ───────────────────────
beforeAll(() => {
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
});
// ── Mocks ─────────────────────────────────────────────────────────────────
// Stub @xyflow/react so ReactFlow renders without canvas/DOM measurement APIs
vi.mock("@xyflow/react", async () => {
const actual = (await vi.importActual("@xyflow/react")) as Record<string, unknown>;
return {
...actual,
Handle: (_props: Record<string, unknown>) => null,
Position: { Left: "left", Right: "right", Top: "top", Bottom: "bottom" },
};
});
// ── Import after mocks ─────────────────────────────────────────────────────
const { CompressionCockpit } =
await import("@/app/(dashboard)/dashboard/compression/studio/CompressionCockpit");
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
return container;
}
function click(el: Element | null): void {
act(() => {
el?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Sample run ────────────────────────────────────────────────────────────
const SAMPLE_RUN: CompressionRunModel = {
requestId: "test-req-001",
comboId: "daily-cascade",
mode: "stacked",
originalTokens: 12480,
compressedTokens: 6090,
savingsPercent: 51.2,
timestamp: 1718000000000,
steps: [
{
engine: "rtk",
originalTokens: 12480,
compressedTokens: 9734,
savingsPercent: 22.0,
techniquesUsed: ["strip-comments", "shell-filter"],
durationMs: 1.8,
},
{
engine: "headroom",
originalTokens: 9734,
compressedTokens: 8524,
savingsPercent: 12.4,
techniquesUsed: ["smartcrusher"],
durationMs: 0.9,
},
{
engine: "caveman",
originalTokens: 8524,
compressedTokens: 6896,
savingsPercent: 19.1,
techniquesUsed: ["pt-BR", "filler-drop"],
durationMs: 0.6,
},
],
};
// ── Tests ─────────────────────────────────────────────────────────────────
describe("CompressionCockpit", () => {
it("renders the cockpit wrapper when a run is provided", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='compression-cockpit']")).toBeTruthy();
});
it("renders the ReactFlow canvas", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.querySelector(".react-flow")).toBeTruthy();
});
it("shows the mode in the header", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.textContent).toContain("stacked");
});
it("shows the comboId in the header", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.textContent).toContain("daily-cascade");
});
it("shows the total savings percentage in the header", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.textContent).toContain("51.2");
});
it("shows engine names (from node labels via ReactFlow)", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
// The engine names appear in the header text even if ReactFlow node internals
// don't render in jsdom — we also check the header region.
const text = container.textContent ?? "";
// At minimum the run metadata renders correctly
expect(text).toContain("test-req-001");
});
it("renders the empty state when no run is given", () => {
const container = mount(<CompressionCockpit />);
expect(container.querySelector("[data-testid='compression-cockpit-empty']")).toBeTruthy();
expect(container.textContent).toContain("No compression run available");
});
it("renders replay controls when a run is provided", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
// Replay button present
const text = container.textContent ?? "";
expect(text).toContain("Replay");
});
it("offers a Canvas/Waterfall view toggle", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
expect(container.querySelector("[data-testid='cockpit-view-canvas']")).toBeTruthy();
expect(container.querySelector("[data-testid='cockpit-view-waterfall']")).toBeTruthy();
});
it("switches to the waterfall inspector and back to the canvas", () => {
const container = mount(<CompressionCockpit run={SAMPLE_RUN} />);
// Default view is the ReactFlow canvas, waterfall hidden.
expect(container.querySelector(".react-flow")).toBeTruthy();
expect(container.querySelector("[data-testid='waterfall-inspector']")).toBeFalsy();
// Switch to the waterfall (A1) view — the previously-orphan component is now reachable.
click(container.querySelector("[data-testid='cockpit-view-waterfall']"));
expect(container.querySelector("[data-testid='waterfall-inspector']")).toBeTruthy();
expect(container.querySelector(".react-flow")).toBeFalsy();
expect(
container.querySelector("[data-testid='waterfall-total-savings']")?.textContent
).toContain("51.2");
// Switch back to the canvas.
click(container.querySelector("[data-testid='cockpit-view-canvas']"));
expect(container.querySelector(".react-flow")).toBeTruthy();
expect(container.querySelector("[data-testid='waterfall-inspector']")).toBeFalsy();
});
});
+212
View File
@@ -0,0 +1,212 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
compressionEventToModel,
compressionRunToFlow,
buildReplayFrames,
type CompressionRunModel,
} from "../../../src/app/(dashboard)/dashboard/compression/studio/compressionFlowModel.ts";
import type { CompressionCompletedPayload } from "../../../src/lib/events/types.ts";
// ── fixture ───────────────────────────────────────────────────────────────
const THREE_ENGINE_PAYLOAD: CompressionCompletedPayload = {
requestId: "abc123",
comboId: "my-combo",
mode: "stacked",
originalTokens: 1000,
compressedTokens: 600,
savingsPercent: 40,
engineBreakdown: [
{
engine: "lite",
originalTokens: 1000,
compressedTokens: 900,
savingsPercent: 10,
techniquesUsed: ["whitespace"],
durationMs: 2,
},
{
engine: "caveman",
originalTokens: 900,
compressedTokens: 750,
savingsPercent: 16.7,
techniquesUsed: ["filler", "dedup"],
durationMs: 5,
},
{
engine: "rtk",
originalTokens: 750,
compressedTokens: 600,
savingsPercent: 20,
techniquesUsed: ["tool-output-trim"],
rulesApplied: ["max-lines"],
durationMs: 8,
},
],
timestamp: 1718000000000,
};
// ── compressionEventToModel ───────────────────────────────────────────────
describe("compressionEventToModel", () => {
it("builds the model from a 3-engine payload", () => {
const model: CompressionRunModel = compressionEventToModel(THREE_ENGINE_PAYLOAD);
assert.equal(model.requestId, "abc123");
assert.equal(model.comboId, "my-combo");
assert.equal(model.mode, "stacked");
assert.equal(model.originalTokens, 1000);
assert.equal(model.compressedTokens, 600);
assert.equal(model.savingsPercent, 40);
assert.equal(model.steps.length, 3);
});
it("maps each engine step correctly", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
assert.equal(model.steps[0].engine, "lite");
assert.equal(model.steps[0].originalTokens, 1000);
assert.equal(model.steps[0].compressedTokens, 900);
assert.equal(model.steps[0].savingsPercent, 10);
assert.deepEqual(model.steps[0].techniquesUsed, ["whitespace"]);
assert.equal(model.steps[0].durationMs, 2);
assert.equal(model.steps[2].engine, "rtk");
assert.deepEqual(model.steps[2].rulesApplied, ["max-lines"]);
});
it("handles null comboId gracefully", () => {
const payload = { ...THREE_ENGINE_PAYLOAD, comboId: null };
const model = compressionEventToModel(payload);
assert.equal(model.comboId, null);
});
it("handles empty engineBreakdown", () => {
const payload = { ...THREE_ENGINE_PAYLOAD, engineBreakdown: [] };
const model = compressionEventToModel(payload);
assert.equal(model.steps.length, 0);
});
});
// ── compressionRunToFlow ──────────────────────────────────────────────────
describe("compressionRunToFlow", () => {
it("returns N+2 nodes for N engine steps (input + N + output)", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const flow = compressionRunToFlow(model);
// 3 engine steps → 1 input + 3 engine + 1 output = 5 nodes
assert.equal(flow.nodes.length, 5, `expected 5 nodes, got ${flow.nodes.length}`);
});
it("node types: first=input, last=output, middle=engine", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const flow = compressionRunToFlow(model);
assert.equal(flow.nodes[0].type, "input");
assert.equal(flow.nodes[flow.nodes.length - 1].type, "output");
assert.equal(flow.nodes[1].type, "engine");
assert.equal(flow.nodes[2].type, "engine");
assert.equal(flow.nodes[3].type, "engine");
});
it("edges connect nodes in sequence (N+1 edges for N+2 nodes)", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const flow = compressionRunToFlow(model);
// 5 nodes → 4 sequential edges
assert.equal(flow.edges.length, 4, `expected 4 edges, got ${flow.edges.length}`);
});
it("each edge source/target connects consecutive nodes", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const flow = compressionRunToFlow(model);
for (let i = 0; i < flow.edges.length; i++) {
assert.equal(
flow.edges[i].source,
flow.nodes[i].id,
`edge ${i} source should be node[${i}].id`
);
assert.equal(
flow.edges[i].target,
flow.nodes[i + 1].id,
`edge ${i} target should be node[${i + 1}].id`
);
}
});
it("engine nodes carry per-step data", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const flow = compressionRunToFlow(model);
// nodes[1] is the first engine step (lite)
const liteNode = flow.nodes[1];
assert.equal((liteNode.data as Record<string, unknown>).engine, "lite");
assert.equal((liteNode.data as Record<string, unknown>).originalTokens, 1000);
assert.equal((liteNode.data as Record<string, unknown>).compressedTokens, 900);
assert.equal((liteNode.data as Record<string, unknown>).savingsPercent, 10);
});
it("handles zero-engine model (only input + output)", () => {
const payload = { ...THREE_ENGINE_PAYLOAD, engineBreakdown: [] };
const model = compressionEventToModel(payload);
const flow = compressionRunToFlow(model);
assert.equal(flow.nodes.length, 2);
assert.equal(flow.edges.length, 1);
assert.equal(flow.nodes[0].type, "input");
assert.equal(flow.nodes[1].type, "output");
});
});
// ── buildReplayFrames ─────────────────────────────────────────────────────
describe("buildReplayFrames", () => {
it("returns progressive snapshots (1 engine applied, 2 applied, ...)", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const frames = buildReplayFrames(model);
// 3 engines → 3 frames
assert.equal(frames.length, 3, `expected 3 frames, got ${frames.length}`);
});
it("frame[0] has 1 step, frame[1] has 2 steps, frame[2] has 3 steps", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const frames = buildReplayFrames(model);
assert.equal(frames[0].steps.length, 1);
assert.equal(frames[1].steps.length, 2);
assert.equal(frames[2].steps.length, 3);
});
it("frames are independent objects (mutations don't bleed)", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const frames = buildReplayFrames(model);
// Mutating frame[0].steps should not affect frame[1]
frames[0].steps.push({ engine: "fake" } as (typeof frames)[0]["steps"][0]);
assert.equal(frames[1].steps.length, 2);
});
it("each frame carries the correct compressedTokens up to that step", () => {
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
const frames = buildReplayFrames(model);
// Frame 0: after lite — compressedTokens = 900
assert.equal(frames[0].compressedTokens, 900);
// Frame 1: after caveman — compressedTokens = 750
assert.equal(frames[1].compressedTokens, 750);
// Frame 2: after rtk — compressedTokens = 600
assert.equal(frames[2].compressedTokens, 600);
});
it("returns empty array for model with no steps", () => {
const payload = { ...THREE_ENGINE_PAYLOAD, engineBreakdown: [] };
const model = compressionEventToModel(payload);
const frames = buildReplayFrames(model);
assert.equal(frames.length, 0);
});
});
@@ -0,0 +1,148 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) roots.pop()?.unmount();
});
for (let i = 0; i < 10; i++) await Promise.resolve();
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
});
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
interface CapturedPut {
url: string;
body: Record<string, unknown>;
}
function setupFetchMock(): { puts: CapturedPut[] } {
const puts: CapturedPut[] = [];
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
const initialConfig = {
enabled: true,
defaultMode: "off",
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
comboOverrides: {},
activeComboId: null,
contextEditing: { enabled: false },
};
const combos = [{ id: "c1", name: "RTK only", pipeline: [{ engine: "rtk" }] }];
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const method = (init?.method ?? "GET").toUpperCase();
if (url.includes("/api/context/combos/default")) return json({}, 404);
if (url.includes("/api/context/combos")) return json({ combos });
if (url.includes("/api/compression/engines")) return json({ engines: [] });
if (url.includes("/api/settings/compression")) {
if (method === "PUT") {
const body = JSON.parse(String(init?.body ?? "{}"));
puts.push({ url, body });
return json({ ...initialConfig, ...body });
}
return json(initialConfig);
}
return json({}, 404);
}
);
return { puts };
}
function setSelectValue(select: HTMLSelectElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, "value")!.set!;
setter.call(select, value);
select.dispatchEvent(new Event("change", { bubbles: true }));
}
describe("CompressionHub — active-profile selector", () => {
async function render() {
const { default: CompressionHub } = await import(
"../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionHub />);
});
await flush();
return container;
}
it("renders the active-profile select with Default + each named combo", async () => {
setupFetchMock();
const container = await render();
const select = container.querySelector('[data-testid="active-profile-select"]') as HTMLSelectElement | null;
expect(select).toBeTruthy();
expect(container.textContent).toContain("Default (from panel)");
expect(container.textContent).toContain("RTK only");
});
it("changing the select to a combo PUTs activeComboId === that id", async () => {
const { puts } = setupFetchMock();
const container = await render();
const select = container.querySelector('[data-testid="active-profile-select"]') as HTMLSelectElement;
await act(async () => {
setSelectValue(select, "c1");
});
await flush();
const settingsPuts = puts.filter((p) => p.url.includes("/api/settings/compression"));
expect(settingsPuts.length).toBeGreaterThan(0);
expect(settingsPuts.pop()!.body.activeComboId).toBe("c1");
});
it("preview shows the Default fallback initially, and the combo engines once a combo is active", async () => {
setupFetchMock();
const container = await render();
const preview = () => container.querySelector('[data-testid="active-profile-preview"]');
expect(preview()).toBeTruthy();
expect(preview()!.textContent).toContain("Default");
const select = container.querySelector('[data-testid="active-profile-select"]') as HTMLSelectElement;
await act(async () => {
setSelectValue(select, "c1");
});
await flush();
expect(preview()!.textContent).toContain("rtk");
});
it("no longer renders the master Token Saver toggle, the mode selector, or reorder buttons", async () => {
setupFetchMock();
const container = await render();
expect(container.querySelector('[aria-label="Toggle Token Saver"]')).toBeNull();
expect(container.querySelector('[aria-label="Move up"]')).toBeNull();
expect(container.querySelector('[aria-label="Move down"]')).toBeNull();
// The Aggressive mode button's hint text is gone with the mode selector.
expect(container.textContent).not.toContain("Summary plus aging");
});
});
@@ -0,0 +1,193 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mountInContainer(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Mock fetch ────────────────────────────────────────────────────────────
const ENGINES = [{ id: "rtk", name: "RTK", stackPriority: 10, stable: true }];
function enginePayload() {
return {
engines: ENGINES.map((e) => ({
id: e.id,
name: e.name,
description: `${e.name} description`,
icon: "compress",
stackable: true,
stackPriority: e.stackPriority,
metadata: { stable: e.stable },
configSchema: [],
})),
};
}
interface FetchCall {
url: string;
method: string;
body: unknown;
}
function setupFetchMock(opts: { contextEditingEnabled?: boolean }): FetchCall[] {
const { contextEditingEnabled = false } = opts;
const calls: FetchCall[] = [];
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const method = (init?.method ?? "GET").toUpperCase();
let parsedBody: unknown;
if (typeof init?.body === "string") {
try {
parsedBody = JSON.parse(init.body);
} catch {
parsedBody = init.body;
}
}
calls.push({ url, method, body: parsedBody });
if (url.includes("/api/settings/compression")) {
return json({
enabled: true,
defaultMode: "stacked",
contextEditing: { enabled: contextEditingEnabled },
});
}
if (url.includes("/api/compression/engines")) {
return json(enginePayload());
}
if (url.includes("/api/context/combos/default")) {
return json({ id: "default-caveman", name: "Standard Savings", pipeline: [] });
}
if (url.includes("/api/context/combos")) {
return json({ combos: [] });
}
if (url.includes("/api/combos")) {
return json({ combos: [] });
}
if (url.includes("/api/compression/language-packs")) {
return json({ packs: [] });
}
return json({}, 404);
}
);
return calls;
}
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe("CompressionHub — Context Editing", () => {
it("renders the delegated-compression section with the Context Editing toggle", async () => {
setupFetchMock({ contextEditingEnabled: false });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("Compressão delegada ao provedor");
expect(text).toContain("Context Editing (Claude)");
});
it("renders the Claude-only delegated note", async () => {
setupFetchMock({ contextEditingEnabled: false });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("apenas para Claude");
expect(text).toContain("não reescrevemos a mensagem");
});
it("PUTs contextEditing: { enabled: true } when the toggle is flipped on", async () => {
const calls = setupFetchMock({ contextEditingEnabled: false });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const toggle = container.querySelector(
'button[role="switch"][aria-label="Context Editing"]'
) as HTMLButtonElement | null;
expect(toggle).not.toBeNull();
expect(toggle?.getAttribute("aria-checked")).toBe("false");
await act(async () => {
toggle?.click();
});
await flush();
const put = calls.find(
(c) => c.method === "PUT" && c.url.includes("/api/settings/compression")
);
expect(put).toBeTruthy();
expect((put?.body as { contextEditing?: { enabled?: boolean } })?.contextEditing).toEqual({
enabled: true,
});
});
});
+223
View File
@@ -0,0 +1,223 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mountInContainer(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Mock fetch ────────────────────────────────────────────────────────────
const ENGINES = [
{ id: "session-dedup", name: "Session Dedup", stackPriority: 3, stable: true },
{ id: "rtk", name: "RTK", stackPriority: 10, stable: true },
{ id: "caveman", name: "Caveman", stackPriority: 20, stable: true },
{ id: "llmlingua", name: "LLMLingua-2", stackPriority: 35, stable: false },
];
function enginePayload() {
return {
engines: ENGINES.map((e) => ({
id: e.id,
name: e.name,
description: `${e.name} description`,
icon: "compress",
stackable: true,
stackPriority: e.stackPriority,
metadata: { stable: e.stable },
configSchema: [],
})),
};
}
function setupFetchMock(opts: {
enabled?: boolean;
mode?: string;
pipeline?: Array<{ engine: string }>;
}) {
const { enabled = true, mode = "stacked", pipeline = [{ engine: "rtk" }] } = opts;
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/api/settings/compression")) {
return json({ enabled, defaultMode: mode });
}
if (url.includes("/api/compression/engines")) {
return json(enginePayload());
}
if (url.includes("/api/context/combos/default")) {
return json({ id: "default-caveman", name: "Standard Savings", pipeline });
}
if (url.includes("/api/context/combos")) {
return json({ combos: [] });
}
if (url.includes("/api/combos")) {
return json({ combos: [] });
}
if (url.includes("/api/compression/language-packs")) {
return json({ packs: [] });
}
return json({}, 404);
});
}
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe("CompressionHub", () => {
it("renders the master switch, mode selector, and the layered pipeline", async () => {
setupFetchMock({ enabled: true, mode: "stacked", pipeline: [{ engine: "rtk" }] });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("Compression Hub");
expect(text).toContain("Token Saver");
expect(text).toContain("Stacked");
// Active pipeline engine (from the default combo) renders
expect(text).toContain("RTK");
// Inactive engines from the catalog render too
expect(text).toContain("Caveman");
// Active-pipeline callout shows when enabled && stacked
expect(text).toContain("Layer pipeline is active");
});
it("INVARIANT #1: no per-layer control issues a PUT/POST to /api/context/combos/default", { timeout: 20000 }, async () => {
const comboWrites: { method: string }[] = [];
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url.includes("/api/context/combos/default")) {
if (init?.method === "PUT" || init?.method === "POST") {
comboWrites.push({ method: init.method });
}
return json({ id: "default", name: "Default", pipeline: [{ engine: "rtk" }] });
}
if (url.includes("/api/settings/compression")) {
return json({ enabled: true, defaultMode: "stacked" });
}
if (url.includes("/api/compression/engines")) {
return json(enginePayload());
}
if (url.includes("/api/context/combos") || url.includes("/api/combos")) {
return json({ combos: [] });
}
if (url.includes("/api/compression/language-packs")) {
return json({ packs: [] });
}
return json({}, 404);
}
);
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
// Click every on/off switch in the Hub (master + any layer controls that remain).
const switches = Array.from(container.querySelectorAll('[role="switch"]'));
for (const sw of switches) {
await act(async () => {
(sw as HTMLElement).click();
});
await flush();
}
expect(comboWrites).toHaveLength(0);
});
it("shows the activation warning when Token Saver is off", async () => {
setupFetchMock({ enabled: false, mode: "off", pipeline: [] });
const { default: CompressionHub } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionHub />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("Enable Token Saver");
expect(text).toContain("only run in Stacked mode");
});
});
describe("CompressionCombosPageClient", () => {
it("renders the Hub on top and the named-combos manager below", async () => {
setupFetchMock({ enabled: true, mode: "stacked", pipeline: [{ engine: "rtk" }] });
const { default: CompressionCombosPageClient } =
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CompressionCombosPageClient />);
});
await flush();
const text = container.textContent ?? "";
expect(text).toContain("Compression Hub");
expect(text).toContain("Named combos");
});
});
+221
View File
@@ -0,0 +1,221 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
ENGINE_IDS,
engineMeta,
} from "../../../open-sse/services/compression/engineCatalog.ts";
// i18n does not resolve to a real locale in vitest/jsdom, so mock next-intl to echo
// the key. This test therefore asserts ONLY on i18n-independent strings: catalog
// labels/descriptions, engine ids, data-testid hooks, and the PUT request body.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
// ── Harness ─────────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
// ── Fetch stub ────────────────────────────────────────────────────────────────
interface CapturedPut {
url: string;
body: Record<string, unknown>;
}
function setupFetchMock(): { puts: CapturedPut[] } {
const puts: CapturedPut[] = [];
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
const initialConfig = {
enabled: true,
defaultMode: "stacked",
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
comboOverrides: {},
engines: {
rtk: { enabled: true, level: "standard" },
caveman: { enabled: false },
},
activeComboId: null,
cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
};
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const method = (init?.method ?? "GET").toUpperCase();
if (url.includes("/api/settings/compression/mcp-accessibility")) {
if (method === "PUT") {
puts.push({ url, body: JSON.parse(String(init?.body ?? "{}")) });
return json({ enabled: true });
}
return json({ enabled: true, maxTextChars: 50000 });
}
if (url.includes("/api/settings/compression")) {
if (method === "PUT") {
const body = JSON.parse(String(init?.body ?? "{}"));
puts.push({ url, body });
// Echo a merged config so the panel keeps a coherent state.
return json({ ...initialConfig, ...body });
}
return json(initialConfig);
}
return json({}, 404);
}
);
return { puts };
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("CompressionPanel", () => {
it("renders a row for every engine id in the catalog", async () => {
setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
for (const id of ENGINE_IDS) {
const row = container.querySelector(`[data-testid="engine-row-${id}"]`);
expect(row, `expected a row for engine "${id}"`).toBeTruthy();
// Catalog label/description are hardcoded English (i18n-independent).
expect(container.textContent).toContain(engineMeta(id).label);
}
});
it("shows the rtk level 'standard' as selected", async () => {
setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
const select = container.querySelector(
`[data-testid="engine-row-rtk"] select`
) as HTMLSelectElement | null;
expect(select).toBeTruthy();
expect(select?.value).toBe("standard");
});
it("toggling caveman PUTs engines.caveman.enabled === true", async () => {
const { puts } = setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
// The data-testid hook wraps the Toggle; its inner <button role="switch"> is the
// clickable element.
const toggle = container.querySelector(
`[data-testid="engine-toggle-caveman"] button`
) as HTMLButtonElement | null;
expect(toggle, "caveman toggle must exist").toBeTruthy();
await act(async () => {
toggle!.click();
});
await flush();
const settingsPuts = puts.filter(
(p) => p.url.includes("/api/settings/compression") && !p.url.includes("mcp-accessibility")
);
expect(settingsPuts.length).toBeGreaterThan(0);
const lastEngines = settingsPuts
.map((p) => p.body.engines as Record<string, { enabled: boolean }> | undefined)
.filter(Boolean)
.pop();
expect(lastEngines).toBeTruthy();
expect(lastEngines!.caveman.enabled).toBe(true);
// Full engines map is sent (whole-row persistence), so rtk is not dropped.
expect(lastEngines!.rtk.enabled).toBe(true);
});
it("derived-pipeline preview reflects the enabled engines", async () => {
setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
const preview = container.querySelector(`[data-testid="derived-pipeline-preview"]`);
expect(preview).toBeTruthy();
// Only rtk is enabled in the initial config → preview mentions rtk, not caveman.
expect(preview?.textContent).toContain("rtk");
expect(preview?.textContent).not.toContain("caveman");
});
});
@@ -0,0 +1,158 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import {
OUTPUT_STYLE_IDS,
outputStyleMeta,
} from "../../../open-sse/services/compression/outputStyles/catalog.ts";
// Locale is mutable per-test so we can exercise the locale gate (terse-cjk → zh only).
const intl = vi.hoisted(() => ({ locale: "en" }));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => intl.locale,
}));
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => root.render(ui));
return container;
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
intl.locale = "en";
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) roots.pop()?.unmount();
});
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
});
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
function setupFetchMock() {
const puts: Array<{ url: string; body: Record<string, unknown> }> = [];
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
const initial = {
enabled: true,
autoTriggerTokens: 0,
preserveSystemPrompt: true,
engines: {},
activeComboId: null,
outputStyles: [],
cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
};
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const method = (init?.method ?? "GET").toUpperCase();
if (url.includes("/api/settings/compression/mcp-accessibility")) return json({ enabled: true });
if (url.includes("/api/settings/compression")) {
if (method === "PUT") {
const body = JSON.parse(String(init?.body ?? "{}"));
puts.push({ url, body });
return json({ ...initial, ...body });
}
return json(initial);
}
return json({}, 404);
}
);
return { puts };
}
describe("CompressionPanel output styles", () => {
it("renders one row per catalog style", async () => {
setupFetchMock();
intl.locale = "zh-CN"; // a locale that matches every gated style, so all rows render
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
for (const id of OUTPUT_STYLE_IDS) {
const row = container.querySelector(`[data-testid="output-style-row-${id}"]`);
expect(row, `expected a row for style "${id}"`).toBeTruthy();
expect(container.textContent).toContain(outputStyleMeta(id).label);
}
});
it("locale-gates terse-cjk: hidden under a non-zh locale", async () => {
setupFetchMock();
intl.locale = "en";
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
// terse-cjk (locale "zh") must NOT be offered under "en"…
expect(container.querySelector(`[data-testid="output-style-row-terse-cjk"]`)).toBeFalsy();
// …while the non-gated styles still render.
expect(container.querySelector(`[data-testid="output-style-row-terse-prose"]`)).toBeTruthy();
expect(container.querySelector(`[data-testid="output-style-row-less-code"]`)).toBeTruthy();
});
it("locale-gates terse-cjk: offered under a zh locale (zh-CN base matches)", async () => {
setupFetchMock();
intl.locale = "zh-CN";
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
expect(container.querySelector(`[data-testid="output-style-row-terse-cjk"]`)).toBeTruthy();
});
it("toggling a style PUTs an outputStyles selection", async () => {
const { puts } = setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
const toggle = container.querySelector(
`[data-testid="output-style-toggle-terse-prose"] button, [data-testid="output-style-toggle-terse-prose"] input`
) as HTMLElement | null;
expect(toggle).toBeTruthy();
await act(async () => {
toggle!.click();
});
await flush();
const put = puts.find((p) => "outputStyles" in p.body);
expect(put, "a PUT carrying outputStyles").toBeTruthy();
expect(
(put!.body.outputStyles as Array<{ id: string }>).some((s) => s.id === "terse-prose")
).toBe(true);
});
});
@@ -0,0 +1,68 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => root.render(ui));
return container;
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) roots.pop()?.unmount();
});
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
});
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
describe("CompressionStylesTile", () => {
it("renders total savings and applied style ids from the summary endpoint", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(
JSON.stringify({
totalRuns: 4,
totalTokensSaved: 1234,
runsWithStyles: 3,
bypassCount: 1,
totalOutputTokens: 900,
appliedStyleCounts: { "terse-prose": 3, "less-code": 1 },
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
)
);
const { default: CompressionStylesTile } = await import(
"../../../src/app/(dashboard)/dashboard/context/CompressionStylesTile"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionStylesTile />);
});
await flush();
expect(container.textContent).toContain("1234");
expect(container.textContent).toContain("terse-prose");
expect(container.textContent).toContain("less-code");
});
});
+165
View File
@@ -0,0 +1,165 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// i18n does not resolve to a real locale in vitest/jsdom, so mock next-intl to echo
// the key. This test asserts ONLY on i18n-independent hooks (data-testid + values)
// and the captured PUT body.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => root.render(ui));
return container;
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) roots.pop()?.unmount();
});
for (let i = 0; i < 10; i++) await Promise.resolve();
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
});
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
interface CapturedPut {
url: string;
body: Record<string, unknown>;
}
function setupFetchMock(overrides?: Record<string, unknown>): { puts: CapturedPut[] } {
const puts: CapturedPut[] = [];
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
const initial = {
enabled: true,
autoTriggerTokens: 0,
preserveSystemPrompt: true,
engines: {},
activeComboId: null,
outputStyles: [],
cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
ultraEngine: "heuristic",
ultraSlmPrewarm: false,
...overrides,
};
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
const method = (init?.method ?? "GET").toUpperCase();
if (url.includes("/api/settings/compression/mcp-accessibility")) return json({ enabled: true });
if (url.includes("/api/settings/compression")) {
if (method === "PUT") {
const body = JSON.parse(String(init?.body ?? "{}"));
puts.push({ url, body });
return json({ ...initial, ...body });
}
return json(initial);
}
return json({}, 404);
}
);
return { puts };
}
describe("CompressionPanel ultra SLM tier", () => {
it("renders the ultra-engine select defaulting to heuristic", async () => {
setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
const select = container.querySelector(
`[data-testid="ultra-engine-select"]`
) as HTMLSelectElement | null;
expect(select, "ultra-engine select must render").toBeTruthy();
expect(select?.value).toBe("heuristic");
// The pre-warm toggle is hidden while heuristic is selected.
expect(
container.querySelector(`[data-testid="ultra-slm-prewarm-toggle"]`)
).toBeFalsy();
});
it("selecting SLM PUTs ultraEngine:'slm' and reveals the pre-warm toggle", async () => {
const { puts } = setupFetchMock();
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
const select = container.querySelector(
`[data-testid="ultra-engine-select"]`
) as HTMLSelectElement;
expect(select).toBeTruthy();
await act(async () => {
select.value = "slm";
select.dispatchEvent(new Event("change", { bubbles: true }));
});
await flush();
const put = puts.find((p) => "ultraEngine" in p.body);
expect(put, "a PUT carrying ultraEngine").toBeTruthy();
expect(put!.body.ultraEngine).toBe("slm");
// Pre-warm toggle now visible.
const toggle = container.querySelector(`[data-testid="ultra-slm-prewarm-toggle"]`);
expect(toggle, "pre-warm toggle must appear when slm is selected").toBeTruthy();
});
it("toggling pre-warm PUTs ultraSlmPrewarm:true when SLM is active", async () => {
const { puts } = setupFetchMock({ ultraEngine: "slm", ultraSlmPrewarm: false });
const { default: CompressionPanel } = await import(
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionPanel />);
});
await flush();
const toggle = container.querySelector(
`[data-testid="ultra-slm-prewarm-toggle"] button, [data-testid="ultra-slm-prewarm-toggle"] input`
) as HTMLElement | null;
expect(toggle, "pre-warm toggle must exist when slm preselected").toBeTruthy();
await act(async () => {
toggle!.click();
});
await flush();
const put = puts.find((p) => "ultraSlmPrewarm" in p.body);
expect(put, "a PUT carrying ultraSlmPrewarm").toBeTruthy();
expect(put!.body.ultraSlmPrewarm).toBe(true);
});
});
@@ -0,0 +1,91 @@
// @vitest-environment jsdom
/**
* Regression guard for #5836 — the red "Token Expired" connection badge must
* NOT flash for OAuth refresh-capable providers (Antigravity/Gemini) whose
* access token merely lapsed but is auto-refreshed. It should render ONLY when
* the connection is terminally expired (testStatus === "expired").
* Continuation of #5326.
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
import ConnectionRow, {
type ConnectionRowConnection,
} from "@/app/(dashboard)/dashboard/providers/[id]/components/ConnectionRow";
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
const baseProps = {
isOAuth: true,
isFirst: true,
isLast: true,
onMoveUp: () => {},
onMoveDown: () => {},
onToggleActive: () => {},
onToggleRateLimit: () => {},
onRetest: () => {},
onEdit: () => {},
onDelete: () => {},
};
function renderRow(connection: ConnectionRowConnection): HTMLElement {
const container = makeContainer();
const root = createRoot(container);
cleanupCallbacks.push(() => act(() => root.unmount()));
act(() => {
root.render(React.createElement(ConnectionRow, { ...baseProps, connection } as never));
});
return container;
}
const PAST = new Date(Date.now() - 60 * 60 * 1000).toISOString(); // 1h ago
describe("ConnectionRow token expiry badge (#5836)", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length) cleanupCallbacks.pop()!();
});
it("does NOT render the red Token Expired badge for a healthy OAuth connection whose access token merely lapsed", () => {
const container = renderRow({
id: "c1",
provider: "antigravity",
testStatus: "active",
isActive: true,
tokenExpiresAt: PAST,
priority: 1,
} as ConnectionRowConnection);
expect(container.textContent).not.toContain("tokenExpiredBadge");
});
it("renders the red Token Expired badge when the connection is terminally expired", () => {
const container = renderRow({
id: "c2",
provider: "antigravity",
testStatus: "expired",
isActive: true,
tokenExpiresAt: PAST,
errorCode: "no_refresh_token",
priority: 1,
} as ConnectionRowConnection);
expect(container.textContent).toContain("tokenExpiredBadge");
});
});
@@ -0,0 +1,151 @@
/**
* Tests for ConversationTab separators — CONTEXT HISTORY / MODEL RESPONSE
* Validates the rendering logic: both sections appear when both request/response have turns;
* only CONTEXT HISTORY appears when response is empty.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts";
import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts";
function makeRequest(overrides: Partial<InterceptedRequest> = {}): InterceptedRequest {
return {
id: "test-id",
source: "agent-bridge",
timestamp: new Date().toISOString(),
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
requestHeaders: { "content-type": "application/json" },
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 200,
detectedKind: "llm",
...overrides,
};
}
describe("ConversationTab separators rendering logic", () => {
it("shows CONTEXT HISTORY section when request has turns", () => {
const reqBody = JSON.stringify({
model: "gpt-4o",
messages: [
{ role: "system", content: "You are helpful." },
{ role: "user", content: "Hello!" },
],
});
const req = makeRequest({ requestBody: reqBody, responseBody: null });
const result = normalizeConversation(req);
if (result !== null) {
assert.ok(result.request.length > 0, "request section should have turns");
// Context History separator should be rendered (guarded by request.length > 0)
const shouldRenderContextHistory = result.request.length > 0;
assert.equal(shouldRenderContextHistory, true);
}
});
it("shows MODEL RESPONSE section when response has turns", () => {
const reqBody = JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "Hi" }],
});
const resBody = JSON.stringify({
choices: [
{
message: { role: "assistant", content: "Hello! How can I help?" },
finish_reason: "stop",
},
],
usage: { prompt_tokens: 5, completion_tokens: 8 },
});
const req = makeRequest({ requestBody: reqBody, responseBody: resBody });
const result = normalizeConversation(req);
if (result !== null) {
// Model Response separator should be rendered (guarded by response.length > 0)
const shouldRenderModelResponse = result.response.length > 0;
assert.equal(shouldRenderModelResponse, true);
}
});
it("does NOT render MODEL RESPONSE when response is empty", () => {
const reqBody = JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "Hi" }],
});
// No response body
const req = makeRequest({ requestBody: reqBody, responseBody: null });
const result = normalizeConversation(req);
if (result !== null) {
// Response section should be empty, so MODEL RESPONSE separator should NOT render
const shouldRenderModelResponse = result.response.length > 0;
assert.equal(shouldRenderModelResponse, false);
// But context history should still render
const shouldRenderContextHistory = result.request.length > 0;
assert.equal(shouldRenderContextHistory, true);
}
});
it("renders both separators when both request and response have turns", () => {
const reqBody = JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "Hi" }],
});
const resBody = JSON.stringify({
choices: [
{
message: { role: "assistant", content: "Hello!" },
finish_reason: "stop",
},
],
});
const req = makeRequest({ requestBody: reqBody, responseBody: resBody });
const result = normalizeConversation(req);
if (result !== null) {
const contextHistoryVisible = result.request.length > 0;
const modelResponseVisible = result.response.length > 0;
assert.equal(contextHistoryVisible, true, "CONTEXT HISTORY should be visible");
assert.equal(modelResponseVisible, true, "MODEL RESPONSE should be visible");
}
});
it("request and response turns are keyed separately (req-N vs res-N)", () => {
// Keys used: `req-${i}` for request turns, `res-${i}` for response turns
const reqKeys = ["req-0", "req-1", "req-2"];
const resKeys = ["res-0", "res-1"];
// Verify no overlap
const allKeys = [...reqKeys, ...resKeys];
const uniqueKeys = new Set(allKeys);
assert.equal(uniqueKeys.size, allKeys.length, "all keys should be unique");
});
it("allTurns still accounts for correct total across both sections", () => {
const request = [
{ role: "user" as const, content: "Hi", contentType: "text" as const },
];
const response = [
{ role: "assistant" as const, content: "Hello!", contentType: "text" as const },
];
// Before: allTurns = [...request, ...response]
// After: both rendered in separate sections
const totalTurns = request.length + response.length;
assert.equal(totalTurns, 2);
});
it("conversationNotAvailable key resolves when body is null (normalizeConversation returns null)", () => {
// When requestBody is null and responseBody is null, normalizeConversation returns null.
// The ConversationTab renders t("conversationNotAvailable") in that case.
const req = makeRequest({ requestBody: null, responseBody: null });
const result = normalizeConversation(req);
// Must return null so the component falls through to the conversationNotAvailable branch.
assert.equal(result, null, "normalizeConversation must return null for non-LLM / null body");
});
});
+125
View File
@@ -0,0 +1,125 @@
/**
* Tests for ConversationTab — normalizeConversation + chat bubble rendering logic
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { normalizeConversation } from "../../../src/mitm/inspector/conversationNormalizer.ts";
import type { InterceptedRequest } from "../../../src/mitm/inspector/types.ts";
function makeRequest(overrides: Partial<InterceptedRequest> = {}): InterceptedRequest {
return {
id: "test-id",
source: "agent-bridge",
timestamp: new Date().toISOString(),
method: "POST",
host: "api.openai.com",
path: "/v1/chat/completions",
requestHeaders: { "content-type": "application/json" },
requestBody: null,
requestSize: 0,
responseHeaders: {},
responseBody: null,
responseSize: 0,
status: 200,
detectedKind: "llm",
...overrides,
};
}
describe("ConversationTab normalizeConversation", () => {
it("returns null for non-LLM request", () => {
const req = makeRequest({ detectedKind: "app", requestBody: null });
const result = normalizeConversation(req);
assert.equal(result, null);
});
it("returns null for request without body", () => {
const req = makeRequest({ requestBody: null, responseBody: null });
const result = normalizeConversation(req);
assert.equal(result, null);
});
it("normalizes OpenAI chat request with user message", () => {
const body = JSON.stringify({
model: "gpt-4o",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello!" },
],
});
const req = makeRequest({ requestBody: body, responseBody: null });
const result = normalizeConversation(req);
// Should not return null for a valid LLM request
if (result !== null) {
assert.ok(Array.isArray(result.request), "request should be an array");
assert.ok(result.request.length >= 1, "should have at least 1 turn");
const roles = result.request.map((t) => t.role);
assert.ok(roles.includes("user") || roles.includes("system"), "should have user or system role");
}
});
it("normalizes OpenAI response with assistant message", () => {
const reqBody = JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "Hi" }],
});
const resBody = JSON.stringify({
choices: [
{
message: {
role: "assistant",
content: "Hello! How can I help?",
},
finish_reason: "stop",
},
],
usage: { prompt_tokens: 10, completion_tokens: 8 },
});
const req = makeRequest({ requestBody: reqBody, responseBody: resBody });
const result = normalizeConversation(req);
if (result !== null) {
// Response turns should include assistant
const responseTurns = result.response;
assert.ok(Array.isArray(responseTurns));
}
});
it("returns NormalizedConversation shape with request/response/contextKey", () => {
const body = JSON.stringify({
model: "gpt-4o",
messages: [{ role: "user", content: "test" }],
});
const req = makeRequest({ requestBody: body });
const result = normalizeConversation(req);
if (result !== null) {
assert.ok("request" in result, "should have request field");
assert.ok("response" in result, "should have response field");
assert.ok("contextKey" in result, "should have contextKey field");
}
});
});
describe("ChatBubble role mapping", () => {
it("maps expected roles", () => {
const validRoles = ["system", "user", "assistant", "tool"] as const;
const roleLabels: Record<(typeof validRoles)[number], string> = {
system: "System",
user: "User",
assistant: "Assistant",
tool: "Tool",
};
for (const role of validRoles) {
assert.ok(roleLabels[role], `Role ${role} should have a label`);
}
});
it("system role is collapsed by default", () => {
// System messages start collapsed per UX spec
const defaultCollapsed = true;
assert.equal(defaultCollapsed, true);
});
});
+132
View File
@@ -0,0 +1,132 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { DASHBOARD_CSRF_HEADER } from "@/shared/constants/dashboardCsrf";
import {
__resetDashboardCsrfTokenForTests,
installDashboardCsrfFetch,
prefetchDashboardCsrfToken,
} from "@/shared/utils/dashboardCsrf";
let fetchMock: ReturnType<typeof vi.fn>;
let uninstall: (() => void) | null = null;
function csrfResponse(): Response {
return new Response(
JSON.stringify({
token: "csrf-token",
expiresAt: new Date(Date.now() + 60_000).toISOString(),
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
beforeEach(() => {
__resetDashboardCsrfTokenForTests();
fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
uninstall = installDashboardCsrfFetch();
});
afterEach(() => {
uninstall?.();
uninstall = null;
__resetDashboardCsrfTokenForTests();
vi.unstubAllGlobals();
});
describe("installDashboardCsrfFetch", () => {
it("adds dashboard CSRF to same-origin unsafe requests", async () => {
fetchMock.mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(new Response("{}"));
await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: "{}",
});
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock).toHaveBeenNthCalledWith(
1,
"/api/auth/csrf",
expect.objectContaining({ cache: "no-store", credentials: "same-origin" })
);
expect(fetchMock).toHaveBeenNthCalledWith(
2,
"/api/settings",
expect.objectContaining({ method: "PATCH" })
);
const headers = fetchMock.mock.calls[1][1]?.headers as Headers;
expect(headers.get(DASHBOARD_CSRF_HEADER)).toBe("csrf-token");
expect(headers.get("Content-Type")).toBe("application/json");
});
it("adds dashboard CSRF to explicit top-level management routes", async () => {
fetchMock.mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(new Response("{}"));
await fetch("/a2a", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0][0]).toBe("/api/auth/csrf");
expect(fetchMock.mock.calls[1][0]).toBe("/a2a");
const headers = fetchMock.mock.calls[1][1]?.headers as Headers;
expect(headers.get(DASHBOARD_CSRF_HEADER)).toBe("csrf-token");
});
it("reuses a prefetched dashboard CSRF token for later unsafe requests", async () => {
fetchMock.mockResolvedValueOnce(csrfResponse()).mockResolvedValueOnce(new Response("{}"));
await prefetchDashboardCsrfToken();
await fetch("/api/settings", { method: "PATCH", body: "{}" });
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(fetchMock.mock.calls[0][0]).toBe("/api/auth/csrf");
expect(fetchMock.mock.calls[1][0]).toBe("/api/settings");
const headers = fetchMock.mock.calls[1][1]?.headers as Headers;
expect(headers.get(DASHBOARD_CSRF_HEADER)).toBe("csrf-token");
});
it("does not modify safe, cross-origin, or already-protected requests", async () => {
fetchMock.mockResolvedValue(new Response("{}"));
await fetch("/api/settings");
await fetch("https://api.example.test/api/settings", { method: "PATCH" });
await fetch("/api/settings", {
method: "PATCH",
headers: { [DASHBOARD_CSRF_HEADER]: "existing-token" },
});
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([
"/api/settings",
"https://api.example.test/api/settings",
"/api/settings",
]);
});
it("does not attach dashboard CSRF to client API aliases or public API routes", async () => {
fetchMock.mockResolvedValue(new Response("{}"));
await fetch("/api/v1/rerank", { method: "POST" });
await fetch("/v1/chat/completions", { method: "POST" });
await fetch("/v1beta/models", { method: "POST" });
await fetch("/responses", { method: "POST" });
await fetch("/codex", { method: "POST" });
await fetch("/api/auth/logout", { method: "POST" });
expect(fetchMock).toHaveBeenCalledTimes(6);
expect(fetchMock.mock.calls.map(([url]) => String(url))).toEqual([
"/api/v1/rerank",
"/v1/chat/completions",
"/v1beta/models",
"/responses",
"/codex",
"/api/auth/logout",
]);
});
});
+15
View File
@@ -0,0 +1,15 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { act } from "react";
import { DiffPane } from "@/app/(dashboard)/dashboard/compression/studio/DiffPane";
let container: HTMLElement; let root: Root;
beforeEach(() => { (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); });
afterEach(() => { act(() => root.unmount()); container.remove(); document.body.innerHTML = ""; });
describe("DiffPane", () => {
it("renders removed and same segments with distinct testids", () => {
act(() => { root.render(<DiffPane segments={[{ type: "same", text: "keep this " }, { type: "removed", text: "drop this" }]} preservedBlocks={[]} />); });
expect(container.querySelector('[data-testid="diff-removed"]')?.textContent).toContain("drop this");
expect(container.querySelector('[data-testid="diff-same"]')?.textContent).toContain("keep this");
});
});
+50
View File
@@ -0,0 +1,50 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { edgeStyle, FLOW_EDGE_COLORS } from "../../../src/shared/components/flow/edgeStyles.ts";
describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => {
it("exposes the shared flow palette", () => {
assert.equal(FLOW_EDGE_COLORS.active, "#22c55e");
assert.equal(FLOW_EDGE_COLORS.error, "#ef4444");
assert.equal(FLOW_EDGE_COLORS.last, "#f59e0b");
assert.equal(FLOW_EDGE_COLORS.idle, "var(--color-border)");
});
it("styles an error edge", () => {
assert.deepEqual(edgeStyle(false, false, true), {
stroke: "#ef4444",
strokeWidth: 2,
opacity: 0.85,
});
});
it("styles an active edge", () => {
assert.deepEqual(edgeStyle(true, false, false), {
stroke: "#22c55e",
strokeWidth: 2.5,
opacity: 1,
});
});
it("styles a last-used edge", () => {
assert.deepEqual(edgeStyle(false, true, false), {
stroke: "#f59e0b",
strokeWidth: 1.5,
opacity: 0.6,
});
});
it("styles an idle edge", () => {
assert.deepEqual(edgeStyle(false, false, false), {
stroke: "var(--color-border)",
strokeWidth: 1,
opacity: 0.2,
});
});
it("applies precedence error > active > last", () => {
assert.equal(edgeStyle(true, true, true).stroke, "#ef4444"); // error wins
assert.equal(edgeStyle(true, true, false).stroke, "#22c55e"); // active beats last
});
});
@@ -0,0 +1,235 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next/navigation", () => ({
useParams: () => ({ id: "openrouter" }),
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: EditConnectionModal } =
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal");
const FREE_TOGGLE = 'button[role="switch"][aria-label="importFreeModelsOnlyLabel"]';
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(props: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<EditConnectionModal
isOpen
providerId={(props.providerId as string) || "openrouter"}
onSave={async () => undefined}
onClose={() => {}}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
async function waitFor(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitFor timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
vi.clearAllMocks();
vi.stubGlobal(
"fetch",
vi.fn(() =>
Promise.resolve({ ok: true, json: async () => ({}), text: async () => "" } as Response)
)
);
vi.stubGlobal("localStorage", {
getItem: () => null,
setItem: () => undefined,
removeItem: () => undefined,
clear: () => undefined,
});
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
describe("EditConnectionModal — import only free models", () => {
it("shows the free-only toggle for a connection whose provider has free models", () => {
const el = render({
connection: { id: "conn-1", provider: "openrouter", providerSpecificData: {} },
});
expect(el.querySelector(FREE_TOGGLE)).toBeTruthy();
});
it("hides the free-only toggle for a provider without free models", () => {
const el = render({
providerId: "anthropic",
connection: { id: "conn-2", provider: "anthropic", providerSpecificData: {} },
});
expect(el.querySelector(FREE_TOGGLE)).toBeNull();
});
it("reflects the persisted flag as initially checked", () => {
const el = render({
connection: {
id: "conn-3",
provider: "openrouter",
providerSpecificData: { importFreeModelsOnly: true },
},
});
expect(el.querySelector(FREE_TOGGLE)!.getAttribute("aria-checked")).toBe("true");
});
it("clears the flag (explicit false) and re-syncs when toggled off", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const onResyncModels = vi.fn().mockResolvedValue(undefined);
const el = render({
connection: {
id: "conn-5",
provider: "openrouter",
name: "OR",
providerSpecificData: { importFreeModelsOnly: true },
},
onSave,
onResyncModels,
});
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
expect(toggle.getAttribute("aria-checked")).toBe("true");
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
// Must be an explicit `false`, not `undefined` — otherwise the PUT merge keeps the old `true`.
expect(onSave.mock.calls[0][0].providerSpecificData?.importFreeModelsOnly).toBe(false);
await waitFor(() => onResyncModels.mock.calls.length > 0);
expect(onResyncModels).toHaveBeenCalledWith("conn-5");
});
it("saves the flag and triggers a re-sync when toggled on", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const onResyncModels = vi.fn().mockResolvedValue(undefined);
const el = render({
connection: { id: "conn-4", provider: "openrouter", name: "OR", providerSpecificData: {} },
onSave,
onResyncModels,
});
const toggle = el.querySelector<HTMLButtonElement>(FREE_TOGGLE)!;
act(() => {
toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.importFreeModelsOnly).toBe(true);
await waitFor(() => onResyncModels.mock.calls.length > 0);
expect(onResyncModels).toHaveBeenCalledWith("conn-4");
});
});
describe("EditConnectionModal — quota scraping fields", () => {
it("saves OpenCode Go workspace and replacement auth cookie", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "opencode-go",
connection: {
id: "conn-opencode-go",
provider: "opencode-go",
name: "OpenCode Go",
authType: "apikey",
providerSpecificData: { workspaceId: "workspace-existing" },
},
onSave,
});
const workspaceInput = el.querySelector<HTMLInputElement>(
'input[name="opencodeGoWorkspaceId"]'
)!;
const cookieInput = el.querySelector<HTMLInputElement>('input[name="opencodeGoAuthCookie"]')!;
expect(workspaceInput.value).toBe("workspace-existing");
setInputValue(workspaceInput, "workspace-updated");
setInputValue(cookieInput, "auth=opencode-cookie");
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect(payload.providerSpecificData?.opencodeGoWorkspaceId).toBe("workspace-updated");
expect(payload.providerSpecificData?.opencodeGoAuthCookie).toBe("auth=opencode-cookie");
});
it("omits Ollama Cloud usage cookie when the edit field is left blank", async () => {
const onSave = vi.fn().mockResolvedValue(undefined);
const el = render({
providerId: "ollama-cloud",
connection: {
id: "conn-ollama-cloud",
provider: "ollama-cloud",
name: "Ollama Cloud",
authType: "apikey",
providerSpecificData: {},
},
onSave,
});
expect(el.querySelector<HTMLInputElement>('input[name="ollamaCloudUsageCookie"]')).toBeTruthy();
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "save"
)!;
act(() => {
saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitFor(() => onSave.mock.calls.length > 0);
const payload = onSave.mock.calls[0][0];
expect("ollamaCloudUsageCookie" in payload.providerSpecificData).toBe(false);
});
});
+340
View File
@@ -0,0 +1,340 @@
// @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";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("@/shared/components", () => ({
Modal: ({
isOpen,
title,
children,
footer,
onClose,
}: {
isOpen?: boolean;
title?: string;
children?: React.ReactNode;
footer?: React.ReactNode;
onClose?: () => void;
}) =>
isOpen
? React.createElement(
"div",
{ "data-testid": "modal", "data-title": title },
React.createElement("button", { onClick: onClose, "data-testid": "modal-close" }, "X"),
children,
footer,
)
: null,
Button: ({
children,
onClick,
disabled,
loading,
"data-testid": testId,
variant,
}: {
children?: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
"data-testid"?: string;
variant?: string;
}) =>
React.createElement(
"button",
{
onClick,
disabled: disabled || loading,
"data-testid": testId,
"data-variant": variant,
},
children,
),
Input: ({
value,
onChange,
placeholder,
"data-testid": testId,
className,
}: {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
"data-testid"?: string;
className?: string;
}) =>
React.createElement("input", {
value,
onChange,
placeholder,
"data-testid": testId,
className,
}),
Select: ({
children,
value,
onChange,
className,
}: {
children: React.ReactNode;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
className?: string;
}) => React.createElement("select", { value, onChange, className }, children),
}));
const MOCK_MEMORY = {
id: "mem-1",
type: "factual" as const,
key: "user.name",
content: "Alice",
metadata: { source: "test" },
};
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("EditMemoryModal", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
});
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders nothing when isOpen=false", async () => {
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={false}
onClose={vi.fn()}
onSaved={vi.fn()}
/>,
);
});
expect(container.querySelector("[data-testid='modal']")).toBeNull();
});
it("renders modal with memory fields populated when isOpen=true", async () => {
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={true}
onClose={vi.fn()}
onSaved={vi.fn()}
/>,
);
});
expect(container.querySelector("[data-testid='modal']")).toBeTruthy();
// The key input should have value "user.name" and content textarea "Alice"
const inputs = Array.from(container.querySelectorAll("input"));
const keyInput = inputs.find((i) => i.value === "user.name");
expect(keyInput).toBeTruthy();
const textareas = Array.from(container.querySelectorAll("textarea"));
const contentTextarea = textareas.find((ta) => ta.value === "Alice");
expect(contentTextarea).toBeTruthy();
});
it("shows metadata JSON in textarea", async () => {
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={true}
onClose={vi.fn()}
onSaved={vi.fn()}
/>,
);
});
const textareas = container.querySelectorAll("textarea");
// Should have at least the content textarea and metadata textarea
expect(textareas.length).toBeGreaterThanOrEqual(2);
// The metadata textarea should contain the JSON
const metadataTextarea = Array.from(textareas).find((ta) =>
ta.value.includes('"source"'),
);
expect(metadataTextarea).toBeTruthy();
});
it("calls PUT /api/memory/[id] and invokes onSaved+onClose when save succeeds", async () => {
const onSaved = vi.fn();
const onClose = vi.fn();
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
});
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={true}
onClose={onClose}
onSaved={onSaved}
/>,
);
});
// Find and click Save button (text="save")
const buttons = Array.from(container.querySelectorAll("button"));
const saveBtn = buttons.find((b) => b.textContent === "save");
expect(saveBtn).toBeTruthy();
await act(async () => {
saveBtn?.click();
});
await act(async () => {
await new Promise((r) => setTimeout(r, 50));
});
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
const putCalls = fetchMock.mock.calls.filter(
(c: [string, { method?: string }]) =>
typeof c[0] === "string" &&
c[0].includes("mem-1") &&
c[1] &&
c[1].method === "PUT",
);
expect(putCalls.length).toBeGreaterThan(0);
expect(onSaved).toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
it("shows error message when PUT fails", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ error: { message: "update failed" } }),
});
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={true}
onClose={vi.fn()}
onSaved={vi.fn()}
/>,
);
});
const buttons = Array.from(container.querySelectorAll("button"));
const saveBtn = buttons.find((b) => b.textContent === "save");
await act(async () => {
saveBtn?.click();
});
await act(async () => {
await new Promise((r) => setTimeout(r, 50));
});
expect(container.textContent).toContain("update failed");
});
it("shows metadata validation error for invalid JSON", async () => {
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={true}
onClose={vi.fn()}
onSaved={vi.fn()}
/>,
);
});
const textareas = Array.from(container.querySelectorAll("textarea"));
// Find the metadata textarea (the one with JSON content)
const metadataTextarea = textareas.find((ta) => ta.value.includes('"source"'));
expect(metadataTextarea).toBeTruthy();
// Use nativeInputValueSetter to set value and fire change event
await act(async () => {
if (metadataTextarea) {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
"value",
)?.set;
nativeSetter?.call(metadataTextarea, "not valid json {{{");
metadataTextarea.dispatchEvent(new Event("change", { bubbles: true }));
}
});
// The error text should contain the i18n key
expect(container.textContent).toContain("editModal.metadataInvalid");
});
it("calls onClose when modal close button is clicked", async () => {
const onClose = vi.fn();
const { default: EditMemoryModal } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EditMemoryModal
memory={MOCK_MEMORY}
isOpen={true}
onClose={onClose}
onSaved={vi.fn()}
/>,
);
});
const closeBtn = container.querySelector("[data-testid='modal-close']") as HTMLButtonElement | null;
expect(closeBtn).toBeTruthy();
await act(async () => {
closeBtn?.click();
});
expect(onClose).toHaveBeenCalled();
});
});
@@ -0,0 +1,188 @@
// @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";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("EmbeddingSourceSelector", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
const defaultSettings = {
embeddingSource: "auto" as const,
embeddingProviderModel: null,
transformersEnabled: false,
staticEnabled: false,
rerankEnabled: false,
rerankProviderModel: null,
};
it("renders all 4 source options", async () => {
const { default: EmbeddingSourceSelector } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EmbeddingSourceSelector
settings={defaultSettings}
providers={[]}
onSave={vi.fn().mockResolvedValue(true)}
/>,
);
});
expect(container.querySelector("[data-testid='embedding-source-auto']")).toBeTruthy();
expect(container.querySelector("[data-testid='embedding-source-remote']")).toBeTruthy();
expect(container.querySelector("[data-testid='embedding-source-static']")).toBeTruthy();
expect(container.querySelector("[data-testid='embedding-source-transformers']")).toBeTruthy();
});
it("shows only providers with hasKey=true in remote dropdown", async () => {
const { default: EmbeddingSourceSelector } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector"
);
const providers = [
{
provider: "openai",
hasKey: true,
models: [
{
id: "openai/text-embedding-3-small",
name: "text-embedding-3-small",
dimensions: 1536,
},
],
},
{
provider: "cohere",
hasKey: false,
models: [{ id: "cohere/embed-english-v3", name: "embed-english", dimensions: 1024 }],
},
];
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EmbeddingSourceSelector
settings={{ ...defaultSettings, embeddingSource: "remote" }}
providers={providers}
onSave={vi.fn().mockResolvedValue(true)}
/>,
);
});
// openai should be visible (hasKey=true)
expect(container.textContent).toContain("text-embedding-3-small");
// cohere should NOT be visible (hasKey=false)
expect(container.textContent).not.toContain("embed-english");
});
it("shows no-provider warning when remote selected but no providers with key", async () => {
const { default: EmbeddingSourceSelector } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EmbeddingSourceSelector
settings={{ ...defaultSettings, embeddingSource: "remote" }}
providers={[{ provider: "cohere", hasKey: false, models: [] }]}
onSave={vi.fn().mockResolvedValue(true)}
/>,
);
});
expect(container.textContent).toContain("embedding.noRemoteProviders");
});
it("shows transformers warning when transformers source selected", async () => {
const { default: EmbeddingSourceSelector } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EmbeddingSourceSelector
settings={{ ...defaultSettings, embeddingSource: "transformers" }}
providers={[]}
onSave={vi.fn().mockResolvedValue(true)}
/>,
);
});
expect(container.textContent).toContain("embedding.transformersWarning");
});
it("toggle-static-enabled calls onSave", async () => {
const { default: EmbeddingSourceSelector } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector"
);
const onSave = vi.fn().mockResolvedValue(true);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EmbeddingSourceSelector
settings={defaultSettings}
providers={[]}
onSave={onSave}
/>,
);
});
const toggleBtn = container.querySelector(
"[data-testid='toggle-static-enabled']",
) as HTMLButtonElement | null;
expect(toggleBtn).toBeTruthy();
await act(async () => {
toggleBtn?.click();
});
expect(onSave).toHaveBeenCalledWith({ staticEnabled: true });
});
it("toggle-transformers-enabled calls onSave", async () => {
const { default: EmbeddingSourceSelector } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector"
);
const onSave = vi.fn().mockResolvedValue(true);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(
<EmbeddingSourceSelector
settings={defaultSettings}
providers={[]}
onSave={onSave}
/>,
);
});
const toggleBtn = container.querySelector(
"[data-testid='toggle-transformers-enabled']",
) as HTMLButtonElement | null;
expect(toggleBtn).toBeTruthy();
await act(async () => {
toggleBtn?.click();
});
expect(onSave).toHaveBeenCalledWith({ transformersEnabled: true });
});
});
+234
View File
@@ -0,0 +1,234 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mountInContainer(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Mock fetch ────────────────────────────────────────────────────────────
function makeEnginePayload(engines: Array<{ id: string; name: string }>) {
return {
engines: engines.map(({ id, name }) => ({
id,
name,
description: "",
icon: "table_rows",
stackable: true,
stackPriority: 15,
metadata: {},
configSchema: [],
})),
};
}
const ANALYTICS_PAYLOAD = {
runs: 0,
tokensSaved: 0,
avgSavingsPercent: 0,
days: 7,
};
function setupFetchMock(engines: Array<{ id: string; name: string }>) {
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/api/compression/engines")) {
return new Response(JSON.stringify(makeEnginePayload(engines)), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/context/combos")) {
return new Response(JSON.stringify({ pipeline: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/context/analytics/engine")) {
return new Response(JSON.stringify(ANALYTICS_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({}), { status: 404 });
});
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe("HeadroomPage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "headroom", name: "Headroom" }]);
const { default: HeadroomPage } =
await import("../../../src/app/(dashboard)/dashboard/context/headroom/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<HeadroomPage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("Headroom");
});
});
describe("SessionDedupPage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "session-dedup", name: "Session Dedup" }]);
const { default: SessionDedupPage } =
await import("../../../src/app/(dashboard)/dashboard/context/session-dedup/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<SessionDedupPage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("Session Dedup");
});
});
describe("CcrPage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "ccr", name: "CCR" }]);
const { default: CcrPage } =
await import("../../../src/app/(dashboard)/dashboard/context/ccr/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<CcrPage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("CCR");
});
});
describe("LlmlinguaPage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "llmlingua", name: "LLMLingua" }]);
const { default: LlmlinguaPage } =
await import("../../../src/app/(dashboard)/dashboard/context/llmlingua/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<LlmlinguaPage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("LLMLingua");
});
});
describe("LitePage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "lite", name: "Lite" }]);
const { default: LitePage } =
await import("../../../src/app/(dashboard)/dashboard/context/lite/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<LitePage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("Lite");
});
});
describe("AggressivePage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "aggressive", name: "Aggressive" }]);
const { default: AggressivePage } =
await import("../../../src/app/(dashboard)/dashboard/context/aggressive/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<AggressivePage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("Aggressive");
});
});
describe("UltraPage", () => {
it("mounts without throwing and renders the engine name", async () => {
setupFetchMock([{ id: "ultra", name: "Ultra" }]);
const { default: UltraPage } =
await import("../../../src/app/(dashboard)/dashboard/context/ultra/page");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<UltraPage />);
});
await act(async () => {
await Promise.resolve();
});
expect(container).toBeTruthy();
expect(container.textContent).toContain("Ultra");
});
});
+247
View File
@@ -0,0 +1,247 @@
// @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";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
if (values) return `${key}:${JSON.stringify(values)}`;
return key;
},
}));
vi.mock("@/shared/components", () => ({
Card: ({ children }: { children: React.ReactNode }) =>
React.createElement("div", { className: "card" }, children),
Button: ({
children,
onClick,
disabled,
loading,
"data-testid": testId,
variant,
size,
}: {
children?: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
"data-testid"?: string;
variant?: string;
size?: string;
}) =>
React.createElement(
"button",
{ onClick, disabled: disabled || loading, "data-testid": testId, "data-variant": variant },
children,
),
}));
// Mock the hooks directly to avoid swr dependency resolution issues
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/hooks/useEngineStatus",
() => ({
useEngineStatus: () => ({
status: {
keyword: { available: true, backend: "FTS5" },
embedding: {
source: "remote",
model: "openai/text-embedding-3-small",
dimensions: 1536,
available: true,
reason: "provider openai with key configured",
cacheStats: { hits: 0, misses: 0, size: 0 },
},
vectorStore: {
backend: "sqlite-vec",
available: true,
rowCount: 10,
needsReindex: 0,
reason: "sqlite-vec loaded",
},
qdrant: { enabled: false, healthy: null, latencyMs: null, error: null },
rerank: {
enabled: false,
provider: null,
model: null,
available: false,
reason: "rerank disabled",
},
},
isLoading: false,
isError: false,
mutate: vi.fn(),
}),
}),
);
const mockSave = vi.fn().mockResolvedValue(true);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/hooks/useMemorySettings",
() => ({
useMemorySettings: () => ({
settings: {
enabled: true,
maxTokens: 2000,
retentionDays: 30,
strategy: "hybrid",
skillsEnabled: false,
embeddingSource: "auto",
embeddingProviderModel: null,
transformersEnabled: false,
staticEnabled: false,
rerankEnabled: false,
rerankProviderModel: null,
vectorStore: "auto",
},
isLoading: false,
isError: false,
mutate: vi.fn(),
save: mockSave,
}),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/MemoryEngineStatus",
() => ({
default: ({ status }: { status: { embedding: { available: boolean } } }) =>
React.createElement(
"div",
{ "data-testid": "engine-status-panel" },
status.embedding.available ? "embedding:available" : "embedding:unavailable",
),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/EmbeddingSourceSelector",
() => ({
default: ({
onSave,
}: {
settings: unknown;
providers: unknown[];
onSave: (u: unknown) => Promise<boolean>;
saving?: boolean;
}) =>
React.createElement(
"div",
{ "data-testid": "embedding-selector" },
React.createElement(
"button",
{
"data-testid": "toggle-transformers-btn",
onClick: () => onSave({ transformersEnabled: true }),
},
"toggle-transformers",
),
),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/QdrantConfigCard",
() => ({
default: () => React.createElement("div", { "data-testid": "qdrant-config-card" }, "QdrantCard"),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/RerankConfigCard",
() => ({
default: () => React.createElement("div", { "data-testid": "rerank-config-card" }, "RerankCard"),
}),
);
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("EngineTab", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ providers: [] }),
});
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders the engine status panel", async () => {
const { default: EngineTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/EngineTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<EngineTab />);
});
expect(container.querySelector("[data-testid='engine-status-panel']")).toBeTruthy();
expect(container.textContent).toContain("embedding:available");
});
it("renders QdrantConfigCard and RerankConfigCard", async () => {
const { default: EngineTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/EngineTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<EngineTab />);
});
expect(container.querySelector("[data-testid='qdrant-config-card']")).toBeTruthy();
expect(container.querySelector("[data-testid='rerank-config-card']")).toBeTruthy();
});
it("renders Reindex Now button", async () => {
const { default: EngineTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/EngineTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<EngineTab />);
});
expect(container.querySelector("[data-testid='reindex-now-button']")).toBeTruthy();
});
it("calls save() when EmbeddingSourceSelector calls onSave", async () => {
mockSave.mockClear();
const { default: EngineTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/EngineTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<EngineTab />);
});
const toggleBtn = container.querySelector(
"[data-testid='toggle-transformers-btn']",
) as HTMLButtonElement | null;
expect(toggleBtn).toBeTruthy();
await act(async () => {
toggleBtn?.click();
});
await act(async () => {
await new Promise((r) => setTimeout(r, 50));
});
expect(mockSave).toHaveBeenCalledWith({ transformersEnabled: true });
});
});
+249
View File
@@ -0,0 +1,249 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import type { EngineConfigField } from "@omniroute/open-sse/services/compression/engines/types";
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.clearAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
const SCHEMA: EngineConfigField[] = [
{
key: "enabled",
type: "boolean",
label: "Enabled",
description: "Enable this engine",
defaultValue: false,
},
{
key: "maxTokens",
type: "number",
label: "Max Tokens",
defaultValue: 1000,
min: 100,
max: 8000,
},
{
key: "prefix",
type: "string",
label: "Prefix",
defaultValue: "",
},
{
key: "strategy",
type: "select",
label: "Strategy",
defaultValue: "fast",
options: [
{ value: "fast", label: "Fast" },
{ value: "thorough", label: "Thorough" },
],
},
{
key: "techniques",
type: "multiselect",
label: "Techniques",
defaultValue: [],
options: [
{ value: "strip-comments", label: "Strip Comments" },
{ value: "minify", label: "Minify" },
],
},
];
const INITIAL_VALUE: Record<string, unknown> = {
enabled: false,
maxTokens: 1000,
prefix: "hello",
strategy: "fast",
techniques: [],
};
describe("EngineConfigForm", () => {
it("renders a checkbox for the boolean field", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
// Label text appears
expect(container.textContent).toContain("Enabled");
// Description appears
expect(container.textContent).toContain("Enable this engine");
// Checkbox exists
const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement | null;
expect(checkbox).toBeTruthy();
expect(checkbox?.checked).toBe(false);
});
it("renders a number input with min/max for the number field", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
expect(container.textContent).toContain("Max Tokens");
const numInput = container.querySelector("input[type='number']") as HTMLInputElement | null;
expect(numInput).toBeTruthy();
expect(numInput?.getAttribute("min")).toBe("100");
expect(numInput?.getAttribute("max")).toBe("8000");
});
it("renders a text input for the string field", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
expect(container.textContent).toContain("Prefix");
const textInput = container.querySelector("input[type='text']") as HTMLInputElement | null;
expect(textInput).toBeTruthy();
expect(textInput?.value).toBe("hello");
});
it("renders a select with 2 options for the select field", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
expect(container.textContent).toContain("Strategy");
const select = container.querySelector("select") as HTMLSelectElement | null;
expect(select).toBeTruthy();
const options = Array.from(select?.querySelectorAll("option") ?? []);
expect(options).toHaveLength(2);
expect(options[0].value).toBe("fast");
expect(options[1].value).toBe("thorough");
});
it("renders 2 checkboxes for the multiselect field", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
expect(container.textContent).toContain("Techniques");
expect(container.textContent).toContain("Strip Comments");
expect(container.textContent).toContain("Minify");
// boolean field checkbox + 2 multiselect checkboxes = 3 total
const allCheckboxes = Array.from(container.querySelectorAll("input[type='checkbox']"));
expect(allCheckboxes).toHaveLength(3); // boolean + 2 multiselect
});
it("calls onChange with flipped boolean when checkbox is toggled", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
// The first checkbox is the boolean field
const checkbox = container.querySelector("input[type='checkbox']") as HTMLInputElement;
expect(checkbox).toBeTruthy();
// React listens to the native "click" event for checkboxes; setting
// .checked + dispatching a MouseEvent("click") is what triggers the
// synthetic onChange handler in jsdom.
act(() => {
checkbox.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledWith({ ...INITIAL_VALUE, enabled: true });
});
it("calls onChange with new number when number input changes", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
const numInput = container.querySelector("input[type='number']") as HTMLInputElement;
expect(numInput).toBeTruthy();
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
act(() => {
nativeSetter?.call(numInput, "2048");
numInput.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledWith({ ...INITIAL_VALUE, maxTokens: 2048 });
});
it("calls onChange with updated array when a multiselect option is checked", async () => {
const { EngineConfigForm } =
await import("../../../src/shared/components/compression/EngineConfigForm");
const onChange = vi.fn();
const container = mount(
<EngineConfigForm schema={SCHEMA} value={INITIAL_VALUE} onChange={onChange} />
);
// The multiselect checkboxes are the 2nd and 3rd checkboxes
const allCheckboxes = Array.from(
container.querySelectorAll("input[type='checkbox']")
) as HTMLInputElement[];
// Index 0 = boolean field, index 1 = "strip-comments", index 2 = "minify"
const stripCommentsCheckbox = allCheckboxes[1];
expect(stripCommentsCheckbox).toBeTruthy();
// React listens to native "click" for checkboxes to trigger synthetic onChange.
act(() => {
stripCommentsCheckbox.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledWith({
...INITIAL_VALUE,
techniques: ["strip-comments"],
});
});
});
+420
View File
@@ -0,0 +1,420 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mountInContainer(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
// Restore mocks first so any in-flight fetch promises settle without blocking
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) {
roots.pop()?.unmount();
}
});
// Drain all remaining microtasks from effects that fired during unmount
for (let i = 0; i < 10; i++) {
await Promise.resolve();
}
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Mock fetch ────────────────────────────────────────────────────────────
const ENGINE_PAYLOAD = {
engines: [
{
id: "headroom",
name: "Headroom",
description: "Headroom engine description",
icon: "🗜️",
stackable: true,
stackPriority: 1,
metadata: { description: "Headroom metadata description" },
configSchema: [
{
key: "enabled",
type: "boolean",
label: "Enabled",
defaultValue: true,
},
{
key: "minRows",
type: "number",
label: "Min rows",
defaultValue: 8,
min: 1,
max: 1000,
},
],
},
],
};
const COMBO_PAYLOAD = {
id: "default",
name: "Default",
description: "Default combo",
pipeline: [],
languagePacks: [],
outputMode: null,
};
const ANALYTICS_PAYLOAD = {
engineId: "headroom",
runs: 0,
tokensSaved: 0,
avgSavingsPercent: 0,
days: 7,
};
const SETTINGS_PAYLOAD = {
enabled: true,
engines: { headroom: { enabled: true } },
aggressive: {
summarizerEnabled: true,
maxTokensPerMessage: 2048,
minSavingsThreshold: 0.05,
},
};
function setupFetchMock() {
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/api/compression/engines")) {
return new Response(JSON.stringify(ENGINE_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/settings/compression")) {
return new Response(JSON.stringify(SETTINGS_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/context/combos/default")) {
return new Response(JSON.stringify(COMBO_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/context/analytics/engine")) {
return new Response(JSON.stringify(ANALYTICS_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/compression/preview")) {
return new Response(
JSON.stringify({
original: "The original context contains duplicated details and verbose wording.",
compressed: "Original context, deduplicated.",
originalTokens: 11,
compressedTokens: 4,
savingsPct: 63.6,
diff: [{ type: "removed", text: "duplicated details and verbose wording" }],
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify({}), { status: 404 });
});
}
// ── Tests ─────────────────────────────────────────────────────────────────
describe("EngineConfigPage", () => {
it("renders the engine name after fetching engine list", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
// Flush any pending microtasks from effects
await act(async () => {
await Promise.resolve();
});
expect(container.textContent).toContain("Headroom");
});
it("does NOT render an engine on/off enable toggle (moved to the panel)", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
// The on/off enable control now lives only in the panel (/dashboard/context/settings).
expect(container.querySelector("[data-toggle='enable']")).toBeNull();
expect(container.textContent).not.toContain("Enable layer");
});
it("renders the config form field label from fetched schema (EngineConfigForm mounted)", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
// EngineConfigForm should render the "Min rows" field label from the schema
expect(container.textContent).toContain("Min rows");
});
it("keeps detailed config but renders no engine enable checkbox", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
// The on/off enable toggle (a checkbox with data-toggle="enable") is gone; the
// detailed config form (the schema fields minus `enabled`) still renders.
expect(container.querySelector("input[type='checkbox'][data-toggle='enable']")).toBeNull();
expect(container.textContent).toContain("Min rows");
expect(container.textContent).toContain("Configuration");
});
it("renders preview original, compressed text, and diff returned by the API", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
const previewButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "Preview"
);
expect(previewButton).toBeTruthy();
await act(async () => {
previewButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
await Promise.resolve();
});
expect(container.textContent).toContain(
"The original context contains duplicated details and verbose wording."
);
expect(container.textContent).toContain("Original context, deduplicated.");
expect(container.textContent).toContain("Diff");
expect(container.textContent).toContain("duplicated details and verbose wording");
});
it("shows empty-state text when analytics returns runs=0", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
// Should show some "no data" copy
const hasEmptyState =
container.textContent?.includes("Sem dados") === true ||
container.textContent?.includes("No data") === true;
expect(hasEmptyState).toBe(true);
});
it("points to the Compression Settings panel for enabling the layer", async () => {
setupFetchMock();
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
// The on/off + level live in the panel now; the page surfaces a link to it.
const settingsLink = container.querySelector('a[href="/dashboard/context/settings"]');
expect(settingsLink).not.toBeNull();
expect(container.textContent).toContain("Compression Settings");
});
it("INVARIANT #1: handleSave writes the detailed sub-object to settings/compression, never PUTs combos/default", async () => {
const AGGRESSIVE_PAYLOAD = {
engines: [
{
id: "aggressive",
name: "Aggressive",
description: "Aggressive engine",
icon: "🗜️",
stackable: true,
stackPriority: 30,
metadata: { description: "Aggressive metadata" },
configSchema: [
{ key: "enabled", type: "boolean", label: "Enabled", defaultValue: true },
{
key: "maxTokensPerMessage",
type: "number",
label: "Max tokens per message",
defaultValue: 2048,
},
],
},
],
};
const settingsPuts: { body: Record<string, unknown> }[] = [];
const comboWrites: { method: string }[] = [];
vi.spyOn(globalThis, "fetch").mockImplementation(
async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
if (url.includes("/api/compression/engines")) {
return new Response(JSON.stringify(AGGRESSIVE_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/settings/compression")) {
if (init?.method === "PUT") {
settingsPuts.push({ body: JSON.parse(init.body as string) });
}
return new Response(
JSON.stringify({
enabled: true,
engines: { aggressive: { enabled: true } },
aggressive: { maxTokensPerMessage: 2048 },
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
if (url.includes("/api/context/combos/default")) {
// A PUT/POST here would violate INVARIANT #1 (the route is a 410 shim).
if (init?.method === "PUT" || init?.method === "POST") {
comboWrites.push({ method: init.method });
}
return new Response(JSON.stringify(COMBO_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.includes("/api/context/analytics/engine")) {
return new Response(JSON.stringify(ANALYTICS_PAYLOAD), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({}), { status: 404 });
}
);
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="aggressive" />);
});
await act(async () => {
await Promise.resolve();
});
const salvarBtn = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.includes("Save") || b.textContent?.includes("Salvar")
);
expect(salvarBtn).toBeTruthy();
await act(async () => {
salvarBtn?.click();
});
await act(async () => {
await Promise.resolve();
});
// INVARIANT #1: no write ever lands on the deprecated default-combo route.
expect(comboWrites).toHaveLength(0);
// The detailed config persists to the engine's sub-object on settings/compression.
expect(settingsPuts.length).toBeGreaterThan(0);
const aggressivePut = settingsPuts.find(
(c) => typeof c.body.aggressive === "object" && c.body.aggressive !== null
);
expect(aggressivePut).toBeDefined();
});
it("does not crash when all fetch calls fail (fail-soft)", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network error"));
const { EngineConfigPage } =
await import("../../../src/shared/components/compression/EngineConfigPage");
let container!: HTMLElement;
await act(async () => {
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
});
await act(async () => {
await Promise.resolve();
});
// Component should still be mounted (not crashed)
expect(container).toBeTruthy();
expect(container.parentNode).toBeTruthy();
});
});
+141
View File
@@ -0,0 +1,141 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest";
// ── Polyfill ResizeObserver ────────────────────────────────────────────────
beforeAll(() => {
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
});
// ── Mocks ─────────────────────────────────────────────────────────────────
// ReactFlow's Handle/Position require a ReactFlowProvider context.
// We stub the xyflow module so EngineNode can render without the provider.
vi.mock("@xyflow/react", async () => {
const actual = (await vi.importActual("@xyflow/react")) as Record<string, unknown>;
return {
...actual,
Handle: (_props: Record<string, unknown>) => null,
Position: { Left: "left", Right: "right", Top: "top", Bottom: "bottom" },
};
});
// ── Import after mocks ─────────────────────────────────────────────────────
const { EngineNode } =
await import("@/app/(dashboard)/dashboard/compression/studio/nodes/EngineNode");
// ── Helpers ───────────────────────────────────────────────────────────────
const containers: HTMLElement[] = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
// ── Sample data ───────────────────────────────────────────────────────────
const SAMPLE_ENGINE_DATA = {
id: "engine-0",
type: "engine",
position: { x: 200, y: 0 },
data: {
engine: "caveman",
originalTokens: 900,
compressedTokens: 729,
savingsPercent: 19.0,
techniquesUsed: ["filler-drop", "dedup"],
durationMs: 5.3,
stepState: "done" as const,
label: "caveman",
},
};
// NodeProps-compatible shim
const nodeProps = {
...SAMPLE_ENGINE_DATA,
selected: false,
isConnectable: true,
dragging: false,
zIndex: 0,
positionAbsoluteX: 200,
positionAbsoluteY: 0,
width: 180,
height: 80,
} as unknown as Parameters<typeof EngineNode>[0];
// ── Tests ─────────────────────────────────────────────────────────────────
describe("EngineNode", () => {
it("renders the engine name", () => {
const container = mount(<EngineNode {...nodeProps} />);
expect(container.textContent).toContain("caveman");
});
it("shows the savings percentage", () => {
const container = mount(<EngineNode {...nodeProps} />);
const el = container.querySelector("[data-testid='savings-percent']");
expect(el).toBeTruthy();
expect(el?.textContent).toContain("19.0");
});
it("shows technique names", () => {
const container = mount(<EngineNode {...nodeProps} />);
expect(container.textContent).toContain("filler-drop");
});
it("marks a skipped node (tokensIn === tokensOut)", () => {
const skippedProps = {
...nodeProps,
data: {
...SAMPLE_ENGINE_DATA.data,
compressedTokens: SAMPLE_ENGINE_DATA.data.originalTokens,
savingsPercent: 0,
stepState: "skipped" as const,
},
};
const container = mount(<EngineNode {...skippedProps} />);
expect(container.textContent).toContain("skip");
});
it("renders layer pills for rtk engine", () => {
const rtkProps = {
...nodeProps,
data: {
...SAMPLE_ENGINE_DATA.data,
engine: "rtk",
stepState: "done" as const,
},
};
const container = mount(<EngineNode {...rtkProps} />);
// rtk maps to L3, L4
expect(container.textContent).toContain("L3");
expect(container.textContent).toContain("L4");
});
});
+22
View File
@@ -0,0 +1,22 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from "vitest";
import { runPreviewBatch } from "@/hooks/usePreviewCompression";
beforeEach(() => vi.restoreAllMocks());
describe("runPreviewBatch fidelityGate", () => {
it("includes fidelityGate:{enabled:true} in every preview payload when on", async () => {
const payloads: any[] = [];
vi.stubGlobal("fetch", vi.fn(async (_u: string, init: any) => {
payloads.push(JSON.parse(init.body));
return { ok: true, json: async () => ({
original: "o", compressed: "c", originalTokens: 5, compressedTokens: 5, savingsPct: 0,
mode: "stacked", durationMs: 1, engineBreakdown: [], diff: [], preservedBlocks: [], ruleRemovals: [],
}) } as any;
}));
await runPreviewBatch({
messages: [{ role: "user", content: "x" }],
laneEngines: ["rtk"], activeEngines: ["rtk"], fidelityGate: true,
});
expect(payloads.length).toBe(2); // 1 lane + 1 combined
expect(payloads.every((p) => p.fidelityGate?.enabled === true)).toBe(true);
});
});
+161
View File
@@ -0,0 +1,161 @@
/**
* tests/unit/ui/fleetAggregation.test.ts
*
* TDD for `aggregateComboEventsToSets` — fleet aggregation for Tela B U2.
* Run: node --import tsx/esm --test tests/unit/ui/fleetAggregation.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { aggregateComboEventsToSets } from "../../../src/app/(dashboard)/dashboard/combos/live/fleetAggregation.ts";
import type { ComboEventInput } from "../../../src/app/(dashboard)/dashboard/combos/live/comboFlowModel.ts";
// ── helpers ───────────────────────────────────────────────────────────────
const WINDOW_MS = 10_000; // 10 seconds
const NOW = 1_000_000; // fixed "now" for deterministic tests
function ev(
type: "attempt" | "succeeded" | "failed",
provider: string,
timestampOffset: number // relative to NOW (negative = in the past)
): ComboEventInput {
return {
comboName: "fleet-combo",
targetIndex: 0,
provider,
model: "m",
type,
timestamp: NOW + timestampOffset,
};
}
// ── aggregateComboEventsToSets ────────────────────────────────────────────
describe("aggregateComboEventsToSets — basic categorization", () => {
it("puts a recently failed provider in error set", () => {
const events: ComboEventInput[] = [ev("failed", "openai", -1000)]; // 1s ago, within 10s window
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(error.has("openai"), "openai should be in error set");
assert.ok(!active.has("openai"), "openai should not be in active set");
assert.ok(!last.has("openai"), "openai should not be in last set");
});
it("puts a recent attempt provider in active set", () => {
const events: ComboEventInput[] = [ev("attempt", "anthropic", -500)];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(active.has("anthropic"), "anthropic should be in active set");
assert.ok(!error.has("anthropic"));
assert.ok(!last.has("anthropic"));
});
it("puts a recent succeeded provider in active set", () => {
const events: ComboEventInput[] = [ev("succeeded", "gemini", -2000)];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(active.has("gemini"), "gemini should be in active set");
assert.ok(!error.has("gemini"));
assert.ok(!last.has("gemini"));
});
it("puts an old event provider in last set (outside window)", () => {
const events: ComboEventInput[] = [ev("attempt", "cohere", -(WINDOW_MS + 1))]; // just outside window
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(last.has("cohere"), "cohere should be in last set");
assert.ok(!active.has("cohere"));
assert.ok(!error.has("cohere"));
});
it("puts an old failed provider in last set (not error)", () => {
const events: ComboEventInput[] = [ev("failed", "mistral", -(WINDOW_MS + 5000))];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(last.has("mistral"), "old failed should be last, not error");
assert.ok(!error.has("mistral"));
});
});
describe("aggregateComboEventsToSets — multiple providers and events", () => {
it("handles multiple providers independently", () => {
const events: ComboEventInput[] = [
ev("failed", "openai", -500), // recent → error
ev("succeeded", "gemini", -1000), // recent → active
ev("attempt", "cohere", -(WINDOW_MS + 1)), // old → last
];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(error.has("openai"));
assert.ok(active.has("gemini"));
assert.ok(last.has("cohere"));
});
it("latest event for provider wins (error beats active for same provider)", () => {
// Two events for openai: succeeded first (older), then failed (newer)
const events: ComboEventInput[] = [
ev("succeeded", "openai", -3000), // older, within window
ev("failed", "openai", -500), // newer → should win
];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(error.has("openai"), "latest event (failed) should win");
assert.ok(!active.has("openai"));
});
it("latest event for provider wins (active beats old failure when newer event is success)", () => {
const events: ComboEventInput[] = [
ev("failed", "openai", -3000), // older failed, within window
ev("succeeded", "openai", -500), // newer success → should win
];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.ok(active.has("openai"), "newer succeeded should win over older failed");
assert.ok(!error.has("openai"));
});
it("returns empty sets for empty events list", () => {
const { active, error, last } = aggregateComboEventsToSets([], WINDOW_MS, NOW);
assert.equal(active.size, 0);
assert.equal(error.size, 0);
assert.equal(last.size, 0);
});
it("a provider appears in at most one set", () => {
const events: ComboEventInput[] = [
ev("failed", "openai", -500),
ev("attempt", "openai", -1000),
];
const { active, error, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
const inSets =
(active.has("openai") ? 1 : 0) + (error.has("openai") ? 1 : 0) + (last.has("openai") ? 1 : 0);
assert.equal(inSets, 1, "provider should be in exactly one set");
});
});
describe("aggregateComboEventsToSets — window boundary", () => {
it("includes event exactly at the window boundary (now - windowMs) as 'last'", () => {
const events: ComboEventInput[] = [ev("attempt", "boundary", -WINDOW_MS)];
const { active, last } = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
// timestamp === NOW - WINDOW_MS: age === WINDOW_MS → not within (age < windowMs)
assert.ok(
last.has("boundary") || !active.has("boundary"),
"boundary event should be last or absent from active"
);
});
it("does not call Date.now() — pure function (same output with same now)", () => {
const events: ComboEventInput[] = [ev("failed", "openai", -500)];
const r1 = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
const r2 = aggregateComboEventsToSets(events, WINDOW_MS, NOW);
assert.deepEqual([...r1.error], [...r2.error]);
assert.deepEqual([...r1.active], [...r2.active]);
assert.deepEqual([...r1.last], [...r2.last]);
});
});
+63
View File
@@ -0,0 +1,63 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest";
import { FlowCanvas } from "@/shared/components/flow/FlowCanvas";
beforeAll(() => {
// ReactFlow relies on ResizeObserver, which jsdom does not implement.
globalThis.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
} as unknown as typeof ResizeObserver;
});
const containers: HTMLElement[] = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
const nodes = [
{ id: "a", position: { x: 0, y: 0 }, data: { label: "A" } },
{ id: "b", position: { x: 120, y: 0 }, data: { label: "B" } },
];
const edges = [{ id: "a-b", source: "a", target: "b" }];
describe("FlowCanvas (U0 — shared ReactFlow wrapper)", () => {
it("renders the canvas with Controls and hides the attribution", () => {
const container = mount(<FlowCanvas nodes={nodes} edges={edges} fitKey="x" />);
expect(container.querySelector(".react-flow")).toBeTruthy();
expect(container.querySelector(".react-flow__controls")).toBeTruthy();
// proOptions.hideAttribution => the attribution element must not render.
expect(container.querySelector(".react-flow__attribution")).toBeNull();
});
it("applies the provided container className for sizing/theming", () => {
const container = mount(
<FlowCanvas nodes={nodes} edges={edges} className="omni-test-canvas h-[300px]" />
);
expect(container.querySelector(".omni-test-canvas")).toBeTruthy();
});
});
+22
View File
@@ -0,0 +1,22 @@
// @vitest-environment jsdom
// tests/unit/ui/fuzzyDedupToggle.test.tsx
import { describe, it, expect, vi, beforeEach } from "vitest";
import { runPreviewBatch } from "@/hooks/usePreviewCompression";
beforeEach(() => vi.restoreAllMocks());
describe("runPreviewBatch fuzzyDedup", () => {
it("includes fuzzyDedup:{enabled:true} in every preview payload when on", async () => {
const payloads: any[] = [];
vi.stubGlobal("fetch", vi.fn(async (_u: string, init: any) => {
payloads.push(JSON.parse(init.body));
return { ok: true, json: async () => ({
original: "o", compressed: "c", originalTokens: 5, compressedTokens: 5, savingsPct: 0,
mode: "stacked", durationMs: 1, engineBreakdown: [], diff: [], preservedBlocks: [], ruleRemovals: [],
}) } as any;
}));
await runPreviewBatch({
messages: [{ role: "user", content: "x" }], laneEngines: ["session-dedup"], activeEngines: ["session-dedup"], fuzzyDedup: true,
});
expect(payloads.length).toBe(2);
expect(payloads.every((p) => p.fuzzyDedup?.enabled === true)).toBe(true);
});
});
@@ -0,0 +1,118 @@
// @vitest-environment jsdom
//
// Regression guard for the infinite render loop on /dashboard/cli-agents/hermes-agent.
//
// HermesAgentToolCard used to list `currentRoles` in the dependency array of the
// effect that calls loadCurrentConfig(); loadCurrentConfig() sets currentRoles to a
// fresh object on every fetch, so the effect re-fired → refetched → re-set → … forever.
// On the detail page isExpanded is hardcoded true, so it spun without end and spammed
// GET /api/cli-tools/hermes-agent-settings in the console. This test mounts the card
// expanded and asserts the settings endpoint is fetched a BOUNDED number of times.
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("@/shared/components", () => ({
Card: ({ children }: any) => <div>{children}</div>,
Button: ({ children, onClick, disabled, ...props }: any) => (
<button type="button" onClick={onClick} disabled={disabled} {...props}>
{children}
</button>
),
ModelSelectModal: () => <div data-testid="ModelSelectModal" />,
}));
const { default: HermesAgentToolCard } = await import(
"@/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard"
);
const SETTINGS_ENDPOINT = "/api/cli-tools/hermes-agent-settings";
const containers: HTMLElement[] = [];
let settingsCalls = 0;
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
settingsCalls = 0;
// fetch mock: count settings GETs and return valid roles. A safety valve breaks any
// runaway loop after 8 calls (returns a non-success body so loadCurrentConfig stops
// mutating currentRoles) so the buggy code FAILS the assertion instead of hanging.
vi.stubGlobal(
"fetch",
vi.fn(async (url: any) => {
const u = typeof url === "string" ? url : String(url);
if (u.includes(SETTINGS_ENDPOINT)) {
settingsCalls++;
if (settingsCalls > 8) {
return { ok: false, json: async () => ({ success: false }) } as any;
}
return {
ok: true,
json: async () => ({
success: true,
roles: {
default: {
model: "openai/gpt-4o",
provider: "omniroute",
base_url: "http://localhost:20128",
},
},
}),
} as any;
}
return { ok: true, json: async () => ({}) } as any;
})
);
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
function renderExpandedCard() {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(
<HermesAgentToolCard
tool={{ name: "Hermes Agent", description: "Hermes Agent" }}
isExpanded={true}
baseUrl="http://localhost:20128"
apiKeys={[{ id: "key-1" }]}
activeProviders={[]}
batchStatus={null}
/>
);
});
return container;
}
describe("HermesAgentToolCard — config-load effect", () => {
it("loads current config once when expanded (does not loop on currentRoles)", async () => {
renderExpandedCard();
// Drive every pending microtask/effect to quiescence. On the buggy code each
// settings response re-triggered the effect, so these flushes would keep firing
// fetches up to the safety valve; on the fixed code it settles after one fetch.
for (let i = 0; i < 6; i++) {
await act(async () => {});
}
expect(settingsCalls).toBeLessThanOrEqual(2);
expect(settingsCalls).toBeGreaterThanOrEqual(1);
});
});
@@ -0,0 +1,76 @@
// @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";
let lastModelSelectProps: any = null;
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("@/shared/components", () => ({
Card: ({ children }: any) => <div>{children}</div>,
Button: ({ children, onClick, disabled, ...props }: any) => (
<button type="button" onClick={onClick} disabled={disabled} {...props}>
{children}
</button>
),
ModelSelectModal: (props: any) => {
lastModelSelectProps = props;
return <div data-testid="ModelSelectModal" />;
},
}));
const { default: HermesAgentToolCard } =
await import("@/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard");
const containers: HTMLElement[] = [];
function renderCard() {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(
<HermesAgentToolCard
tool={{ name: "Hermes Agent", description: "Hermes Agent" }}
isExpanded={false}
baseUrl="http://localhost:3000"
apiKeys={[{ id: "key-1" }]}
activeProviders={[]}
batchStatus={null}
/>
);
});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
lastModelSelectProps = null;
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
});
describe("HermesAgentToolCard", () => {
it("keeps OpenCode Free available in the model picker even with no active connections", async () => {
renderCard();
await act(async () => {});
expect(lastModelSelectProps).toBeTruthy();
expect(lastModelSelectProps.activeProviders).toEqual([]);
expect(lastModelSelectProps.alwaysIncludeProviders).toContain("opencode");
});
});
@@ -0,0 +1,77 @@
/**
* Tests for HistoricSessionBanner — render with sessionName/null + backToLive callback
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
// Pure logic tests — no DOM needed (no next-intl in node:test runner)
describe("HistoricSessionBanner logic", () => {
it("resolves session display name when provided", () => {
const sessionName = "My Test Session";
const display = sessionName ?? "Untitled session";
assert.equal(display, "My Test Session");
});
it("falls back to untitledSession when sessionName is null", () => {
const sessionName: string | null = null;
const fallback = "Untitled session";
const display = sessionName ?? fallback;
assert.equal(display, "Untitled session");
});
it("falls back to untitledSession when sessionName is empty string", () => {
const sessionName: string | null = "";
// empty string is falsy — banner should show fallback
const fallback = "Untitled session";
const display = sessionName || fallback;
assert.equal(display, "Untitled session");
});
it("onBackToLive callback is invoked on click", () => {
let called = false;
const onBackToLive = () => { called = true; };
// Simulate button click
onBackToLive();
assert.equal(called, true);
});
it("banner renders when selectedSessionId is defined (gate logic)", () => {
const selectedSessionId: string | undefined = "session-abc";
// In TrafficInspectorPageClient the banner renders when this is not undefined
const shouldRender = selectedSessionId !== undefined;
assert.equal(shouldRender, true);
});
it("banner does not render when selectedSessionId is undefined (gate logic)", () => {
const selectedSessionId: string | undefined = undefined;
const shouldRender = selectedSessionId !== undefined;
assert.equal(shouldRender, false);
});
it("backToLive sets sessionId to undefined", () => {
let sessionId: string | undefined = "session-abc";
const backToLive = () => { sessionId = undefined; };
backToLive();
assert.equal(sessionId, undefined);
});
it("session name is looked up from sessions array", () => {
const sessions = [
{ id: "s1", name: "Session Alpha", startedAt: "2024-01-01", requestCount: 5 },
{ id: "s2", name: undefined, startedAt: "2024-01-02", requestCount: 3 },
];
const selectedId = "s1";
const name = sessions.find((s) => s.id === selectedId)?.name ?? null;
assert.equal(name, "Session Alpha");
});
it("session name is null when session not found in array", () => {
const sessions = [
{ id: "s1", name: "Session Alpha", startedAt: "2024-01-01", requestCount: 5 },
];
const selectedId = "not-exist";
const name = sessions.find((s) => s.id === selectedId)?.name ?? null;
assert.equal(name, null);
});
});
@@ -0,0 +1,161 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
function makeTranslator() {
const t = (key: string) => key;
t.rich = (key: string) => key;
return t;
}
vi.mock("next-intl", () => ({
useTranslations: () => makeTranslator(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
refresh: vi.fn(),
}),
}));
vi.mock("next/link", () => ({
default: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
<a href={String(href)} {...props}>
{children}
</a>
),
}));
vi.mock("next/dynamic", () => ({
default: () =>
function DynamicStub() {
return <div data-testid="dynamic-component" />;
},
}));
vi.mock("@/shared/components", () => ({
Card: ({ children }: { children: React.ReactNode }) => <section>{children}</section>,
CardSkeleton: () => <div data-testid="card-skeleton" />,
Button: ({
children,
loading: _loading,
fullWidth: _fullWidth,
variant: _variant,
size: _size,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
loading?: boolean;
fullWidth?: boolean;
variant?: string;
size?: string;
}) => <button {...props}>{children}</button>,
Modal: ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) =>
isOpen ? <div role="dialog">{children}</div> : null,
}));
vi.mock("@/shared/components/ProviderIcon", () => ({
default: () => <span data-testid="provider-icon" />,
}));
const notifyMock = {
success: vi.fn(),
error: vi.fn(),
addNotification: vi.fn(),
};
function useNotificationStoreMock() {
return notifyMock;
}
useNotificationStoreMock.getState = () => notifyMock;
vi.mock("@/store/notificationStore", () => ({
useNotificationStore: useNotificationStoreMock,
}));
vi.mock("@/shared/hooks/useElectron", () => ({
useIsElectron: () => false,
useOpenExternal: () => ({ openExternal: vi.fn() }),
}));
vi.mock("@/shared/utils/clipboard", () => ({
copyToClipboard: vi.fn(async () => undefined),
}));
const { default: HomePageClient } =
await import("../../../src/app/(dashboard)/dashboard/HomePageClient");
function jsonResponse(body: unknown) {
return {
ok: true,
json: async () => body,
} as Response;
}
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal(
"fetch",
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url === "/api/settings") {
return Promise.resolve(
jsonResponse({
showQuickStartOnHome: true,
showProviderTopologyOnHome: false,
})
);
}
if (url === "/api/providers") {
return Promise.resolve(jsonResponse({ connections: [] }));
}
if (url === "/api/models") {
return Promise.resolve(jsonResponse({ models: [] }));
}
if (url === "/api/system/version") {
return Promise.resolve(
jsonResponse({
current: "0.0.0-test",
latest: "0.0.0-test",
updateAvailable: false,
channel: "test",
autoUpdateSupported: false,
})
);
}
if (url === "/api/provider-nodes") {
return Promise.resolve(jsonResponse({ nodes: [] }));
}
return Promise.resolve(jsonResponse({}));
})
);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.unstubAllGlobals();
vi.clearAllMocks();
});
it("renders the dashboard home client without throwing an internal server error", async () => {
await act(async () => {
root.render(<HomePageClient machineId="test-machine" />);
});
expect(container.textContent).not.toContain("Internal Server Error");
expect(container.textContent).toContain("quickStart");
});
@@ -0,0 +1,66 @@
// @vitest-environment jsdom
//
// #4606: the provider-topology card was extracted into HomeProviderTopologySection
// and its activity fetch gated behind widget visibility in HomePageClient
// (`appearanceSettingsLoaded && showProviderTopologyOnHome`). This guards the
// extracted section: it renders the topology card and feeds live active-requests
// through selectActiveRequests into ProviderTopology (Rule #18 for the change).
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock("next/dynamic", () => ({
default: () => (props: Record<string, unknown>) => (
<div data-testid="provider-topology" data-providers={String((props.providers as unknown[])?.length ?? 0)} />
),
}));
vi.mock("@/shared/components", () => ({
Card: ({ children }: { children: React.ReactNode }) => <div data-testid="card">{children}</div>,
}));
const liveRequestsMock = vi.fn(() => ({ activeRequests: [] as unknown[] }));
vi.mock("@/hooks/useLiveDashboard", () => ({
useLiveRequests: () => liveRequestsMock(),
}));
const { HomeProviderTopologySection } = await import(
"../../../src/app/(dashboard)/dashboard/HomeProviderTopologySection"
);
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
vi.clearAllMocks();
});
it("renders the topology card and forwards providers to ProviderTopology", () => {
act(() => {
root.render(
<HomeProviderTopologySection
providers={[
{ id: "p1", provider: "openai", name: "OpenAI" },
{ id: "p2", provider: "anthropic", name: "Anthropic" },
]}
lastProvider="openai"
errorProvider=""
/>
);
});
expect(container.querySelector("[data-testid='card']")).not.toBeNull();
const topology = container.querySelector("[data-testid='provider-topology']");
expect(topology).not.toBeNull();
expect(topology?.getAttribute("data-providers")).toBe("2");
});
@@ -0,0 +1,68 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useLiveRequests } from "../../../src/hooks/useLiveDashboard";
function LiveRequestsHarness({ enabled }: { enabled: boolean }) {
useLiveRequests({ enabled });
return null;
}
describe("home topology hidden networking (#4596)", () => {
const websocketMock = vi.fn();
let root: ReturnType<typeof createRoot> | null = null;
let container: HTMLDivElement | null = null;
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
websocketMock.mockClear();
vi.stubGlobal(
"WebSocket",
class WebSocketMock {
static OPEN = 1;
readyState = 0;
onopen: (() => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onclose: (() => void) | null = null;
onerror: (() => void) | null = null;
constructor(url: string) {
websocketMock(url);
}
send() {}
close() {}
}
);
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => {
root?.unmount();
});
container?.remove();
root = null;
container = null;
vi.unstubAllGlobals();
});
it("does NOT open a WebSocket when the topology section is disabled", () => {
act(() => {
root!.render(<LiveRequestsHarness enabled={false} />);
});
expect(websocketMock).not.toHaveBeenCalled();
});
it("opens a WebSocket when the topology section is enabled", () => {
act(() => {
root!.render(<LiveRequestsHarness enabled={true} />);
});
expect(websocketMock).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,48 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
// Regression guard for #5991 — clicking "Update now" showed an "Internal Server
// Error" screen (Minified React error #31). The handler POSTs /api/system/version
// (a loopback-only auto-update endpoint) and, on a non-OK JSON response, did:
// notify.error(data.error || "Failed to start update.");
// OmniRoute's error envelope is `{ error: { code, message, correlation_id } }`, so
// `data.error` is an OBJECT. notify.error rendered that object as a React child →
// React #31 crash. The fix funnels the body through extractApiErrorMessage() (the
// same helper introduced in #5340) so a string always reaches the toast.
const here = dirname(fileURLToPath(import.meta.url));
const source = readFileSync(
resolve(here, "../../../src/app/(dashboard)/dashboard/HomePageClient.tsx"),
"utf8"
);
test("HomePageClient imports the safe API error extractor", () => {
assert.match(
source,
/import\s*\{\s*extractApiErrorMessage\s*\}\s*from\s*["']@\/shared\/http\/apiErrorMessage["']/,
"HomePageClient must import extractApiErrorMessage to render API errors safely (#5991)"
);
});
test("the update-error handler funnels the body through extractApiErrorMessage (#5991)", () => {
// The update failure path must extract a string, not hand the raw envelope object
// (which triggers React #31) to notify.error.
assert.match(
source,
/notify\.error\(\s*extractApiErrorMessage\(\s*data\s*,/,
"the update-error notify.error must use extractApiErrorMessage(data, …) (#5991)"
);
});
test("the update-error handler never passes the raw error object to notify.error (#5991)", () => {
// The pre-fix pattern `notify.error(data.error || …)` rendered an object as a React
// child. It must not come back.
assert.doesNotMatch(
source,
/notify\.error\(\s*data\.error\b/,
"notify.error(data.error …) renders the error envelope object as a React child → React #31 (#5991)"
);
});
@@ -0,0 +1,41 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
// Regression guard for the escalated mesh report "Nobody solve this color issue 🤫":
// the update-step status icon for the `warning` state used a bare `text-yellow-500`
// (Tailwind #eab308, no dark: variant) while its `done`/`failed` siblings use the
// vivid `text-green-500` / `text-red-500`. yellow-500 has poor contrast on light
// backgrounds (~1.9:1, fails WCAG) and is inconsistent with the project's warning
// convention, which is `amber` everywhere else in this same component (e.g. the
// `bg-amber-500/10 text-amber-500` badge). The warning icon must use amber, not yellow.
const here = dirname(fileURLToPath(import.meta.url));
const source = readFileSync(
resolve(here, "../../../src/app/(dashboard)/dashboard/HomePageClient.tsx"),
"utf8"
);
// Isolate the update-step status-icon block: the `warning` material symbol icon span.
// Match the className on the span immediately preceding the `warning` glyph.
const warningIconMatch = source.match(
/className="material-symbols-outlined ([^"]*)"[^>]*>\s*warning\s*</
);
test("update-step warning icon renders with an amber color, not bare yellow", () => {
assert.ok(
warningIconMatch,
"expected to find the update-step `warning` material-symbols icon span in HomePageClient.tsx"
);
const className = warningIconMatch![1];
assert.ok(
className.includes("text-amber-500"),
`warning icon should use the project's amber warning convention; got: "${className}"`
);
assert.ok(
!className.includes("text-yellow-500"),
"warning icon must not use the low-contrast bare `text-yellow-500` (no dark variant, fails WCAG on light backgrounds)"
);
});
+9
View File
@@ -0,0 +1,9 @@
// @vitest-environment jsdom
// tests/unit/ui/ionizerLane.test.tsx
import { describe, it, expect } from "vitest";
import { LANE_ENGINES } from "@/app/(dashboard)/dashboard/compression/studio/PlaygroundInput";
describe("studio lane engines", () => {
it("includes the ionizer engine so it has its own playground lane", () => {
expect(LANE_ENGINES).toContain("ionizer");
});
});
@@ -0,0 +1,76 @@
/**
* tests/unit/ui/live-compression-accumulate.test.ts
*
* F5.1 coverage gap (F3.3): the `useLiveCompression` accumulator (`accumulateRun`)
* — which folds incoming `compression.completed` payloads into the run list — was
* exported "for unit tests" but never tested. This pins ordering + the cap.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { accumulateRun } from "../../../src/hooks/useLiveCompression.ts";
import type { CompressionRunModel } from "../../../src/app/(dashboard)/dashboard/compression/studio/compressionFlowModel.ts";
import type { CompressionCompletedPayload } from "../../../src/lib/events/types.ts";
function payload(requestId: string): CompressionCompletedPayload {
return {
requestId,
comboId: "my-combo",
mode: "stacked",
originalTokens: 1000,
compressedTokens: 600,
savingsPercent: 40,
engineBreakdown: [
{
engine: "rtk",
originalTokens: 1000,
compressedTokens: 600,
savingsPercent: 40,
techniquesUsed: ["tool-output-trim"],
durationMs: 3,
},
],
timestamp: 1718000000000,
};
}
describe("accumulateRun — live compression run accumulator (F3.3)", () => {
it("adds the first run derived from the payload (requestId carried through)", () => {
const runs = accumulateRun([], payload("r1"));
assert.equal(runs.length, 1);
assert.equal(runs[0].requestId, "r1");
assert.equal(runs[0].mode, "stacked");
});
it("prepends so the list is most-recent-first", () => {
let runs: CompressionRunModel[] = [];
runs = accumulateRun(runs, payload("r1"));
runs = accumulateRun(runs, payload("r2"));
runs = accumulateRun(runs, payload("r3"));
assert.deepEqual(
runs.map((r) => r.requestId),
["r3", "r2", "r1"],
);
});
it("caps the list at maxRuns, dropping the oldest", () => {
let runs: CompressionRunModel[] = [];
for (const id of ["a", "b", "c", "d"]) {
runs = accumulateRun(runs, payload(id), /* maxRuns */ 2);
}
assert.equal(runs.length, 2, "never grows beyond maxRuns");
assert.deepEqual(
runs.map((r) => r.requestId),
["d", "c"],
"keeps the 2 newest, newest-first; oldest (a, b) dropped",
);
});
it("does not mutate the previous array (returns a new list)", () => {
const prev = accumulateRun([], payload("r1"));
const next = accumulateRun(prev, payload("r2"));
assert.equal(prev.length, 1, "input array is left untouched");
assert.notEqual(prev, next);
});
});
+27
View File
@@ -0,0 +1,27 @@
// @vitest-environment jsdom
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import { act } from "react";
vi.mock("@/hooks/useLiveCompression", () => ({
useLiveCompression: () => ({ runs: [], lastRun: null, getRunById: () => undefined, isConnected: false, reconnect: () => {} }),
}));
vi.mock("@xyflow/react", async () => {
const actual = await vi.importActual<any>("@xyflow/react");
return { ...actual, Handle: () => null };
});
let container: HTMLElement; let root: Root;
beforeEach(() => {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
(globalThis as any).ResizeObserver ||= class { observe(){} unobserve(){} disconnect(){} };
container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container);
});
afterEach(() => { act(() => root.unmount()); container.remove(); document.body.innerHTML = ""; vi.restoreAllMocks(); });
describe("live page", () => {
it("renders the WS cockpit (empty state when no traffic)", async () => {
const { default: LivePage } = await import("@/app/(dashboard)/dashboard/compression/live/page");
await act(async () => { root.render(<LivePage />); });
expect(container.querySelector('[data-testid="compression-cockpit-empty"]')).toBeTruthy();
});
});
+153
View File
@@ -0,0 +1,153 @@
// @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 ─────────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// ── Import component at module level (after mocks) ────────────────────────────
const { default: MarkdownMessage } = await import(
"../../../src/app/(dashboard)/dashboard/playground/components/MarkdownMessage"
);
// ── Helpers ───────────────────────────────────────────────────────────────────
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderMarkdown(content: string, className?: string): HTMLDivElement {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(<MarkdownMessage content={content} className={className} />);
});
containers.push({ root, el });
return el;
}
async function waitForCondition(fn: () => boolean, timeoutMs = 3000): Promise<void> {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitForCondition timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("MarkdownMessage", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean })
.IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
it("renders a code block as <pre><code>", async () => {
const el = renderMarkdown("```js\nconsole.log(1)\n```");
// Wait for the component to render
await waitForCondition(() => el.innerHTML.length > 0);
const pre = el.querySelector("pre");
const code = el.querySelector("code");
expect(pre || code).toBeTruthy();
expect(el.innerHTML).toContain("console.log");
});
it("renders a markdown table as <table>", async () => {
const tableMarkdown = `
| Name | Age |
|------|-----|
| Alice | 30 |
| Bob | 25 |
`;
const el = renderMarkdown(tableMarkdown);
await waitForCondition(() => el.innerHTML.length > 0);
const table = el.querySelector("table");
expect(table).toBeTruthy();
const cells = el.querySelectorAll("td");
expect(cells.length).toBeGreaterThan(0);
});
it("renders a markdown list as <ul><li>", async () => {
const listMarkdown = `
- Item one
- Item two
- Item three
`;
const el = renderMarkdown(listMarkdown);
await waitForCondition(() => el.querySelector("ul") !== null);
const ul = el.querySelector("ul");
expect(ul).toBeTruthy();
const listItems = el.querySelectorAll("li");
expect(listItems.length).toBe(3);
});
it("renders a markdown link as <a href>", async () => {
const el = renderMarkdown("[Click here](https://example.com)");
await waitForCondition(() => el.querySelector("a") !== null);
const anchor = el.querySelector("a");
expect(anchor).toBeTruthy();
expect(anchor?.getAttribute("href")).toBe("https://example.com");
});
it("security: <script> tags appear as literal text, not executed", () => {
const xssContent = "<script>alert(1)</script>";
const el = renderMarkdown(xssContent);
// Should NOT contain a <script> element in the DOM
const scriptEl = el.querySelector("script");
expect(scriptEl).toBeNull();
// react-markdown by default does not render raw HTML
// The content should not create a <script> tag
const innerHTML = el.innerHTML;
expect(innerHTML).not.toContain("<script>");
});
it("renders plain text content", async () => {
const el = renderMarkdown("Hello, world!");
await waitForCondition(() => el.textContent !== null && el.textContent.includes("Hello"));
expect(el.textContent).toContain("Hello, world!");
});
it("accepts optional className prop", () => {
const el = renderMarkdown("Text", "my-custom-class");
// The wrapper div should have the class
const wrapper = el.firstElementChild;
expect(wrapper).toBeTruthy();
expect(wrapper?.classList.contains("my-custom-class")).toBe(true);
});
it("renders strong and emphasis formatting", async () => {
const el = renderMarkdown("**bold text** and _italic text_");
await waitForCondition(
() => el.querySelector("strong") !== null || el.querySelector("em") !== null,
);
const strong = el.querySelector("strong");
const em = el.querySelector("em");
expect(strong).toBeTruthy();
expect(em).toBeTruthy();
});
});
+341
View File
@@ -0,0 +1,341 @@
// @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";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
if (values) return `${key}:${JSON.stringify(values)}`;
return key;
},
}));
// Mock shared components
vi.mock("@/shared/components", () => ({
Card: ({ children }: { children: React.ReactNode }) =>
React.createElement("div", { className: "card" }, children),
Badge: ({ children, variant, title }: { children: React.ReactNode; variant?: string; title?: string }) =>
React.createElement("span", { "data-variant": variant, title }, children),
Button: ({
children,
onClick,
disabled,
loading,
variant,
size,
"data-testid": testId,
}: {
children?: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
variant?: string;
size?: string;
"data-testid"?: string;
}) =>
React.createElement(
"button",
{
onClick,
disabled: disabled || loading,
"data-variant": variant,
"data-testid": testId,
},
children,
),
Input: ({
value,
onChange,
placeholder,
"data-testid": testId,
className,
onKeyDown,
}: {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
"data-testid"?: string;
className?: string;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}) =>
React.createElement("input", {
value,
onChange,
placeholder,
"data-testid": testId,
className,
onKeyDown,
}),
Select: ({
children,
value,
onChange,
}: {
children: React.ReactNode;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
}) => React.createElement("select", { value, onChange }, children),
Modal: ({
isOpen,
title,
children,
footer,
onClose,
}: {
isOpen?: boolean;
title?: string;
children?: React.ReactNode;
footer?: React.ReactNode;
onClose?: () => void;
}) =>
isOpen
? React.createElement(
"div",
{ "data-testid": "modal", "data-title": title },
React.createElement("button", { onClick: onClose, "data-testid": "modal-close" }, "X"),
children,
footer,
)
: null,
}));
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/EditMemoryModal",
() => ({
default: ({
isOpen,
onClose,
}: {
isOpen: boolean;
memory: unknown;
onClose: () => void;
onSaved: () => void;
}) =>
isOpen
? React.createElement("div", { "data-testid": "edit-memory-modal" }, [
React.createElement("button", { key: "close", onClick: onClose }, "close"),
])
: null,
}),
);
const MOCK_MEMORIES = [
{
id: "mem-1",
apiKeyId: "key-1",
sessionId: null,
type: "factual",
key: "user.name",
content: "Alice",
metadata: {},
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
expiresAt: null,
},
{
id: "mem-2",
apiKeyId: "key-1",
sessionId: null,
type: "episodic",
key: "event.meeting",
content: "Had a meeting",
metadata: {},
createdAt: "2026-01-02T00:00:00Z",
updatedAt: "2026-01-02T00:00:00Z",
expiresAt: null,
},
];
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("MemoriesTab", () => {
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: MOCK_MEMORIES,
total: 2,
totalPages: 1,
stats: {
total: 2,
tokensUsed: 150,
hitRate: 0.75,
cacheStats: { hits: 3, misses: 1 },
},
}),
});
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders memories after fetch", async () => {
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoriesTab />);
});
// Wait for the 300ms debounce
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
expect(container.textContent).toContain("user.name");
expect(container.textContent).toContain("Alice");
});
it("shows hit rate card when cacheStats.hits + misses > 0", async () => {
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoriesTab />);
});
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
// hitRate is shown since cacheStats.hits=3, misses=1
expect(container.textContent).toContain("hitRate");
});
it("does not show hit rate when cacheStats is 0/0", async () => {
vi.resetModules();
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: MOCK_MEMORIES,
total: 2,
totalPages: 1,
stats: {
total: 2,
tokensUsed: 0,
hitRate: 0,
cacheStats: { hits: 0, misses: 0 },
},
}),
});
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoriesTab />);
});
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
// hitRate card should NOT appear
expect(container.querySelector("[data-testid='hit-rate-card']")).toBeNull();
});
it("opens edit modal when pencil button is clicked", async () => {
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoriesTab />);
});
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
const editBtn = container.querySelector("[data-testid='edit-memory-mem-1']") as HTMLButtonElement | null;
expect(editBtn).toBeTruthy();
await act(async () => {
editBtn?.click();
});
expect(container.querySelector("[data-testid='edit-memory-modal']")).toBeTruthy();
});
it("shows empty state when no memories returned", async () => {
vi.resetModules();
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
data: [],
total: 0,
totalPages: 1,
stats: { total: 0, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
}),
});
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoriesTab />);
});
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
expect(container.querySelector("[data-testid='memories-empty-state']")).toBeTruthy();
});
it("calls DELETE when delete confirmed", async () => {
const mockFetch = vi.fn();
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
data: MOCK_MEMORIES,
total: 2,
totalPages: 1,
stats: { total: 2, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
}),
});
mockFetch.mockResolvedValue({ ok: true, json: async () => ({}) });
globalThis.fetch = mockFetch;
const { default: MemoriesTab } = await import(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoriesTab />);
});
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
const deleteBtn = container.querySelector("[data-testid='delete-memory-mem-1']") as HTMLButtonElement | null;
expect(deleteBtn).toBeTruthy();
await act(async () => {
deleteBtn?.click();
});
// Confirm modal shown
const modal = container.querySelector("[data-testid='modal']");
expect(modal).toBeTruthy();
// Find danger button inside modal
const dangerBtns = Array.from(container.querySelectorAll("button")).filter(
(b) => b.getAttribute("data-variant") === "danger",
);
expect(dangerBtns.length).toBeGreaterThan(0);
await act(async () => {
dangerBtns[0].click();
});
// DELETE should have been called
const deleteCalls = (mockFetch as ReturnType<typeof vi.fn>).mock.calls.filter(
(c: [string, ...unknown[]]) =>
typeof c[0] === "string" && c[0].includes("mem-1") && c[1] && (c[1] as { method: string }).method === "DELETE",
);
expect(deleteCalls.length).toBeGreaterThan(0);
});
});
+156
View File
@@ -0,0 +1,156 @@
// @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";
// Mutable state for controlling what tab the navigation mock returns
let mockTabValue: string | null = null;
vi.mock("next/navigation", () => ({
useSearchParams: () => ({
get: (key: string) => (key === "tab" ? mockTabValue : null),
toString: () => (mockTabValue ? `tab=${mockTabValue}` : ""),
}),
useRouter: () => ({
replace: vi.fn(),
}),
}));
vi.mock("next-intl", () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
if (values) {
return `${key}:${JSON.stringify(values)}`;
}
return key;
},
getTranslations: () => async (key: string) => key,
}));
vi.mock("swr", () => ({
default: () => ({ data: null, error: null, isLoading: false, mutate: vi.fn() }),
}));
// Mock all child tab components + concept card
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/MemoryConceptCard",
() => ({
default: () => React.createElement("div", { "data-testid": "concept-card" }, "ConceptCard"),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/MemoriesTab",
() => ({
default: () =>
React.createElement("div", { "data-testid": "memories-tab-content" }, "MemoriesTab"),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/PlaygroundTab",
() => ({
default: () =>
React.createElement("div", { "data-testid": "playground-tab-content" }, "PlaygroundTab"),
}),
);
vi.mock(
"../../../src/app/(dashboard)/dashboard/memory/components/tabs/EngineTab",
() => ({
default: () =>
React.createElement("div", { "data-testid": "engine-tab-content" }, "EngineTab"),
}),
);
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("MemoryPage", () => {
beforeEach(() => {
mockTabValue = null;
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders the concept card", async () => {
const { default: MemoryPage } = await import(
"../../../src/app/(dashboard)/dashboard/memory/page"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoryPage />);
});
expect(container.querySelector("[data-testid='concept-card']")).toBeTruthy();
});
it("renders 3 tab buttons", async () => {
const { default: MemoryPage } = await import(
"../../../src/app/(dashboard)/dashboard/memory/page"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoryPage />);
});
const tabs = ["memories", "playground", "engine"];
for (const tab of tabs) {
expect(container.querySelector(`[data-testid='tab-${tab}']`)).toBeTruthy();
}
});
it("defaults to memories tab (tab=null)", async () => {
mockTabValue = null;
const { default: MemoryPage } = await import(
"../../../src/app/(dashboard)/dashboard/memory/page"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoryPage />);
});
expect(container.querySelector("[data-testid='memories-tab-content']")).toBeTruthy();
expect(container.querySelector("[data-testid='playground-tab-content']")).toBeNull();
expect(container.querySelector("[data-testid='engine-tab-content']")).toBeNull();
});
it("shows playground tab when ?tab=playground", async () => {
mockTabValue = "playground";
const { default: MemoryPage } = await import(
"../../../src/app/(dashboard)/dashboard/memory/page"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoryPage />);
});
expect(container.querySelector("[data-testid='playground-tab-content']")).toBeTruthy();
expect(container.querySelector("[data-testid='memories-tab-content']")).toBeNull();
});
it("shows engine tab when ?tab=engine", async () => {
mockTabValue = "engine";
const { default: MemoryPage } = await import(
"../../../src/app/(dashboard)/dashboard/memory/page"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<MemoryPage />);
});
expect(container.querySelector("[data-testid='engine-tab-content']")).toBeTruthy();
});
});
@@ -0,0 +1,68 @@
// @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";
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("MitmProxyError boundary", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders a recoverable fallback instead of throwing", async () => {
const { default: MitmProxyError } =
await import("../../../src/app/(dashboard)/dashboard/system/mitm-proxy/error");
const container = makeContainer();
const root = createRoot(container);
const reset = vi.fn();
const error = Object.assign(new Error("boom"), { digest: "abc123" });
await act(async () => {
root.render(<MitmProxyError error={error} reset={reset} />);
});
expect(container.querySelector("[role='alert']")).toBeTruthy();
expect(container.textContent).toContain("Failed to load MITM proxy");
});
it("calls reset() exactly once when Try Again is clicked", async () => {
const { default: MitmProxyError } =
await import("../../../src/app/(dashboard)/dashboard/system/mitm-proxy/error");
const container = makeContainer();
const root = createRoot(container);
const reset = vi.fn();
const error = Object.assign(new Error("boom"), { digest: undefined });
await act(async () => {
root.render(<MitmProxyError error={error} reset={reset} />);
});
const button = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent === "Try Again"
) as HTMLButtonElement | undefined;
expect(button).toBeTruthy();
await act(async () => {
button?.click();
});
expect(reset).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,121 @@
/**
* Tests for the MITM Proxy "page moved" banner (C4).
* Validates:
* - Banner renders with pageMoved.title text
* - "Go now" button triggers router.replace
* - Auto-redirect is set up via setTimeout
*/
// @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";
const mockReplace = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: mockReplace }),
}));
vi.mock("next-intl", () => ({
useTranslations: (ns: string) => (key: string) => `${ns}.${key}`,
}));
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
describe("MitmProxyMovedPage — page-moved banner (C4)", { timeout: 30000 }, () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.useFakeTimers();
mockReplace.mockClear();
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
vi.useRealTimers();
vi.clearAllMocks();
});
it("renders with pageMoved.title text", async () => {
const { default: MitmProxyMovedPage } = await import(
"../../../src/app/(dashboard)/dashboard/system/mitm-proxy/page"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(MitmProxyMovedPage));
});
expect(container.textContent).toContain("agentBridge.pageMoved.title");
});
it("renders pageMoved.message text", async () => {
const { default: MitmProxyMovedPage } = await import(
"../../../src/app/(dashboard)/dashboard/system/mitm-proxy/page"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(MitmProxyMovedPage));
});
expect(container.textContent).toContain("agentBridge.pageMoved.message");
});
it("clicking goNow button calls router.replace with agent-bridge path", async () => {
const { default: MitmProxyMovedPage } = await import(
"../../../src/app/(dashboard)/dashboard/system/mitm-proxy/page"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(MitmProxyMovedPage));
});
const goNowBtn = Array.from(container.querySelectorAll("button")).find((b) =>
b.textContent?.includes("agentBridge.pageMoved.goNow")
);
expect(goNowBtn).not.toBeNull();
await act(async () => {
goNowBtn?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(mockReplace).toHaveBeenCalledWith("/dashboard/tools/agent-bridge");
});
it("auto-redirect fires after 2500ms via setTimeout", async () => {
const { default: MitmProxyMovedPage } = await import(
"../../../src/app/(dashboard)/dashboard/system/mitm-proxy/page"
);
const container = makeContainer();
await act(async () => {
const root = createRoot(container);
root.render(React.createElement(MitmProxyMovedPage));
});
// Not yet redirected
expect(mockReplace).not.toHaveBeenCalled();
// Advance timers past the 2500ms threshold
await act(async () => {
vi.advanceTimersByTime(2600);
});
expect(mockReplace).toHaveBeenCalledWith("/dashboard/tools/agent-bridge");
});
});
+273
View File
@@ -0,0 +1,273 @@
// @vitest-environment jsdom
/**
* TDD tests for click-to-edit model alias inline edit affordance.
* Ported from decolua/9router#2130.
*
* Covers PassthroughModelRow and ModelRow:
* 1) Clicking the alias span enters edit mode (shows an input).
* 2) Typing a new value and pressing Enter calls onSetAlias with the new value.
* 3) Pressing Escape cancels without calling onSetAlias.
* 4) Blur (losing focus) submits like Enter.
*/
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
if (values) {
return Object.entries(values).reduce(
(acc, [k, v]) => acc.replace(`{${k}}`, String(v)),
key
);
}
return key;
},
}));
vi.mock("@/shared/components", () => ({
Badge: ({ children }: any) => <span>{children}</span>,
Button: ({ children, onClick }: any) => <button onClick={onClick}>{children}</button>,
}));
vi.mock(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/ModelCompatPopover",
() => ({ default: () => null })
);
vi.mock("@/shared/utils/modelCatalogSearch", () => ({
getModelCatalogSourceLabel: () => "system",
normalizeModelCatalogSource: () => "system",
}));
const { default: PassthroughModelRow } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelRow"
);
const { default: ModelRow } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow"
);
const t = (key: string, values?: Record<string, unknown>) => {
if (values) {
return Object.entries(values).reduce(
(acc, [k, v]) => acc.replace(`{${k}}`, String(v)),
key
);
}
return key;
};
const noop = async () => {};
const noopSync = () => {};
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function mountPassthrough(props: Record<string, unknown> = {}) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<PassthroughModelRow
modelId="gpt-4o"
fullModel="openai/gpt-4o"
alias={null}
onCopy={noopSync}
onSetAlias={noopSync}
t={t}
effectiveModelNormalize={() => false}
effectiveModelPreserveDeveloper={() => false}
saveModelCompatFlags={noopSync}
getUpstreamHeadersRecord={() => ({})}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
function mountModelRow(props: Record<string, unknown> = {}) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<ModelRow
model={{ id: "gpt-4o", name: "GPT-4o" }}
fullModel="openai/gpt-4o"
provider="openai"
alias={undefined}
onCopy={noopSync}
onSetAlias={noopSync}
t={t}
effectiveModelNormalize={() => false}
effectiveModelPreserveDeveloper={() => false}
saveModelCompatFlags={noopSync}
getUpstreamHeadersRecord={() => ({})}
{...(props as any)}
/>
);
});
containers.push({ root, el });
return el;
}
beforeEach(() => {});
afterEach(() => {
for (const { root, el } of containers) {
act(() => root.unmount());
el.remove();
}
containers.length = 0;
});
// ---------------------------------------------------------------------------
// PassthroughModelRow inline-edit tests
// ---------------------------------------------------------------------------
describe("PassthroughModelRow — inline alias edit", () => {
it("clicking the alias span enters edit mode (shows input)", () => {
const el = mountPassthrough({ alias: "my-alias" });
const span = el.querySelector("span.cursor-pointer");
expect(span).toBeTruthy();
expect(el.querySelector("input")).toBeNull();
act(() => {
span!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(el.querySelector("input")).toBeTruthy();
});
it("Enter key calls onSetAlias with the new value", () => {
const onSetAlias = vi.fn();
const el = mountPassthrough({ alias: "old-alias", onSetAlias });
const span = el.querySelector("span.cursor-pointer");
act(() => {
span!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const input = el.querySelector("input") as HTMLInputElement;
expect(input).toBeTruthy();
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(input, "new-alias");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
});
expect(onSetAlias).toHaveBeenCalledWith("new-alias");
});
it("Escape cancels without calling onSetAlias", () => {
const onSetAlias = vi.fn();
const el = mountPassthrough({ alias: "old-alias", onSetAlias });
const span = el.querySelector("span.cursor-pointer");
act(() => {
span!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const input = el.querySelector("input") as HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(input, "something-else");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
});
expect(onSetAlias).not.toHaveBeenCalled();
expect(el.querySelector("input")).toBeNull();
});
it("shows placeholder text when no alias is set", () => {
const el = mountPassthrough({ alias: null });
const span = el.querySelector("span.cursor-pointer");
// providerText uses fallback "Click to set alias" when key not found via t()
expect(span?.textContent).toBeTruthy();
expect(span?.textContent).not.toBe("");
});
it("shows alias value when alias is set", () => {
const el = mountPassthrough({ alias: "my-alias" });
const span = el.querySelector("span.cursor-pointer");
expect(span?.textContent).toContain("my-alias");
});
});
// ---------------------------------------------------------------------------
// ModelRow inline-edit tests
// ---------------------------------------------------------------------------
describe("ModelRow — inline alias edit", () => {
it("clicking the alias span enters edit mode (shows input)", () => {
const el = mountModelRow({ alias: "my-alias" });
const span = el.querySelector("span.cursor-pointer");
expect(span).toBeTruthy();
expect(el.querySelector("input[type=text]")).toBeNull();
act(() => {
span!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(el.querySelector("input[type=text]")).toBeTruthy();
});
it("Enter key calls onSetAlias with the new value", () => {
const onSetAlias = vi.fn();
const el = mountModelRow({ alias: "old-alias", onSetAlias });
const span = el.querySelector("span.cursor-pointer");
act(() => {
span!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const input = el.querySelector("input[type=text]") as HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(input, "new-alias");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
});
expect(onSetAlias).toHaveBeenCalledWith("new-alias");
});
it("Escape cancels without calling onSetAlias", () => {
const onSetAlias = vi.fn();
const el = mountModelRow({ alias: "old-alias", onSetAlias });
const span = el.querySelector("span.cursor-pointer");
act(() => {
span!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const input = el.querySelector("input[type=text]") as HTMLInputElement;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(input, "something-else");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
});
expect(onSetAlias).not.toHaveBeenCalled();
expect(el.querySelector("input[type=text]")).toBeNull();
});
});
@@ -0,0 +1,202 @@
// @vitest-environment jsdom
//
// Port of decolua/9router PR #889 (Fajar Hidayat <fajarhide@gmail.com>):
// "Add model deselection functionality in ComboFormModal and ComboDetailPage".
//
// Upstream UX: clicking an already-added (highlighted) model in ModelSelectModal
// should TOGGLE — invoke onDeselect instead of onSelect — and the modal must stay
// open when keepOpenOnSelect so the user can add/remove several models in one
// session. OmniRoute already had the visual highlight (addedModelValues) but no
// deselect callback nor an opt-out for the auto-close — added here.
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: ModelSelectModal } = await import("@/shared/components/ModelSelectModal");
const containers: HTMLElement[] = [];
async function renderModal(props: Partial<React.ComponentProps<typeof ModelSelectModal>> = {}) {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
await act(async () => {
root.render(
<ModelSelectModal
isOpen={true}
onClose={() => {}}
onSelect={() => {}}
showCombos={false}
activeProviders={[{ provider: "openai" }]}
{...props}
/>
);
});
await act(async () => {});
return { container, root };
}
function findModelButton(container: HTMLElement, label: string): HTMLButtonElement | null {
const buttons = Array.from(container.querySelectorAll("button"));
return (buttons.find((b) => (b.textContent || "").includes(label)) as HTMLButtonElement) || null;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
if (url === "/api/provider-nodes") {
return { ok: true, json: async () => ({ nodes: [] }) };
}
if (url === "/api/provider-models") {
return { ok: true, json: async () => ({ models: {} }) };
}
if (url === "/api/combos") {
return { ok: true, json: async () => ({ combos: [] }) };
}
return { ok: true, json: async () => ({}) };
})
);
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
describe("ModelSelectModal — deselect / toggle behavior (upstream PR #889)", () => {
it("calls onDeselect (not onSelect) when clicking a model already in addedModelValues", async () => {
const onSelect = vi.fn();
const onDeselect = vi.fn();
// We don't know exactly which OpenAI model labels the system catalog ships at
// any given time, so probe the rendered DOM for one and use its value.
const { container } = await renderModal({
onSelect,
onDeselect,
addedModelValues: [],
});
// Find the first OpenAI model rendered (it must exist — openai is an active provider).
const firstModelButton = container.querySelector(
"button[class*='hover:border-primary']"
) as HTMLButtonElement | null;
expect(firstModelButton, "expected at least one openai model button to render").not.toBeNull();
const modelName = (firstModelButton!.textContent || "").trim();
expect(modelName.length).toBeGreaterThan(0);
// First click — model is NOT yet added → must call onSelect, not onDeselect.
await act(async () => {
firstModelButton!.click();
});
expect(onSelect).toHaveBeenCalledTimes(1);
expect(onDeselect).not.toHaveBeenCalled();
// The handler we got back from onSelect should be a model object with .value.
const selectedArg = onSelect.mock.calls[0][0];
expect(selectedArg).toMatchObject({ value: expect.any(String) });
});
it("toggles to onDeselect when the model is already in addedModelValues", async () => {
const onSelect = vi.fn();
const onDeselect = vi.fn();
// Render once to discover the model value
const probe = await renderModal({ onSelect: vi.fn(), onDeselect: vi.fn() });
const probeButton = probe.container.querySelector(
"button[class*='hover:border-primary']"
) as HTMLButtonElement | null;
expect(probeButton).not.toBeNull();
// The on-click handler embeds the model value; trigger once to capture it.
const tempSelect = vi.fn();
const probe2 = await renderModal({ onSelect: tempSelect, addedModelValues: [] });
const probeButton2 = probe2.container.querySelector(
"button[class*='hover:border-primary']"
) as HTMLButtonElement | null;
await act(async () => {
probeButton2!.click();
});
const capturedValue = tempSelect.mock.calls[0][0].value as string;
expect(typeof capturedValue).toBe("string");
expect(capturedValue.length).toBeGreaterThan(0);
// Now render with that value pre-added and click again — must invoke onDeselect.
const { container } = await renderModal({
onSelect,
onDeselect,
addedModelValues: [capturedValue],
keepOpenOnSelect: true,
});
// The already-added model now renders with the emerald/added class — look for ✓.
const addedButton = Array.from(container.querySelectorAll("button")).find((b) =>
(b.textContent || "").includes("✓")
) as HTMLButtonElement | undefined;
expect(addedButton, "expected an added (✓) button to render").toBeDefined();
await act(async () => {
addedButton!.click();
});
expect(onDeselect).toHaveBeenCalledTimes(1);
expect(onSelect).not.toHaveBeenCalled();
expect(onDeselect.mock.calls[0][0]).toMatchObject({ value: capturedValue });
});
it("does NOT auto-close the modal when keepOpenOnSelect is true", async () => {
const onSelect = vi.fn();
const onClose = vi.fn();
const { container } = await renderModal({
onSelect,
onClose,
keepOpenOnSelect: true,
});
const firstModelButton = container.querySelector(
"button[class*='hover:border-primary']"
) as HTMLButtonElement | null;
expect(firstModelButton).not.toBeNull();
await act(async () => {
firstModelButton!.click();
});
expect(onSelect).toHaveBeenCalledTimes(1);
expect(onClose).not.toHaveBeenCalled();
});
it("still auto-closes by default (keepOpenOnSelect defaults to false, backward-compat)", async () => {
const onSelect = vi.fn();
const onClose = vi.fn();
const { container } = await renderModal({ onSelect, onClose });
const firstModelButton = container.querySelector(
"button[class*='hover:border-primary']"
) as HTMLButtonElement | null;
expect(firstModelButton).not.toBeNull();
await act(async () => {
firstModelButton!.click();
});
expect(onSelect).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,119 @@
// @vitest-environment jsdom
//
// Regression coverage for the "keepOpenOnSelect" prop added in feat/port-pr-1031.
// Mirrors the UX of upstream PR decolua/9router#1031: when a caller (e.g. combo
// creation) opts out of the auto-close-on-select behaviour, the modal renders a
// "Done" button in the footer so the user has a clear way to confirm they are
// finished adding entries.
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: ModelSelectModal } = await import("@/shared/components/ModelSelectModal");
const containers: HTMLElement[] = [];
async function renderModal(props: Partial<React.ComponentProps<typeof ModelSelectModal>> = {}) {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
await act(async () => {
root.render(
<ModelSelectModal
isOpen={true}
onClose={() => {}}
onSelect={() => {}}
showCombos={false}
activeProviders={[]}
{...props}
/>
);
});
await act(async () => {});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
if (url === "/api/provider-nodes") {
return { ok: true, json: async () => ({ nodes: [] }) };
}
if (url === "/api/provider-models") {
return { ok: true, json: async () => ({ models: {} }) };
}
if (url === "/api/combos") {
return { ok: true, json: async () => ({ combos: [] }) };
}
return { ok: true, json: async () => ({}) };
})
);
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
describe("ModelSelectModal keepOpenOnSelect", () => {
it("does not render the Done button by default (auto-close behaviour preserved)", async () => {
const container = await renderModal();
const doneButton = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "done"
);
expect(doneButton).toBeUndefined();
});
it("renders the Done button when keepOpenOnSelect is true", async () => {
const container = await renderModal({ keepOpenOnSelect: true });
const doneButton = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "done"
);
expect(doneButton).toBeDefined();
});
it("clicking Done triggers onClose without invoking onSelect again", async () => {
const onClose = vi.fn();
const onSelect = vi.fn();
const container = await renderModal({ keepOpenOnSelect: true, onClose, onSelect });
const doneButton = Array.from(container.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "done"
);
expect(doneButton).toBeDefined();
await act(async () => {
doneButton!.click();
});
expect(onClose).toHaveBeenCalledTimes(1);
expect(onSelect).not.toHaveBeenCalled();
});
it("does not render the Done button when multiSelect is true (multiSelect owns its own footer)", async () => {
// multiSelect already ships a Clear + Done footer driven by selectedModels.
// keepOpenOnSelect must defer to it to avoid two competing Done buttons.
const container = await renderModal({ keepOpenOnSelect: true, multiSelect: true });
const doneButtons = Array.from(container.querySelectorAll("button")).filter(
(b) => b.textContent?.trim() === "done"
);
// Exactly one Done button — the one inside the multiSelect footer, not a duplicate.
expect(doneButtons.length).toBe(1);
});
});
@@ -0,0 +1,88 @@
// @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";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
const { default: ModelSelectModal } = await import("@/shared/components/ModelSelectModal");
const containers: HTMLElement[] = [];
async function renderModal(props: Partial<React.ComponentProps<typeof ModelSelectModal>> = {}) {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
await act(async () => {
root.render(
<ModelSelectModal
isOpen={true}
onClose={() => {}}
onSelect={() => {}}
showCombos={false}
activeProviders={[]}
{...props}
/>
);
});
await act(async () => {});
return container;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.stubGlobal(
"fetch",
vi.fn(async (url: string) => {
if (url === "/api/provider-nodes") {
return { ok: true, json: async () => ({ nodes: [] }) };
}
if (url === "/api/provider-models") {
return { ok: true, json: async () => ({ models: {} }) };
}
if (url === "/api/combos") {
return { ok: true, json: async () => ({ combos: [] }) };
}
return { ok: true, json: async () => ({}) };
})
);
});
afterEach(() => {
while (containers.length > 0) {
containers.pop()?.remove();
}
document.body.innerHTML = "";
vi.unstubAllGlobals();
});
describe("ModelSelectModal zero-config providers", () => {
it("shows OpenCode Free models when explicitly included without an active connection", async () => {
const container = await renderModal({ alwaysIncludeProviders: ["opencode"] });
expect(container.textContent).toContain("OpenCode Free");
expect(container.textContent).toContain("Big Pickle");
});
it("does not show OpenCode Free by default without an active connection", async () => {
const container = await renderModal();
expect(container.textContent).not.toContain("OpenCode Free");
expect(container.textContent).not.toContain("Big Pickle");
});
it("treats null explicit provider lists as empty", async () => {
const container = await renderModal({ alwaysIncludeProviders: null });
expect(container.textContent).not.toContain("OpenCode Free");
expect(container.textContent).not.toContain("Big Pickle");
});
});
@@ -0,0 +1,86 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, afterEach } from "vitest";
const { ModelVisibilityToolbar } = await import(
"../../../src/app/(dashboard)/dashboard/providers/[id]/components/ModelRow"
);
// providerText() falls back to the English string when t.has(key) is false.
const t: any = Object.assign((k: string) => k, { has: () => false });
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function render(extra: Record<string, unknown>) {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<ModelVisibilityToolbar
t={t}
filterValue=""
onFilterChange={() => {}}
activeCount={0}
totalCount={0}
onSelectAll={() => {}}
onDeselectAll={() => {}}
{...(extra as any)}
/>
);
});
containers.push({ root, el });
return el;
}
const byText = (el: HTMLElement, text: string) =>
Array.from(el.querySelectorAll("button")).find((b) => b.textContent?.trim() === text);
const byTextIncludes = (el: HTMLElement, text: string) =>
Array.from(el.querySelectorAll("button")).find((b) => b.textContent?.includes(text));
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
});
describe("ModelVisibilityToolbar — free filter & sort", () => {
it("does not render free-filter or sort controls when their props are absent", () => {
const el = render({});
expect(byText(el, "Free only")).toBeUndefined();
expect(byTextIncludes(el, "Free first")).toBeUndefined();
});
it("renders All / Free only / Paid only when freeFilter props are provided", () => {
const el = render({ freeFilter: "all", onFreeFilterChange: () => {} });
expect(byText(el, "Free only")).toBeTruthy();
expect(byText(el, "Paid only")).toBeTruthy();
});
it("fires onFreeFilterChange with the chosen filter", () => {
const onFreeFilterChange = vi.fn();
const el = render({ freeFilter: "all", onFreeFilterChange });
act(() => {
byText(el, "Free only")!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onFreeFilterChange).toHaveBeenCalledWith("free");
act(() => {
byText(el, "Paid only")!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onFreeFilterChange).toHaveBeenCalledWith("paid");
});
it("toggles sort free-first", () => {
const onSortFreeFirstChange = vi.fn();
const el = render({ sortFreeFirst: false, onSortFreeFirstChange });
const btn = byTextIncludes(el, "Free first")!;
expect(btn.getAttribute("aria-pressed")).toBe("false");
act(() => {
btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onSortFreeFirstChange).toHaveBeenCalledWith(true);
});
});
@@ -0,0 +1,95 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
const containers: HTMLElement[] = [];
const roots: Array<{ unmount: () => void }> = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(ui);
});
return container;
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(async () => {
vi.restoreAllMocks();
await act(async () => {
while (roots.length > 0) roots.pop()?.unmount();
});
for (let i = 0; i < 10; i++) await Promise.resolve();
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
});
async function flush() {
await act(async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
});
}
function combo(id: string, name: string) {
return {
id,
name,
description: `${name} desc`,
pipeline: [{ engine: "rtk", intensity: "standard" }],
languagePacks: ["en"],
outputMode: false,
outputModeIntensity: "full",
isDefault: false,
};
}
function setupFetchMock() {
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
const combos = [combo("c1", "Alpha"), combo("c2", "Bravo")];
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input.toString();
void init;
if (url.includes("/api/context/combos/") && url.includes("/assignments")) return json({ assignments: [] });
if (url.includes("/api/context/combos")) return json({ combos });
if (url.includes("/api/combos")) return json({ combos: [] });
if (url.includes("/api/compression/language-packs")) return json({ packs: [] });
if (url.includes("/api/settings/compression")) return json({ activeComboId: "c2", enabled: true });
return json({}, 404);
});
}
describe("NamedCombosManager — active badge, no set-as-default", () => {
async function render() {
const { default: CompressionCombosPageClient } = await import(
"../../../src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient"
);
let container!: HTMLElement;
await act(async () => {
container = mount(<CompressionCombosPageClient />);
});
await flush();
return container;
}
it("renders no 'Set as default' button", async () => {
setupFetchMock();
const container = await render();
expect(container.textContent).not.toContain("Set as default");
});
it("shows the '● Active' badge only on the combo whose id === activeComboId", async () => {
setupFetchMock();
const container = await render();
expect(container.querySelector('[data-testid="active-badge-c2"]')).toBeTruthy();
expect(container.querySelector('[data-testid="active-badge-c1"]')).toBeNull();
});
});
+248
View File
@@ -0,0 +1,248 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// ── Import component (no module-level mocks needed) ──────────────────────────
const { default: NoAuthAccountCard } = await import(
"../../../src/shared/components/NoAuthAccountCard"
);
// ── Helpers ──────────────────────────────────────────────────────────────────
const PROVIDER_ID = "mimocode";
function makeFingerprints(n: number): string[] {
// Deterministic, distinguishable, > 10 chars each so the truncation is exercised.
return Array.from({ length: n }, (_, i) => `fp${String(i).padStart(2, "0")}deadbeefcafe`);
}
function setupFetch(fingerprints: string[]) {
const connections =
fingerprints.length > 0
? [
{
id: "conn-1",
provider: PROVIDER_ID,
providerSpecificData: { fingerprints, accountProxies: [] },
},
]
: [];
const mockFetch = vi.fn((url: string) => {
if (String(url).includes("/api/providers")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ connections }),
} as Response);
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as Response);
});
vi.stubGlobal("fetch", mockFetch);
return mockFetch;
}
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderCard() {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
let counter = 0;
act(() => {
root.render(
<NoAuthAccountCard
providerId={PROVIDER_ID}
providerName="MiMoCode"
generateAccountId={() => `gen-${counter++}`}
/>
);
});
containers.push({ root, el });
return el;
}
async function waitForCondition(fn: () => boolean, timeoutMs = 2000) {
const start = Date.now();
while (!fn()) {
if (Date.now() - start > timeoutMs) throw new Error("waitForCondition timed out");
await new Promise((r) => setTimeout(r, 20));
}
}
const grid = (el: HTMLElement) =>
el.querySelector<HTMLElement>("[data-testid='noauth-account-grid']");
// ── Lifecycle ─────────────────────────────────────────────────────────────────
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
for (const { root, el } of containers.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
// ── Tests ──────────────────────────────────────────────────────────────────────
describe("NoAuthAccountCard compact grid", () => {
it("renders the account list as a multi-column grid container", async () => {
setupFetch(makeFingerprints(15));
const el = renderCard();
await waitForCondition(() => grid(el) !== null);
const container = grid(el)!;
// Must be a responsive grid (not the old single-column stack) so 15 accounts stay compact.
expect(container.className).toContain("grid");
expect(container.className).toMatch(/sm:grid-cols-2|lg:grid-cols-3/);
});
it("renders one chip per account fingerprint", async () => {
setupFetch(makeFingerprints(15));
const el = renderCard();
await waitForCondition(() => grid(el)?.querySelectorAll("[data-account-id]").length === 15);
expect(grid(el)!.querySelectorAll("[data-account-id]").length).toBe(15);
});
it("constrains height with vertical scroll so a long list never grows unbounded", async () => {
setupFetch(makeFingerprints(15));
const el = renderCard();
await waitForCondition(() => grid(el) !== null);
const container = grid(el)!;
expect(container.className).toContain("overflow-y-auto");
expect(container.className).toMatch(/max-h-/);
});
it("keeps a proxy control on every chip (shield icon)", async () => {
setupFetch(makeFingerprints(3));
const el = renderCard();
await waitForCondition(() => grid(el)?.querySelectorAll("[data-account-id]").length === 3);
const chips = Array.from(grid(el)!.querySelectorAll<HTMLElement>("[data-account-id]"));
for (const chip of chips) {
const proxyBtn = chip.querySelector("button[title]");
expect(proxyBtn).toBeTruthy();
expect(proxyBtn!.textContent).toContain("shield");
}
});
it("opens the proxy editor when a chip's shield is clicked", async () => {
setupFetch(makeFingerprints(3));
const el = renderCard();
await waitForCondition(() => grid(el)?.querySelectorAll("[data-account-id]").length === 3);
const firstChip = grid(el)!.querySelector<HTMLElement>("[data-account-id]")!;
const proxyBtn = firstChip.querySelector<HTMLButtonElement>("button[title]")!;
act(() => {
proxyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
await waitForCondition(() => el.textContent?.includes("Proxy for Account 1") ?? false);
expect(el.textContent).toContain("Proxy for Account 1");
// No saved proxies in this fixture → editor falls back to the custom inputs.
expect(el.querySelector("input[placeholder='Host']")).toBeTruthy();
});
});
// ── #5217 (Gap 1): Proxy Pool dropdown (by-id reference) ──────────────────────
const SAVED_PROXIES = [
{ id: "pool-1", name: "US East", type: "socks5", host: "1.2.3.4", port: 1080, status: "active" },
{ id: "pool-2", name: "EU West", type: "http", host: "9.9.9.9", port: 8080, status: "active" },
];
function setupFetchWithProxies(fingerprints: string[], accountProxies: unknown[] = []) {
const connections = [
{
id: "conn-1",
provider: PROVIDER_ID,
providerSpecificData: { fingerprints, accountProxies },
},
];
const putBodies: unknown[] = [];
const mockFetch = vi.fn((url: string, init?: RequestInit) => {
const u = String(url);
if (u.includes("/api/settings/proxies")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ items: SAVED_PROXIES }),
} as Response);
}
if (u.includes("/api/providers")) {
if (init?.method === "PUT" && typeof init.body === "string") {
putBodies.push(JSON.parse(init.body));
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ connections }),
} as Response);
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as Response);
});
vi.stubGlobal("fetch", mockFetch);
return { mockFetch, putBodies };
}
describe("NoAuthAccountCard proxy pool dropdown (#5217 Gap 1)", () => {
it("defaults the editor to the Saved Proxy Pool dropdown when pool proxies exist", async () => {
setupFetchWithProxies(makeFingerprints(2));
const el = renderCard();
await waitForCondition(() => grid(el)?.querySelectorAll("[data-account-id]").length === 2);
const proxyBtn = grid(el)!
.querySelector<HTMLElement>("[data-account-id]")!
.querySelector<HTMLButtonElement>("button[title]")!;
act(() => proxyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitForCondition(() => el.textContent?.includes("Proxy for Account 1") ?? false);
// The saved dropdown lists every pool proxy by name; the manual Host input is hidden.
const select = el.querySelector<HTMLSelectElement>("select")!;
expect(select).toBeTruthy();
expect(el.textContent).toContain("US East");
expect(el.textContent).toContain("EU West");
expect(el.querySelector("input[placeholder='Host']")).toBeNull();
});
it("persists a by-id reference {fingerprint, proxyId} when a pool proxy is selected", async () => {
const fps = makeFingerprints(2);
const { putBodies } = setupFetchWithProxies(fps);
const el = renderCard();
await waitForCondition(() => grid(el)?.querySelectorAll("[data-account-id]").length === 2);
const proxyBtn = grid(el)!
.querySelector<HTMLElement>("[data-account-id]")!
.querySelector<HTMLButtonElement>("button[title]")!;
act(() => proxyBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitForCondition(() => el.querySelector("select") !== null);
const select = el.querySelector<HTMLSelectElement>("select")!;
const setter = Object.getOwnPropertyDescriptor(
window.HTMLSelectElement.prototype,
"value"
)!.set!;
act(() => {
setter.call(select, "pool-2");
select.dispatchEvent(new Event("change", { bubbles: true }));
});
const saveBtn = Array.from(el.querySelectorAll("button")).find(
(b) => b.textContent?.trim() === "Save"
)!;
act(() => saveBtn.dispatchEvent(new MouseEvent("click", { bubbles: true })));
await waitForCondition(() => putBodies.length > 0);
const body = putBodies.at(-1) as { providerSpecificData?: { accountProxies?: unknown[] } };
const stored = body.providerSpecificData?.accountProxies ?? [];
expect(stored).toEqual([{ fingerprint: fps[0], proxyId: "pool-2" }]);
});
it("lights the shield for an account stored as a by-id reference", async () => {
const fps = makeFingerprints(2);
setupFetchWithProxies(fps, [{ fingerprint: fps[0], proxyId: "pool-1" }]);
const el = renderCard();
await waitForCondition(() => grid(el)?.querySelectorAll("[data-account-id]").length === 2);
const firstShield = grid(el)!
.querySelector<HTMLElement>("[data-account-id]")!
.querySelector<HTMLButtonElement>("button[title]")!;
// Tooltip is resolved from the referenced pool record, not an inline proxy.
expect(firstShield.getAttribute("title")).toContain("1.2.3.4");
expect(firstShield.className).toContain("text-blue-400");
});
});
@@ -0,0 +1,111 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mock next-intl translations (the page imports useTranslations("auth")).
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
import CallbackPage from "@/app/callback/page";
/**
* Regression guard for ported upstream PR decolua/9router#998 (security):
* the OAuth callback page must never relay {code, state} to a wildcard
* postMessage target ("*"), as a hostile opener can read the code/state and
* complete the OAuth flow as the user. Only the same-origin parent and
* Codex's fixed loopback helper (127.0.0.1:1455) are trusted targets.
*/
describe("OAuth callback page — postMessage target origin scope (#998)", () => {
let container: HTMLDivElement;
let root: Root;
let postMessageSpy: ReturnType<typeof vi.fn>;
let originalOpener: typeof window.opener;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
postMessageSpy = vi.fn();
originalOpener = window.opener;
// Set the callback URL with OAuth params (triggers the postMessage send).
window.history.replaceState({}, "", "/callback?code=test_code_abc123&state=test_state_xyz789");
// Stub window.opener as a CROSS-ORIGIN opener: same-origin probe must throw
// (mimics a real cross-origin window.opener), which means the page falls into
// the fallback path that previously used a wildcard "*" target origin.
Object.defineProperty(window, "opener", {
configurable: true,
writable: true,
value: {
postMessage: postMessageSpy,
get location(): never {
throw new Error("cross-origin access blocked");
},
},
});
});
afterEach(() => {
act(() => root.unmount());
container.remove();
Object.defineProperty(window, "opener", {
configurable: true,
writable: true,
value: originalOpener,
});
vi.clearAllMocks();
});
it("never targets the wildcard '*' origin even when opener is cross-origin", async () => {
await act(async () => {
root.render(<CallbackPage />);
});
// Give useEffect a microtask to flush.
await act(async () => {
await Promise.resolve();
});
const targetOrigins = postMessageSpy.mock.calls.map((call) => call[1]);
expect(targetOrigins).not.toContain("*");
});
it("only targets trusted origins (same-origin + Codex 127.0.0.1:1455)", async () => {
await act(async () => {
root.render(<CallbackPage />);
});
await act(async () => {
await Promise.resolve();
});
const trusted = new Set([window.location.origin, "http://localhost:1455", "http://127.0.0.1:1455"]);
const targetOrigins = postMessageSpy.mock.calls.map((call) => call[1]);
expect(targetOrigins.length).toBeGreaterThan(0);
for (const origin of targetOrigins) {
expect(trusted.has(origin)).toBe(true);
}
});
it("delivers the OAuth code/state payload at least once via postMessage", async () => {
await act(async () => {
root.render(<CallbackPage />);
});
await act(async () => {
await Promise.resolve();
});
// Sanity: the scoped postMessage path still actually attempts delivery.
expect(postMessageSpy).toHaveBeenCalled();
const firstCall = postMessageSpy.mock.calls[0];
expect(firstCall[0]).toMatchObject({
type: "oauth_callback",
data: expect.objectContaining({
code: "test_code_abc123",
state: "test_state_xyz789",
}),
});
});
});

Some files were not shown because too many files have changed in this diff Show More