From f9865ea99dffe44b8f73346010bdefdc4684d453 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:49:33 +0200 Subject: [PATCH 1/2] refactor(cli): fold internal-dispatch and pin help banner Phase 5 of the CLI deepening: - fold src/cli/internal-dispatch.ts into src/cli/dispatch.ts as three explicit runner entries (__tray-start, __tray-restart, __startup-health) that use the injected deps directly; delete the module and its dedicated test - add banner-coverage test to cli-registry.test.ts and fix the drift it caught: printUsage now lists the previously-missing visible commands (route, logs, api-key) Behavior preserved: typecheck green; 144 pass / 4 known pre-existing environmental failures; smoke tests (version 0, help nosuch 1, ready invalid 64, --help header+new lines) pass --- src/cli/dispatch.ts | 24 ++++++--------- src/cli/help.ts | 3 ++ src/cli/internal-dispatch.ts | 20 ------------- tests/cli-registry.test.ts | 45 +++++++++++++++++++++++++++++ tests/internal-cli-dispatch.test.ts | 36 ----------------------- 5 files changed, 57 insertions(+), 71 deletions(-) delete mode 100644 src/cli/internal-dispatch.ts delete mode 100644 tests/internal-cli-dispatch.test.ts diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index 0945a07df1..3cc13491c3 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -14,7 +14,6 @@ import type { ReadyArgs } from "./ready"; import type { LiveProxy } from "../server/proxy-liveness"; import type { OcxConfig } from "../types"; import { hasHelpFlag, printSubcommandUsage, printUsage } from "./help"; -import { dispatchInternalCliCommand, type InternalCliCommand } from "./internal-dispatch"; import { setIntegrationEnabled, shouldSyncCodexOnStart } from "../codex/desired-state"; import { syncModelsToCodex } from "../codex/sync"; import { collectOrcaCodexHomeDiagnostic } from "../codex/home"; @@ -50,18 +49,6 @@ export interface CliDispatchDeps { type CommandRunner = (deps: CliDispatchDeps) => Promise; -async function runInternalTrayCommand(deps: CliDispatchDeps): Promise { - await dispatchInternalCliCommand(deps.command as InternalCliCommand, { - trayStart: async () => { await deps.handleTrayProxyStart(); }, - trayRestart: deps.handleTrayProxyRestart, - startupHealth: async () => { - const { collectStartupHealth } = await import("../codex/autostart-health"); - console.log(JSON.stringify(collectStartupHealth(deps.loadConfig()))); - }, - }); - return 0; -} - const commandRunners: Record = { init: async () => { const { runInit } = await import("./init"); @@ -329,12 +316,19 @@ const commandRunners: Record = { await refreshVersionCache(channel); return 0; }, - "__tray-start": runInternalTrayCommand, + "__tray-start": async deps => { + await deps.handleTrayProxyStart(); + return 0; + }, "__tray-restart": async deps => { await deps.handleTrayProxyRestart(); return Number(process.exitCode ?? 0); }, - "__startup-health": runInternalTrayCommand, + "__startup-health": async deps => { + const { collectStartupHealth } = await import("../codex/autostart-health"); + console.log(JSON.stringify(collectStartupHealth(deps.loadConfig()))); + return 0; + }, "__tray-host": async () => { const { runWindowsTrayHost } = await import("../tray/windows"); await runWindowsTrayHost(); diff --git a/src/cli/help.ts b/src/cli/help.ts index 049c060aba..0be35e3a27 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -51,6 +51,9 @@ Usage: ocx combo Combo failover/round-robin routing ocx agent Subagents, injection, effort caps, and sidecars ocx observe Logs, usage, storage, memory, and debug data + ocx route Routing features (combo, policy) + ocx logs [filters] Alias of ocx observe logs + ocx api-key Alias of ocx access key ocx access External API keys and endpoint information ocx export --client Print a client config wired to the running proxy (7 clients) ocx integration client Enable, disable, inspect or roll back a client integration diff --git a/src/cli/internal-dispatch.ts b/src/cli/internal-dispatch.ts deleted file mode 100644 index 37f9a5484e..0000000000 --- a/src/cli/internal-dispatch.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type InternalCliCommand = "__tray-start" | "__tray-restart" | "__startup-health"; - -export interface InternalCliHandlers { - trayStart: () => void | Promise; - trayRestart: () => void | Promise; - startupHealth: () => void | Promise; -} - -/** Dispatch fixed internal commands without accepting caller-selected process arguments. */ -export async function dispatchInternalCliCommand( - command: InternalCliCommand, - handlers: InternalCliHandlers, -): Promise { - switch (command) { - case "__tray-start": return void await handlers.trayStart(); - case "__tray-restart": return void await handlers.trayRestart(); - case "__startup-health": return void await handlers.startupHealth(); - default: throw new Error(`Unsupported internal CLI command: ${String(command)}`); - } -} diff --git a/tests/cli-registry.test.ts b/tests/cli-registry.test.ts index be2b2eb4ae..52e1c1f27c 100644 --- a/tests/cli-registry.test.ts +++ b/tests/cli-registry.test.ts @@ -1,4 +1,6 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; import { CLI_COMMANDS, findCommand } from "../src/cli/registry"; import { DISPATCH_ALIASES, DISPATCH_COMMANDS } from "../src/cli/dispatch"; @@ -76,8 +78,51 @@ describe("CLI command registry parity", () => { } }); + test("internal tray commands are dispatched as explicit runners", () => { + const internalRunners = [ + "__tray-start", + "__tray-restart", + "__startup-health", + "__tray-host", + "__gui-update-worker", + "__refresh-version", + ]; + for (const name of internalRunners) { + expect(DISPATCH_COMMANDS).toContain(name); + const entry = CLI_COMMANDS.find(command => command.name === name); + expect(entry?.hidden).toBe(true); + } + }); + test("entry names are unique", () => { const names = CLI_COMMANDS.map(entry => entry.name); expect(new Set(names).size).toBe(names.length); }); }); + +describe("help banner command coverage", () => { + // The banner is curated (aliases shown inline, subcommands elided), so it is + // not required to match the registry exactly. It must never drop a visible + // command entirely: every visible canonical command has to appear. + test("every visible canonical command appears in the printUsage banner", () => { + const helpSrc = readFileSync(fileURLToPath(new URL("../src/cli/help.ts", import.meta.url)), "utf8"); + + // Commands whose `name` is only ever used as another entry's alias + // (setup/eject/remove/model) are shown inline as "(alias: ...)" rather + // than as their own banner line, so they are not required here. + const aliasNames = new Set(); + for (const entry of CLI_COMMANDS) { + for (const alias of entry.aliases ?? []) aliasNames.add(alias); + } + + const missing = CLI_COMMANDS.filter(entry => { + if (entry.hidden) return false; + if (aliasNames.has(entry.name)) return false; + // A command counts as covered when the banner carries its full usage + // line or at least its canonical name. + return !helpSrc.includes(entry.usage) && !helpSrc.includes(entry.name); + }).map(entry => entry.name); + + expect(missing).toEqual([]); + }); +}); diff --git a/tests/internal-cli-dispatch.test.ts b/tests/internal-cli-dispatch.test.ts deleted file mode 100644 index 5c64299ff8..0000000000 --- a/tests/internal-cli-dispatch.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { dispatchInternalCliCommand, type InternalCliCommand } from "../src/cli/internal-dispatch"; - -describe("internal CLI dispatcher", () => { - test.each([ - ["__tray-start", "trayStart"], - ["__tray-restart", "trayRestart"], - ["__startup-health", "startupHealth"], - ] as const)("routes %s to only %s", async (command, expected) => { - const calls: string[] = []; - await dispatchInternalCliCommand(command as InternalCliCommand, { - trayStart: async () => { calls.push("trayStart"); }, - trayRestart: async () => { calls.push("trayRestart"); }, - startupHealth: async () => { calls.push("startupHealth"); }, - }); - expect(calls).toEqual([expected]); - }); - - test("propagates an action failure to the CLI boundary", async () => { - await expect(dispatchInternalCliCommand("__tray-start", { - trayStart: async () => { throw new Error("start failed"); }, - trayRestart: () => {}, - startupHealth: () => {}, - })).rejects.toThrow("start failed"); - }); - - test("rejects an unknown runtime value without invoking a handler", async () => { - const calls: string[] = []; - await expect(dispatchInternalCliCommand("__invalid" as InternalCliCommand, { - trayStart: () => { calls.push("trayStart"); }, - trayRestart: () => { calls.push("trayRestart"); }, - startupHealth: () => { calls.push("startupHealth"); }, - })).rejects.toThrow("Unsupported internal CLI command"); - expect(calls).toEqual([]); - }); -}); From 3cd51f84885c9c9a9b89867a786db4587fd53db3 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:47:12 +0200 Subject: [PATCH 2/2] fix(cli): return tray-start status and tighten banner coverage Addresses two CodeRabbit findings on #1456: - __tray-start now returns 1 when handleTrayProxyStart() returns false (could not make the proxy live) instead of always reporting success - banner-coverage test now matches 'ocx ' at the start of a banner line (escaped) rather than any substring; adds the previously uncovered usage/storage/memory banner lines --- src/cli/dispatch.ts | 3 +-- src/cli/help.ts | 3 +++ tests/cli-registry.test.ts | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index 3cc13491c3..6e4a05140c 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -317,8 +317,7 @@ const commandRunners: Record = { return 0; }, "__tray-start": async deps => { - await deps.handleTrayProxyStart(); - return 0; + return (await deps.handleTrayProxyStart()) ? 0 : 1; }, "__tray-restart": async deps => { await deps.handleTrayProxyRestart(); diff --git a/src/cli/help.ts b/src/cli/help.ts index 0be35e3a27..c3a3e7252b 100644 --- a/src/cli/help.ts +++ b/src/cli/help.ts @@ -53,6 +53,9 @@ Usage: ocx observe Logs, usage, storage, memory, and debug data ocx route Routing features (combo, policy) ocx logs [filters] Alias of ocx observe logs + ocx usage [--range <7d|30d|all>] Alias of ocx observe usage + ocx storage [--json] Alias of ocx observe storage + ocx memory [--json] Alias of ocx observe memory ocx api-key Alias of ocx access key ocx access External API keys and endpoint information ocx export --client Print a client config wired to the running proxy (7 clients) diff --git a/tests/cli-registry.test.ts b/tests/cli-registry.test.ts index 52e1c1f27c..5c91b97aaf 100644 --- a/tests/cli-registry.test.ts +++ b/tests/cli-registry.test.ts @@ -119,8 +119,10 @@ describe("help banner command coverage", () => { if (entry.hidden) return false; if (aliasNames.has(entry.name)) return false; // A command counts as covered when the banner carries its full usage - // line or at least its canonical name. - return !helpSrc.includes(entry.usage) && !helpSrc.includes(entry.name); + // line or its canonical name at the start of an `ocx ` banner line. + const escaped = entry.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const commandLine = new RegExp(`^\\s*ocx\\s+${escaped}(?:\\s|$)`, "m"); + return !helpSrc.includes(entry.usage) && !commandLine.test(helpSrc); }).map(entry => entry.name); expect(missing).toEqual([]);