diff --git a/next.config.mjs b/next.config.mjs index a7298f6e9f..653bc93e93 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -344,6 +344,19 @@ const nextConfig = { destination: "/docs/ops/vm-deployment-guide", permanent: true, }, + // CLI Pages — Plano 14 (F9) + { source: "/dashboard/cli-tools", destination: "/dashboard/cli-code", permanent: true }, + { + source: "/dashboard/cli-tools/:path*", + destination: "/dashboard/cli-code/:path*", + permanent: true, + }, + { source: "/dashboard/agents", destination: "/dashboard/acp-agents", permanent: true }, + { + source: "/dashboard/agents/:path*", + destination: "/dashboard/acp-agents/:path*", + permanent: true, + }, ]; }, diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 5115037d36..4eb83d39d9 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -930,7 +930,13 @@ "settingsAuthzSubtitle": "Route inventory and bypass policy", "docsSubtitle": "Documentation", "issuesSubtitle": "Report a bug", - "changelogSubtitle": "Release notes" + "changelogSubtitle": "Release notes", + "cliCode": "CLI Code's", + "cliCodeSubtitle": "Code tools pointing to OmniRoute", + "cliAgents": "CLI Agents", + "cliAgentsSubtitle": "Autonomous CLI agents", + "acpAgents": "ACP Agents", + "acpAgentsSubtitle": "CLIs spawned by OmniRoute" }, "webhooks": { "title": "Webhooks", @@ -7289,5 +7295,137 @@ "policyLabel": "Policy:", "resetIn": "reset in", "quotaTotal": "total" + }, + "cliCommon": { + "concept": { + "code": { + "title": "CLI Code's", + "phrase": "Code tools you point at OmniRoute", + "flow": "You → CLI Code → OmniRoute → Provider", + "seeOther": "See →" + }, + "agent": { + "title": "CLI Agents", + "phrase": "Broad-purpose autonomous CLI agents you point at OmniRoute", + "flow": "You → CLI Agent → OmniRoute → Provider", + "seeOther": "See →" + }, + "acp": { + "title": "ACP Agents", + "phrase": "CLIs that OmniRoute spawns as execution backend (reverse flow)", + "flow": "Client → OmniRoute → spawn CLI (stdio/ACP) → response", + "seeOther": "See →" + } + }, + "comparison": { + "title": "Understand the 3 CLI types in OmniRoute", + "thisPage": "[This page ✓]", + "code": { + "title": "Code tool", + "desc": "Points to Omni", + "flow": "you → CLI → Omni → provider", + "examples": "e.g.: claude, codex" + }, + "agent": { + "title": "Broad autonomous agent", + "desc": "Points to Omni", + "flow": "you → agent → Omni", + "examples": "e.g.: hermes, goose" + }, + "acp": { + "title": "CLI used as backend by Omni", + "desc": "Reverse execution", + "flow": "Omni → spawn CLI → resp", + "examples": "e.g.: claude, codex (ACP)" + } + }, + "card": { + "detected": "Detected", + "notDetected": "Not detected", + "configured": "Configured", + "notConfigured": "Not configured", + "configure": "Configure →", + "howToInstall": "How to install →", + "manualConfig": "Manual config", + "installGuide": "Install guide", + "endpointLabel": "Endpoint", + "baseUrlPartial": "Partial Base URL", + "refreshDetection": "Refresh detection", + "alsoAcp": "also ACP" + }, + "detail": { + "back": "Back", + "apply": "Save", + "reset": "Clear", + "manualConfig": "Manual configuration", + "vendor": "Vendor", + "category": "Type", + "detectionStatus": "Detection", + "configStatus": "Configuration", + "baseUrlLabel": "Base URL", + "apiKeyLabel": "API Key", + "modelMappingLabel": "Model mapping", + "noActiveProviders": "No active providers.", + "noActiveProvidersDesc": "Go to Providers to connect at least 1 provider before configuring the CLI.", + "openProviders": "Open Providers →" + } + }, + "cliCode": { + "pageTitle": "CLI Code's", + "pageSubtitle": "Code tools pointing to OmniRoute", + "searchPlaceholder": "Search CLI…", + "filterDetectionLabel": "Detection", + "filterBaseUrlLabel": "Base URL", + "detectionAll": "All", + "detectionInstalled": "Installed", + "detectionNotFound": "Not found", + "baseUrlAll": "All", + "baseUrlFull": "Full", + "baseUrlPartial": "Partial" + }, + "cliAgents": { + "pageTitle": "CLI Agents", + "pageSubtitle": "Broad-purpose autonomous CLI agents", + "refreshDetection": "Refresh detection", + "searchPlaceholder": "Search agent…", + "detectionFilterLabel": "Detection", + "detectionAll": "All", + "detectionInstalled": "Installed", + "detectionNotInstalled": "Not installed", + "visibleCount": "{count} visible", + "emptyState": "No CLI agents found with the current filters." + }, + "acpAgents": { + "pageTitle": "ACP Agents", + "pageSubtitle": "CLIs that OmniRoute spawns as execution backend", + "scanning": "Detecting agents…", + "refresh": "Refresh", + "setupGuideTitle": "Setup guide", + "setupGuideDetectCliTitle": "Detect CLI", + "setupGuideDetectCliDesc": "Agents are identified by running the --version command in PATH.", + "setupGuideCustomAgentTitle": "Add custom agent", + "setupGuideCustomAgentDesc": "Fill in the form below to register a custom CLI agent.", + "setupGuideCommandMissingTitle": "Command not found", + "setupGuideCommandMissingDesc": "Check that the binary is in PATH.", + "fingerprintSettingsHint": "Configure routing and fingerprints in", + "settingsRoutingLink": "Settings → Routing", + "installed": "Installed", + "notFound": "Not found", + "builtIn": "Built-in", + "custom": "Custom", + "agentUseCaseHint": "Available for spawn via ACP.", + "remove": "Remove", + "addCustomAgent": "Add custom agent", + "addCustomAgentDesc": "Register a custom CLI agent to be spawned via ACP.", + "addAgent": "Add agent", + "agentName": "Name", + "agentNamePlaceholder": "e.g.: My Agent", + "binaryName": "Binary", + "binaryNamePlaceholder": "e.g.: myagent", + "versionCommand": "Version command", + "versionCommandPlaceholder": "e.g.: myagent --version", + "spawnArgs": "Spawn arguments", + "spawnArgsPlaceholder": "e.g.: --quiet, --json", + "cliCodeRedirectCta": "Open CLI Code's" } } diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index cc60285114..e544082210 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -930,7 +930,13 @@ "settingsAuthzSubtitle": "Inventário de rotas e política de bypass", "docsSubtitle": "Documentação", "issuesSubtitle": "Reportar um bug", - "changelogSubtitle": "Notas de versão" + "changelogSubtitle": "Notas de versão", + "cliCode": "CLI Code's", + "cliCodeSubtitle": "Ferramentas de código que apontam para o OmniRoute", + "cliAgents": "CLI Agents", + "cliAgentsSubtitle": "Agentes autônomos com CLI", + "acpAgents": "ACP Agents", + "acpAgentsSubtitle": "CLIs spawnadas pelo OmniRoute" }, "webhooks": { "title": "Webhooks", @@ -7279,5 +7285,137 @@ "policyLabel": "Política:", "resetIn": "redefinir em", "quotaTotal": "total" + }, + "cliCommon": { + "concept": { + "code": { + "title": "CLI Code's", + "phrase": "Ferramentas de código que você aponta para o OmniRoute", + "flow": "Você → CLI Code → OmniRoute → Provider", + "seeOther": "Ver →" + }, + "agent": { + "title": "CLI Agents", + "phrase": "Agentes autônomos (CLI) de propósito amplo que você aponta para o OmniRoute", + "flow": "Você → CLI Agent → OmniRoute → Provider", + "seeOther": "Ver →" + }, + "acp": { + "title": "ACP Agents", + "phrase": "CLIs que o OmniRoute spawna como backend de execução (fluxo reverso)", + "flow": "Cliente → OmniRoute → spawn CLI (stdio/ACP) → resposta", + "seeOther": "Ver →" + } + }, + "comparison": { + "title": "Entenda os 3 tipos de CLI no OmniRoute", + "thisPage": "[Esta página ✓]", + "code": { + "title": "Ferramenta de código", + "desc": "Aponta para o Omni", + "flow": "você → CLI → Omni → provider", + "examples": "ex: claude, codex" + }, + "agent": { + "title": "Agente autônomo amplo", + "desc": "Aponta para o Omni", + "flow": "você → agente → Omni", + "examples": "ex: hermes, goose" + }, + "acp": { + "title": "CLI usada como backend pelo Omni", + "desc": "Execução reversa", + "flow": "Omni → spawn CLI → resp", + "examples": "ex: claude, codex (ACP)" + } + }, + "card": { + "detected": "Detectado", + "notDetected": "Não detectado", + "configured": "Configurado", + "notConfigured": "Não configurado", + "configure": "Configurar →", + "howToInstall": "Como instalar →", + "manualConfig": "Manual config", + "installGuide": "Install guide", + "endpointLabel": "Endpoint", + "baseUrlPartial": "Base URL parcial", + "refreshDetection": "Atualizar detecção", + "alsoAcp": "também ACP" + }, + "detail": { + "back": "Voltar", + "apply": "Salvar", + "reset": "Limpar", + "manualConfig": "Configuração manual", + "vendor": "Origem", + "category": "Tipo", + "detectionStatus": "Detecção", + "configStatus": "Configuração", + "baseUrlLabel": "Base URL", + "apiKeyLabel": "API Key", + "modelMappingLabel": "Mapeamento de modelos", + "noActiveProviders": "Nenhum provider ativo.", + "noActiveProvidersDesc": "Vá em Providers para conectar pelo menos 1 provider antes de configurar a CLI.", + "openProviders": "Abrir Providers →" + } + }, + "cliCode": { + "pageTitle": "CLI Code's", + "pageSubtitle": "Ferramentas de código que apontam para o OmniRoute", + "searchPlaceholder": "Buscar CLI…", + "filterDetectionLabel": "Detecção", + "filterBaseUrlLabel": "Base URL", + "detectionAll": "Todas", + "detectionInstalled": "Instaladas", + "detectionNotFound": "Não detectadas", + "baseUrlAll": "Todas", + "baseUrlFull": "Completa", + "baseUrlPartial": "Parcial" + }, + "cliAgents": { + "pageTitle": "CLI Agents", + "pageSubtitle": "Agentes autônomos (CLI) de propósito amplo", + "refreshDetection": "Atualizar detecção", + "searchPlaceholder": "Buscar agente…", + "detectionFilterLabel": "Detecção", + "detectionAll": "Todos", + "detectionInstalled": "Instalados", + "detectionNotInstalled": "Não instalados", + "visibleCount": "{count} visíveis", + "emptyState": "Nenhum agente CLI encontrado com os filtros atuais." + }, + "acpAgents": { + "pageTitle": "ACP Agents", + "pageSubtitle": "CLIs que o OmniRoute spawna como backend de execução", + "scanning": "Detectando agentes…", + "refresh": "Atualizar", + "setupGuideTitle": "Guia de setup", + "setupGuideDetectCliTitle": "Detectar CLI", + "setupGuideDetectCliDesc": "Os agentes são identificados rodando o comando --version no PATH.", + "setupGuideCustomAgentTitle": "Adicionar agente customizado", + "setupGuideCustomAgentDesc": "Preencha o formulário abaixo para registrar um agente CLI customizado.", + "setupGuideCommandMissingTitle": "Comando não encontrado", + "setupGuideCommandMissingDesc": "Verifique se o binário está no PATH.", + "fingerprintSettingsHint": "Configure roteamento e fingerprints em", + "settingsRoutingLink": "Configurações → Roteamento", + "installed": "Instalado", + "notFound": "Não encontrado", + "builtIn": "Built-in", + "custom": "Customizado", + "agentUseCaseHint": "Disponível para spawn via ACP.", + "remove": "Remover", + "addCustomAgent": "Adicionar agente customizado", + "addCustomAgentDesc": "Registre um agente CLI customizado que será spawnado via ACP.", + "addAgent": "Adicionar agente", + "agentName": "Nome", + "agentNamePlaceholder": "ex: Meu Agente", + "binaryName": "Binário", + "binaryNamePlaceholder": "ex: meuagente", + "versionCommand": "Comando de versão", + "versionCommandPlaceholder": "ex: meuagente --version", + "spawnArgs": "Argumentos de spawn", + "spawnArgsPlaceholder": "ex: --quiet, --json", + "cliCodeRedirectCta": "Abrir CLI Code's" } } diff --git a/src/i18n/request.ts b/src/i18n/request.ts index e5eb20991c..5d7ee13120 100644 --- a/src/i18n/request.ts +++ b/src/i18n/request.ts @@ -21,8 +21,23 @@ export default getRequestConfig(async () => { const messages = (await import(`./messages/${locale}.json`)).default; + // 4. Merge EN as namespace-level fallback for locales that are missing new namespaces. + // Only applied when the active locale is not EN (avoids a redundant import). + // Merging is shallow at the top-level namespace key — if a namespace is already + // present in the locale file it is kept as-is; missing namespaces fall back to EN. + // This ensures new namespaces (e.g. cliCode, cliAgents, acpAgents, cliCommon added + // in plan 14 F9) are displayed in English for the 39 non-EN/non-pt-BR locales until + // translations are shipped. + let mergedMessages: Record = messages as Record; + if (locale !== DEFAULT_LOCALE) { + const enMessages = ( + await import(`./messages/${DEFAULT_LOCALE}.json`) + ).default as Record; + mergedMessages = { ...enMessages, ...mergedMessages }; + } + return { locale, - messages, + messages: mergedMessages, }; }); diff --git a/src/shared/components/Header.tsx b/src/shared/components/Header.tsx index ed0bb37a4d..569676f359 100644 --- a/src/shared/components/Header.tsx +++ b/src/shared/components/Header.tsx @@ -48,7 +48,9 @@ const HEADER_DESCRIPTIONS: Partial> = { quota: "limitsDescription", runtime: "runtimeDescription", media: "mediaDescription", - agents: "agentsDescription", + "cli-code": "cliToolsDescription", + "cli-agents": "agentsDescription", + "acp-agents": "agentsDescription", "cloud-agents": "cloudAgentsDescription", memory: "memoryDescription", skills: "skillsDescription", @@ -98,8 +100,6 @@ const HEADER_DESCRIPTIONS: Partial> = { // Proxy sub-pages "mitm-proxy": "mitmProxyDescription", "1proxy": "oneProxyDescription", - // OmniProxy items - "cli-tools": "cliToolsDescription", }; // Build href → sidebar item lookup (non-external items only) diff --git a/src/shared/constants/sidebarVisibility.ts b/src/shared/constants/sidebarVisibility.ts index f6cf0eb4dc..e70891617b 100644 --- a/src/shared/constants/sidebarVisibility.ts +++ b/src/shared/constants/sidebarVisibility.ts @@ -13,7 +13,8 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [ "context-rtk", "context-combos", // OmniProxy > Tools - "cli-tools", + "cli-code", + "cli-agents", "acp-agents", "cloud-agents", // OmniProxy > Integrations @@ -229,18 +230,25 @@ const TOOLS_GROUP: SidebarItemGroup = { titleFallback: "Tools", items: [ { - id: "cli-tools", - href: "/dashboard/cli-tools", - i18nKey: "cliTools", - subtitleKey: "cliToolsSubtitle", + id: "cli-code", + href: "/dashboard/cli-code", + i18nKey: "cliCode", + subtitleKey: "cliCodeSubtitle", icon: "terminal", }, + { + id: "cli-agents", + href: "/dashboard/cli-agents", + i18nKey: "cliAgents", + subtitleKey: "cliAgentsSubtitle", + icon: "smart_toy", + }, { id: "acp-agents", href: "/dashboard/acp-agents", i18nKey: "acpAgents", subtitleKey: "acpAgentsSubtitle", - icon: "smart_toy", + icon: "device_hub", }, { id: "cloud-agents", @@ -801,7 +809,8 @@ const DEVELOPER_SHOWN: ReadonlySet = new Set([ "context-caveman", "context-rtk", "context-combos", - "cli-tools", + "cli-code", + "cli-agents", "acp-agents", "api-endpoints", "analytics", diff --git a/tests/unit/i18n-cli-namespaces.test.ts b/tests/unit/i18n-cli-namespaces.test.ts new file mode 100644 index 0000000000..0922791050 --- /dev/null +++ b/tests/unit/i18n-cli-namespaces.test.ts @@ -0,0 +1,124 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const pt = require("../../src/i18n/messages/pt-BR.json"); +const en = require("../../src/i18n/messages/en.json"); + +// ─── PT-BR namespace presence ───────────────────────────────────────────────── + +test("pt-BR has cliCommon namespace", () => { + assert.ok(pt.cliCommon, "expected pt-BR.json to have 'cliCommon' namespace"); +}); + +test("pt-BR has cliCode namespace", () => { + assert.ok(pt.cliCode, "expected pt-BR.json to have 'cliCode' namespace"); +}); + +test("pt-BR has cliAgents namespace", () => { + assert.ok(pt.cliAgents, "expected pt-BR.json to have 'cliAgents' namespace"); +}); + +test("pt-BR has acpAgents namespace", () => { + assert.ok(pt.acpAgents, "expected pt-BR.json to have 'acpAgents' namespace"); +}); + +// ─── PT-BR page titles ──────────────────────────────────────────────────────── + +test("pt-BR cliCode.pageTitle is 'CLI Code's'", () => { + assert.equal(pt.cliCode.pageTitle, "CLI Code's"); +}); + +test("pt-BR cliAgents.pageTitle is 'CLI Agents'", () => { + assert.equal(pt.cliAgents.pageTitle, "CLI Agents"); +}); + +test("pt-BR acpAgents.pageTitle is 'ACP Agents'", () => { + assert.equal(pt.acpAgents.pageTitle, "ACP Agents"); +}); + +// ─── PT-BR cliCommon content ────────────────────────────────────────────────── + +test("pt-BR cliCommon.concept.code.phrase contains 'código'", () => { + assert.ok( + typeof pt.cliCommon.concept?.code?.phrase === "string" && + pt.cliCommon.concept.code.phrase.includes("código"), + `expected cliCommon.concept.code.phrase to contain 'código', got: ${pt.cliCommon.concept?.code?.phrase}` + ); +}); + +test("pt-BR cliCommon.comparison.title is non-empty string", () => { + assert.ok( + typeof pt.cliCommon.comparison?.title === "string" && pt.cliCommon.comparison.title.length > 0 + ); +}); + +// ─── PT-BR sidebar keys ─────────────────────────────────────────────────────── + +test("pt-BR sidebar has cliCode key", () => { + assert.ok(pt.sidebar?.cliCode, "expected pt-BR sidebar to have 'cliCode' key"); +}); + +test("pt-BR sidebar has cliAgents key", () => { + assert.ok(pt.sidebar?.cliAgents, "expected pt-BR sidebar to have 'cliAgents' key"); +}); + +test("pt-BR sidebar has acpAgents key", () => { + assert.ok(pt.sidebar?.acpAgents, "expected pt-BR sidebar to have 'acpAgents' key"); +}); + +// ─── EN namespace presence ──────────────────────────────────────────────────── + +test("en has cliCommon namespace", () => { + assert.ok(en.cliCommon, "expected en.json to have 'cliCommon' namespace"); +}); + +test("en has cliCode namespace", () => { + assert.ok(en.cliCode, "expected en.json to have 'cliCode' namespace"); +}); + +test("en has cliAgents namespace", () => { + assert.ok(en.cliAgents, "expected en.json to have 'cliAgents' namespace"); +}); + +test("en has acpAgents namespace", () => { + assert.ok(en.acpAgents, "expected en.json to have 'acpAgents' namespace"); +}); + +// ─── EN page titles ─────────────────────────────────────────────────────────── + +test("en cliCode.pageTitle is 'CLI Code's'", () => { + assert.equal(en.cliCode.pageTitle, "CLI Code's"); +}); + +test("en cliAgents.pageTitle is 'CLI Agents'", () => { + assert.equal(en.cliAgents.pageTitle, "CLI Agents"); +}); + +test("en cliAgents.pageTitle is 'ACP Agents'", () => { + assert.equal(en.acpAgents.pageTitle, "ACP Agents"); +}); + +// ─── EN cliCommon content ───────────────────────────────────────────────────── + +test("en cliCommon.concept.code.phrase is a non-empty string", () => { + assert.ok( + typeof en.cliCommon.concept?.code?.phrase === "string" && + en.cliCommon.concept.code.phrase.length > 0 + ); +}); + +// ─── EN sidebar keys ────────────────────────────────────────────────────────── + +test("en sidebar has cliCode key", () => { + assert.ok(en.sidebar?.cliCode, "expected en sidebar to have 'cliCode' key"); +}); + +test("en sidebar has cliAgents key", () => { + assert.ok(en.sidebar?.cliAgents, "expected en sidebar to have 'cliAgents' key"); +}); + +test("en sidebar has acpAgents key", () => { + assert.ok(en.sidebar?.acpAgents, "expected en sidebar to have 'acpAgents' key"); +}); diff --git a/tests/unit/redirects-cli-renames.test.ts b/tests/unit/redirects-cli-renames.test.ts new file mode 100644 index 0000000000..9fcde5f7c9 --- /dev/null +++ b/tests/unit/redirects-cli-renames.test.ts @@ -0,0 +1,50 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +const repoRoot = join(import.meta.dirname, "../.."); +const configSource = await readFile(join(repoRoot, "next.config.mjs"), "utf8"); + +test("next.config.mjs has permanent redirect from /dashboard/cli-tools to /dashboard/cli-code", () => { + assert.ok( + configSource.includes('source: "/dashboard/cli-tools"') && + configSource.includes('destination: "/dashboard/cli-code"'), + "expected /dashboard/cli-tools → /dashboard/cli-code redirect in next.config.mjs" + ); +}); + +test("next.config.mjs has permanent wildcard redirect from /dashboard/cli-tools/:path* to /dashboard/cli-code/:path*", () => { + assert.ok( + configSource.includes('source: "/dashboard/cli-tools/:path*"') && + configSource.includes('destination: "/dashboard/cli-code/:path*"'), + "expected /dashboard/cli-tools/:path* → /dashboard/cli-code/:path* redirect in next.config.mjs" + ); +}); + +test("next.config.mjs has permanent redirect from /dashboard/agents to /dashboard/acp-agents", () => { + assert.ok( + configSource.includes('source: "/dashboard/agents"') && + configSource.includes('destination: "/dashboard/acp-agents"'), + "expected /dashboard/agents → /dashboard/acp-agents redirect in next.config.mjs" + ); +}); + +test("next.config.mjs has permanent wildcard redirect from /dashboard/agents/:path* to /dashboard/acp-agents/:path*", () => { + assert.ok( + configSource.includes('source: "/dashboard/agents/:path*"') && + configSource.includes('destination: "/dashboard/acp-agents/:path*"'), + "expected /dashboard/agents/:path* → /dashboard/acp-agents/:path* redirect in next.config.mjs" + ); +}); + +test("all 4 CLI redirect entries use permanent: true", () => { + // Extract the CLI Pages block + const cliBlock = configSource.slice(configSource.indexOf("// CLI Pages — Plano 14 (F9)")); + assert.ok(cliBlock.length > 0, "expected CLI Pages block to be present"); + const permanentCount = (cliBlock.match(/permanent: true/g) || []).length; + assert.ok( + permanentCount >= 4, + `expected at least 4 'permanent: true' entries in CLI Pages block, found ${permanentCount}` + ); +}); diff --git a/tests/unit/sidebar-cli-renames.test.ts b/tests/unit/sidebar-cli-renames.test.ts new file mode 100644 index 0000000000..45ed1c00a4 --- /dev/null +++ b/tests/unit/sidebar-cli-renames.test.ts @@ -0,0 +1,41 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const sidebarVisibility = await import("../../src/shared/constants/sidebarVisibility.ts"); + +test("HIDEABLE_SIDEBAR_ITEM_IDS includes cli-code (plan 14 rename)", () => { + assert.ok( + (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("cli-code"), + "expected 'cli-code' in HIDEABLE_SIDEBAR_ITEM_IDS" + ); +}); + +test("HIDEABLE_SIDEBAR_ITEM_IDS includes cli-agents (plan 14 new entry)", () => { + assert.ok( + (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("cli-agents"), + "expected 'cli-agents' in HIDEABLE_SIDEBAR_ITEM_IDS" + ); +}); + +test("HIDEABLE_SIDEBAR_ITEM_IDS includes acp-agents (renamed from agents)", () => { + assert.ok( + (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("acp-agents"), + "expected 'acp-agents' in HIDEABLE_SIDEBAR_ITEM_IDS" + ); +}); + +test("HIDEABLE_SIDEBAR_ITEM_IDS does NOT include legacy cli-tools", () => { + assert.equal( + (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("cli-tools"), + false, + "expected 'cli-tools' to be removed from HIDEABLE_SIDEBAR_ITEM_IDS (plan 14 rename to cli-code)" + ); +}); + +test("HIDEABLE_SIDEBAR_ITEM_IDS does NOT include legacy agents", () => { + assert.equal( + (sidebarVisibility.HIDEABLE_SIDEBAR_ITEM_IDS as readonly string[]).includes("agents"), + false, + "expected 'agents' to be removed from HIDEABLE_SIDEBAR_ITEM_IDS (plan 14 rename to acp-agents)" + ); +}); diff --git a/tests/unit/sidebar-tools-group.test.ts b/tests/unit/sidebar-tools-group.test.ts new file mode 100644 index 0000000000..7bed6ea644 --- /dev/null +++ b/tests/unit/sidebar-tools-group.test.ts @@ -0,0 +1,72 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const sidebarVisibility = await import("../../src/shared/constants/sidebarVisibility.ts"); + +function getToolsGroup() { + const omniProxySection = sidebarVisibility.SIDEBAR_SECTIONS.find( + (section) => section.id === "omni-proxy" + ); + assert.ok(omniProxySection, "expected omni-proxy section to exist"); + + const toolsGroup = omniProxySection.children.find( + (child): child is (typeof sidebarVisibility.SIDEBAR_SECTIONS)[number]["children"][number] & { + type: "group"; + } => + "type" in child && + (child as { type: string }).type === "group" && + (child as { id: string }).id === "tools" + ); + assert.ok(toolsGroup, "expected tools group to exist in omni-proxy section"); + return toolsGroup as { + type: "group"; + id: string; + items: readonly { id: string; href: string; i18nKey: string }[]; + }; +} + +test("TOOLS_GROUP items follow plan 14 order: cli-code → cli-agents → acp-agents → cloud-agents", () => { + const toolsGroup = getToolsGroup(); + const itemIds = toolsGroup.items.map((item) => item.id); + assert.deepEqual( + itemIds, + ["cli-code", "cli-agents", "acp-agents", "cloud-agents"], + "TOOLS_GROUP items order must be cli-code, cli-agents, acp-agents, cloud-agents" + ); +}); + +test("TOOLS_GROUP cli-code item has correct href and i18nKey", () => { + const toolsGroup = getToolsGroup(); + const cliCode = toolsGroup.items.find((item) => item.id === "cli-code"); + assert.ok(cliCode, "expected cli-code in TOOLS_GROUP"); + assert.equal(cliCode.href, "/dashboard/cli-code"); + assert.equal(cliCode.i18nKey, "cliCode"); +}); + +test("TOOLS_GROUP cli-agents item has correct href and i18nKey", () => { + const toolsGroup = getToolsGroup(); + const cliAgents = toolsGroup.items.find((item) => item.id === "cli-agents"); + assert.ok(cliAgents, "expected cli-agents in TOOLS_GROUP"); + assert.equal(cliAgents.href, "/dashboard/cli-agents"); + assert.equal(cliAgents.i18nKey, "cliAgents"); +}); + +test("TOOLS_GROUP acp-agents item has correct href and i18nKey", () => { + const toolsGroup = getToolsGroup(); + const acpAgents = toolsGroup.items.find((item) => item.id === "acp-agents"); + assert.ok(acpAgents, "expected acp-agents in TOOLS_GROUP"); + assert.equal(acpAgents.href, "/dashboard/acp-agents"); + assert.equal(acpAgents.i18nKey, "acpAgents"); +}); + +test("TOOLS_GROUP does NOT contain legacy cli-tools or agents entries", () => { + const toolsGroup = getToolsGroup(); + const legacyIds = toolsGroup.items + .map((item) => item.id) + .filter((id) => id === "cli-tools" || id === "agents"); + assert.deepEqual( + legacyIds, + [], + "TOOLS_GROUP must not contain legacy 'cli-tools' or 'agents' entries" + ); +}); diff --git a/tests/unit/sidebar-visibility.test.ts b/tests/unit/sidebar-visibility.test.ts index 59c0bf3750..d2c9282ff1 100644 --- a/tests/unit/sidebar-visibility.test.ts +++ b/tests/unit/sidebar-visibility.test.ts @@ -49,8 +49,9 @@ test("primary sidebar items place limits after cache", () => { "context-caveman", "context-rtk", "context-combos", - "cli-tools", - "agents", + "cli-code", + "cli-agents", + "acp-agents", "cloud-agents", "api-endpoints", "webhooks",