From b50205fc5f36dbabce43de985ddc2ac5fee529fe Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Wed, 1 Jul 2026 03:06:07 -0400 Subject: [PATCH] Move CLI profile sync toggles to CLI Code (#5778) * move CLI profile sync toggles to CLI Code * test CLI profile auto-sync toggles * Document CLI profile auto-sync flags * docs(changelog): note CLI profile auto-sync card moved to CLI Code (#5778) --------- Co-authored-by: Diego Rodrigues de Sa e Souza --- .env.example | 2 +- CHANGELOG.md | 2 +- docs/guides/CLAUDE-CODE-CONFIGURATION.md | 2 +- docs/guides/CODEX-CLI-CONFIGURATION.md | 2 +- docs/reference/ENVIRONMENT.md | 10 ++ .../dashboard/cli-code/CliCodePageClient.tsx | 10 +- .../components/CliProfileAutoSyncToggles.tsx | 8 +- .../(dashboard)/dashboard/providers/page.tsx | 6 +- tests/unit/feature-flags-settings.test.ts | 27 ++- tests/unit/ui/CliCodePage.test.tsx | 168 ++++++++++++++---- 10 files changed, 185 insertions(+), 52 deletions(-) rename src/app/(dashboard)/dashboard/{providers => cli-code}/components/CliProfileAutoSyncToggles.tsx (90%) diff --git a/.env.example b/.env.example index e3bc58391d..d54669069c 100644 --- a/.env.example +++ b/.env.example @@ -532,7 +532,7 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # Auto-sync CLI profile files after provider model discovery changes. OPT-IN, default OFF for # both. When enabled, writes only the tool's profile files (~/.codex/*.config.toml or # ~/.claude/profiles//settings.json); never changes the active/default config. Both also -# require CLI_ALLOW_CONFIG_WRITES (default on). Toggle from the providers dashboard, or set here. +# require CLI_ALLOW_CONFIG_WRITES (default on). Toggle from the CLI Code dashboard, or set here. # Leave unset to disable. (Feature flags — a DB/dashboard override takes precedence over env.) # OMNIROUTE_AUTO_SYNC_CODEX_PROFILES=true # OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES=true diff --git a/CHANGELOG.md b/CHANGELOG.md index c03c7dfe2b..60e85b2258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### ✨ New Features -- **providers (CLI profile auto-sync):** opt-in toggles to auto-regenerate CLI tool profiles after a provider model sync. When enabled, a model-catalog change (re)writes that tool's profile files from the live catalog — Codex (`~/.codex/*.config.toml`) and now **Claude Code** (`~/.claude/profiles//settings.json`, via an extracted `syncClaudeProfilesFromModels` + a new `claudeProfileAutoSync.ts` mirroring the Codex path). Both are **off by default** and never touch the active/default CLI config; they are backed by the `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES` / `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` feature flags (DB/dashboard override > env > default "false") and additionally gated behind the existing `CLI_ALLOW_CONFIG_WRITES` write-guard. A **"CLI profile auto-sync"** card on the providers dashboard toggles each. Regression guards: `tests/unit/claude-profile-auto-sync-gate.test.ts`, `tests/unit/codex-profile-auto-sync-gate.test.ts`, `tests/unit/cli/setup-claude.test.ts` (follow-up to #5737). +- **providers (CLI profile auto-sync):** opt-in toggles to auto-regenerate CLI tool profiles after a provider model sync. When enabled, a model-catalog change (re)writes that tool's profile files from the live catalog — Codex (`~/.codex/*.config.toml`) and now **Claude Code** (`~/.claude/profiles//settings.json`, via an extracted `syncClaudeProfilesFromModels` + a new `claudeProfileAutoSync.ts` mirroring the Codex path). Both are **off by default** and never touch the active/default CLI config; they are backed by the `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES` / `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` feature flags (DB/dashboard override > env > default "false") and additionally gated behind the existing `CLI_ALLOW_CONFIG_WRITES` write-guard. A **"CLI profile auto-sync"** card on the CLI Code dashboard toggles each (moved from the providers dashboard in [#5778](https://github.com/diegosouzapw/OmniRoute/pull/5778) — thanks [@rdself](https://github.com/rdself)). Regression guards: `tests/unit/claude-profile-auto-sync-gate.test.ts`, `tests/unit/codex-profile-auto-sync-gate.test.ts`, `tests/unit/cli/setup-claude.test.ts` (follow-up to #5737). - **cli (startup banner):** the `serve` startup banner now prints the running OmniRoute version (`v3.8.x`) beneath the ASCII logo, so the active version is visible at a glance without a separate `--version` call. Regression guard: `tests/unit/cli-serve-version-banner.test.ts`. Thanks [@chirag127](https://github.com/chirag127) ([#5752](https://github.com/diegosouzapw/OmniRoute/pull/5752)). diff --git a/docs/guides/CLAUDE-CODE-CONFIGURATION.md b/docs/guides/CLAUDE-CODE-CONFIGURATION.md index 1820adef02..a34ee464fd 100644 --- a/docs/guides/CLAUDE-CODE-CONFIGURATION.md +++ b/docs/guides/CLAUDE-CODE-CONFIGURATION.md @@ -83,7 +83,7 @@ profile per model at `~/.claude/profiles//settings.json`, reusing the **Auto-sync after model discovery (opt-in).** OmniRoute can regenerate these same `~/.claude/profiles//settings.json` files automatically whenever a provider model sync changes the live catalog — so new/renamed models get profiles without re-running the -command. It is **off by default**: toggle it from the **providers dashboard** ("CLI profile +command. It is **off by default**: toggle it from the **CLI Code dashboard** ("CLI profile auto-sync" → Claude Code), or set `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES=true` (it also honors `CLI_ALLOW_CONFIG_WRITES`, on by default). When enabled it only writes profile files; it never changes your active/default Claude config, auth, or the `~/.claude/settings.json`. diff --git a/docs/guides/CODEX-CLI-CONFIGURATION.md b/docs/guides/CODEX-CLI-CONFIGURATION.md index 5a059bcd95..efc1c5f0b7 100644 --- a/docs/guides/CODEX-CLI-CONFIGURATION.md +++ b/docs/guides/CODEX-CLI-CONFIGURATION.md @@ -243,7 +243,7 @@ omniroute setup-codex --codex-home /path/to/.codex The command fetches `/v1/models`, uses tuned profiles for known models, falls back to catalog metadata for other compatible text models, and writes `~/.codex/.config.toml` for each. Idempotent — safe to re-run. -OmniRoute can also **auto-sync** these same profile files after a successful provider model discovery/import changes the live catalog. This is **opt-in and off by default**: toggle it from the **providers dashboard** ("CLI profile auto-sync" → Codex), or set `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES=true` (it also honors `CLI_ALLOW_CONFIG_WRITES`, on by default). When enabled it only writes separate `~/.codex/*.config.toml` profile files; it never changes the active/default `~/.codex/config.toml`, Codex-lb settings, auth, or provider selection. +OmniRoute can also **auto-sync** these same profile files after a successful provider model discovery/import changes the live catalog. This is **opt-in and off by default**: toggle it from the **CLI Code dashboard** ("CLI profile auto-sync" → Codex), or set `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES=true` (it also honors `CLI_ALLOW_CONFIG_WRITES`, on by default). When enabled it only writes separate `~/.codex/*.config.toml` profile files; it never changes the active/default `~/.codex/config.toml`, Codex-lb settings, auth, or provider selection. --- diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 7c39548eb2..30b8aa5437 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -353,6 +353,16 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex, | `CLI_DEVIN_BIN` | `devin` | `open-sse/executors/devin-cli.ts` | Custom path to the Devin CLI binary (v3.8.0). Used by the Windsurf/Devin executor. | | `HERMES_HOME` | `~/.hermes` | `src/lib/cli-helper/config-generator/hermesHome.ts` | Hermes Agent home directory where OmniRoute reads/writes the Hermes CLI config. Matches the env var the Hermes PowerShell installer sets on Windows (`%LOCALAPPDATA%\hermes`). | +### CLI Profile Auto-Sync + +These feature flags are opt-in and default off. They can also be toggled from +the CLI Code dashboard. + +| Variable | Default | Source File | Description | +| --------------------------------------- | ------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OMNIROUTE_AUTO_SYNC_CODEX_PROFILES` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | After a provider model sync, automatically rewrites `~/.codex/*.config.toml` profile files from the live catalog. Requires `CLI_ALLOW_CONFIG_WRITES`; never changes the active/default Codex config, auth, Codex-lb settings, or provider choice. | +| `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | After a provider model sync, automatically rewrites `~/.claude/profiles//settings.json` Claude Code profile files from the live catalog. Requires `CLI_ALLOW_CONFIG_WRITES`; never changes the active/default Claude config, auth, or provider choice. | + ### Docker Example ```bash diff --git a/src/app/(dashboard)/dashboard/cli-code/CliCodePageClient.tsx b/src/app/(dashboard)/dashboard/cli-code/CliCodePageClient.tsx index 86e4af3360..1a5de54891 100644 --- a/src/app/(dashboard)/dashboard/cli-code/CliCodePageClient.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/CliCodePageClient.tsx @@ -9,6 +9,7 @@ import { EXPECTED_CODE_COUNT } from "@/shared/schemas/cliCatalog"; import { CliToolCard, CliConceptCard, CliComparisonCard } from "@/shared/components/cli"; import { useToolBatchStatuses } from "@/shared/hooks/cli/useToolBatchStatuses"; import type { CliCatalogEntry } from "@/shared/schemas/cliCatalog"; +import CliProfileAutoSyncToggles from "./components/CliProfileAutoSyncToggles"; // ── Static catalogue slice ──────────────────────────────────────────────────── @@ -58,7 +59,9 @@ export default function CliCodePageClient({ machineId: _machineId }: CliCodePage useEffect(() => { let cancelled = false; fetch("/api/providers") - .then((res) => (res.ok ? res.json() : Promise.resolve({ connections: [] }))) + .then((res) => + res.ok ? res.json() : Promise.resolve({ connections: [] }) + ) .then((data) => { if (cancelled) return; const active = (data.connections ?? []).filter((c) => c.isActive !== false); @@ -99,8 +102,7 @@ export default function CliCodePageClient({ machineId: _machineId }: CliCodePage return CODE_TOOLS.filter(([id, tool]) => { // Search filter if (q) { - const haystack = - `${tool.name} ${tool.vendor} ${tool.description}`.toLowerCase(); + const haystack = `${tool.name} ${tool.vendor} ${tool.description}`.toLowerCase(); if (!haystack.includes(q)) return false; } @@ -131,6 +133,8 @@ export default function CliCodePageClient({ machineId: _machineId }: CliCodePage {/* Comparison card */} + + {/* Header bar */}
{/* Title + subtitle */} diff --git a/src/app/(dashboard)/dashboard/providers/components/CliProfileAutoSyncToggles.tsx b/src/app/(dashboard)/dashboard/cli-code/components/CliProfileAutoSyncToggles.tsx similarity index 90% rename from src/app/(dashboard)/dashboard/providers/components/CliProfileAutoSyncToggles.tsx rename to src/app/(dashboard)/dashboard/cli-code/components/CliProfileAutoSyncToggles.tsx index 74f4bdedcb..0069de66d8 100644 --- a/src/app/(dashboard)/dashboard/providers/components/CliProfileAutoSyncToggles.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/components/CliProfileAutoSyncToggles.tsx @@ -13,10 +13,10 @@ function isOn(value: string | undefined): boolean { } /** - * Providers-dashboard toggle card for the opt-in "auto-sync CLI profiles after model - * discovery" feature. Reads/writes the OMNIROUTE_AUTO_SYNC_{CODEX,CLAUDE}_PROFILES feature - * flags via /api/settings/feature-flags. Both default off; enabling one makes a provider - * model sync regenerate that tool's profile files from the live catalog. + * Toggle card for the opt-in "auto-sync CLI profiles after model discovery" feature. + * Reads/writes the OMNIROUTE_AUTO_SYNC_{CODEX,CLAUDE}_PROFILES feature flags via + * /api/settings/feature-flags. Both default off; enabling one makes a provider model + * sync regenerate that tool's profile files from the live catalog. */ export default function CliProfileAutoSyncToggles() { const [codexOn, setCodexOn] = useState(false); diff --git a/src/app/(dashboard)/dashboard/providers/page.tsx b/src/app/(dashboard)/dashboard/providers/page.tsx index 97b051279e..466b3e53d3 100644 --- a/src/app/(dashboard)/dashboard/providers/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/page.tsx @@ -43,7 +43,6 @@ import NoAuthProvidersSection from "./components/NoAuthProvidersSection"; import ProviderCard from "./components/ProviderCard"; import ProviderCountBadge from "./components/ProviderCountBadge"; import ProviderSummaryCard from "./components/ProviderSummaryCard"; -import CliProfileAutoSyncToggles from "./components/CliProfileAutoSyncToggles"; import { buildCompactProviderEntriesForPage, getCompactProviderAuthType, @@ -394,7 +393,8 @@ export default function ProvidersPage() { // Count API keys in "warning" state across all connections const warning = providerConnections.reduce((warnCount, conn) => { const health = (conn as any).providerSpecificData?.apiKeyHealth as - Record | undefined; + | Record + | undefined; if (!health) return warnCount; return warnCount + Object.values(health).filter((h) => h.status === "warning").length; }, 0); @@ -865,8 +865,6 @@ export default function ProvidersPage() { testingMode={testingMode} /> - - {/* Expiration Banner */} {expirations?.summary && (expirations.summary.expired > 0 || expirations.summary.expiringSoon > 0) && ( diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index f0f5feb190..fccb08ee55 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -30,17 +30,19 @@ const { isControlPlaneProxyDirectFallbackEnabled, } = await import("../../src/shared/utils/featureFlags.ts"); +const EXPECTED_FEATURE_FLAG_COUNT = 40; + // ────────────────────────────────────────────────────── // Test group 1 — Flag definitions registry // ────────────────────────────────────────────────────── describe("featureFlagDefinitions", () => { - it("has exactly 38 flag definitions", () => { - assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, 38); + it("has exactly 40 flag definitions", () => { + assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, EXPECTED_FEATURE_FLAG_COUNT); }); it("has unique keys for all flags", () => { const keys = FEATURE_FLAG_DEFINITIONS.map((d) => d.key); - assert.strictEqual(new Set(keys).size, 38); + assert.strictEqual(new Set(keys).size, EXPECTED_FEATURE_FLAG_COUNT); }); it("has valid categories for all flags", () => { @@ -158,6 +160,21 @@ describe("featureFlagDefinitions", () => { assert.strictEqual(def.requiresRestart, false); assert.strictEqual(def.warningLevel, "danger"); }); + + it("defines CLI profile auto-sync flags as CLI booleans disabled by default", () => { + for (const key of [ + "OMNIROUTE_AUTO_SYNC_CODEX_PROFILES", + "OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES", + ]) { + const def = FEATURE_FLAG_DEFINITIONS.find((d) => d.key === key); + assert.ok(def, `${key} should exist`); + assert.strictEqual(def.category, "cli"); + assert.strictEqual(def.type, "boolean"); + assert.strictEqual(def.defaultValue, "false"); + assert.strictEqual(def.requiresRestart, false); + assert.strictEqual(def.warningLevel, "caution"); + } + }); }); // ────────────────────────────────────────────────────── @@ -295,9 +312,9 @@ describe("resolveFeatureFlag", () => { }); describe("resolveAllFeatureFlags", () => { - it("returns all 38 flags", () => { + it("returns all 40 flags", () => { const all = resolveAllFeatureFlags(); - assert.strictEqual(all.length, 38); + assert.strictEqual(all.length, EXPECTED_FEATURE_FLAG_COUNT); }); it("marks DB-overridden flags with source 'db'", () => { diff --git a/tests/unit/ui/CliCodePage.test.tsx b/tests/unit/ui/CliCodePage.test.tsx index d414e0699d..9334d1645c 100644 --- a/tests/unit/ui/CliCodePage.test.tsx +++ b/tests/unit/ui/CliCodePage.test.tsx @@ -3,6 +3,7 @@ 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 ───────────────────────────────────────────────────────────────────── @@ -62,17 +63,35 @@ vi.mock("@/shared/components", () => ({ ), CardSkeleton: () =>
, - Input: ({ - placeholder, - value, + Input: ({ placeholder, value, onChange }: React.InputHTMLAttributes) => ( + + ), + Toggle: ({ + checked = false, + disabled = false, + label, + description, onChange, - }: React.InputHTMLAttributes) => ( - + }: { + checked?: boolean; + disabled?: boolean; + label?: string; + description?: string; + onChange?: (checked: boolean) => void; + }) => ( +
+ + {description ? {description} : null} +
), })); @@ -98,22 +117,39 @@ vi.mock("@/shared/hooks/cli/useToolBatchStatuses", () => ({ // ── 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) => { +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" -); +const { default: CliCodePageClient } = + await import("@/app/(dashboard)/dashboard/cli-code/CliCodePageClient"); // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -133,11 +169,15 @@ async function renderPage(props: { machineId?: string } = {}): Promise { await Promise.resolve(); }); - - return container; } // ── Lifecycle ───────────────────────────────────────────────────────────────── @@ -157,14 +197,32 @@ beforeEach(() => { 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) => { + 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; }); @@ -191,26 +249,77 @@ describe("CliCodePageClient", () => { expect(container.querySelector('[data-testid="cli-comparison-card"]')).not.toBeNull(); }); - it("2. renders 19 CliToolCard cards when catalogue is OK (code + baseUrlSupport != none)", async () => { + 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(19); + 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 19 initially visible - expect(container.querySelectorAll('[data-testid="cli-tool-card"]').length).toBe(19); + // 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 }) - ); + input.dispatchEvent(new Event("input", { bubbles: true })); // Simulate onChange const syntheticEvent = { target: { value: "claude" }, @@ -242,7 +351,7 @@ describe("CliCodePageClient", () => { ); }); - // We can verify with a simpler approach: check the card count remains 19 (no crash) + // 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); }); @@ -262,7 +371,7 @@ describe("CliCodePageClient", () => { const cards = container.querySelectorAll('[data-testid="cli-tool-card"]'); // After filtering for "claude code", only Claude Code CLI should match - expect(cards.length).toBeLessThan(19); + 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"); @@ -353,11 +462,6 @@ describe("CliCodePageClient", () => { }); // Helper wrapper (not exported) — needed only for test 3 internal use -function TestWrapper({ - children, -}: { - children: React.ReactNode; - search?: string; -}) { +function TestWrapper({ children }: { children: React.ReactNode; search?: string }) { return <>{children}; }