From 7409e83236bc0c3535d4504da4965194a5de0466 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:52:24 +0200 Subject: [PATCH 1/3] test(cli): add focused dispatch behavior tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 6 (final) of the CLI deepening: add tests/cli-dispatch.test.ts covering the pure dispatch contract — DISPATCH_COMMANDS/DISPATCH_ALIASES invariants, alias resolution (setup/init, eject/restore, remove/uninstall, model/models), and dispatchCommand exit-code returns for help forms (0) and unknown commands (1). Full-stack verification: 213 pass / 4 known pre-existing environmental failures; typecheck green. --- tests/cli-dispatch.test.ts | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/cli-dispatch.test.ts diff --git a/tests/cli-dispatch.test.ts b/tests/cli-dispatch.test.ts new file mode 100644 index 0000000000..c37765eb9e --- /dev/null +++ b/tests/cli-dispatch.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { CLI_COMMANDS } from "../src/cli/registry"; +import { DISPATCH_ALIASES, DISPATCH_COMMANDS, dispatchCommand } from "../src/cli/dispatch"; +import type { CliDispatchDeps } from "../src/cli/dispatch"; + +/** Minimal fake deps. dispatchCommand only touches deps for real command + * runners, which these tests never invoke, so an empty object is enough. */ +const fakeDeps = {} as unknown as CliDispatchDeps; + +describe("CLI dispatch command coverage", () => { + test("every non-hidden registry command is dispatchable", () => { + const aliasResolved = new Set([...DISPATCH_COMMANDS, ...DISPATCH_ALIASES.keys()]); + const missing = CLI_COMMANDS.filter(entry => { + if (entry.hidden) return false; + // A visible command counts as dispatchable when it is a direct runner + // key or an alias that resolves to one (setup/eject/remove/model). + return !aliasResolved.has(entry.name); + }).map(entry => entry.name); + expect(missing).toEqual([]); + }); + + test("every dispatch alias resolves to a dispatchable command", () => { + for (const [alias, target] of DISPATCH_ALIASES) { + expect(DISPATCH_COMMANDS).toContain(target); + expect(alias).not.toBe(target); + } + }); +}); + +describe("CLI dispatch aliases", () => { + test("canonical alias pairs resolve to their command", () => { + expect(DISPATCH_ALIASES.get("setup")).toBe("init"); + expect(DISPATCH_ALIASES.get("eject")).toBe("restore"); + expect(DISPATCH_ALIASES.get("remove")).toBe("uninstall"); + expect(DISPATCH_ALIASES.get("model")).toBe("models"); + }); +}); + +describe("dispatchCommand exit codes", () => { + test("returns 0 for help forms", async () => { + expect(await dispatchCommand({ kind: "help", command: "help", args: ["help"] }, fakeDeps)).toBe(0); + expect(await dispatchCommand({ kind: "help", command: "--help", args: ["--help"] }, fakeDeps)).toBe(0); + expect(await dispatchCommand({ kind: "command", command: undefined, args: [] }, fakeDeps)).toBe(0); + }); + + test("returns 1 for an unknown command", async () => { + const head = { kind: "command" as const, command: "definitely-not-a-command", args: ["definitely-not-a-command"] }; + expect(await dispatchCommand(head, fakeDeps)).toBe(1); + }); +}); From 189db280b6abe283a9caf91f4cce097c09b85d80 Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:11:32 +0200 Subject: [PATCH 2/3] fix(cli): test alias dispatch resolution and -h help form Addresses two CodeRabbit findings on #1457: - extract resolveDispatchCommand (the pure resolver dispatchCommand uses for runner selection) and test all four aliases (setup/eject/remove/model) plus canonical/unknown/undefined cases at the resolution level, so a regression in the alias lookup is caught - cover the -h help form in the dispatchCommand help-forms test --- src/cli/dispatch.ts | 10 +++++++++- tests/cli-dispatch.test.ts | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index 6e4a05140c..99a552b5a8 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -508,13 +508,21 @@ for (const entry of CLI_COMMANDS) { export const DISPATCH_COMMANDS: ReadonlySet = new Set(Object.keys(commandRunners)); export const DISPATCH_ALIASES: ReadonlyMap = aliasTargets; +/** Resolve the runner key for a command, following registry aliases to the + * canonical runner. Returns undefined when the command is unknown. */ +export function resolveDispatchCommand(command: string | undefined): string | undefined { + if (command === undefined) return undefined; + if (command in commandRunners) return command; + return aliasTargets.get(command); +} + export async function dispatchCommand(head: CliHead, deps: CliDispatchDeps): Promise { const command = head.command; if (command === undefined || command === "help" || command === "--help" || command === "-h") { printUsage(); return 0; } - const runner = commandRunners[command] ?? commandRunners[aliasTargets.get(command) ?? ""]; + const runner = commandRunners[resolveDispatchCommand(command) ?? ""]; if (!runner) { console.error(`Unknown command: ${command}`); printUsage(); diff --git a/tests/cli-dispatch.test.ts b/tests/cli-dispatch.test.ts index c37765eb9e..ba60076104 100644 --- a/tests/cli-dispatch.test.ts +++ b/tests/cli-dispatch.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { CLI_COMMANDS } from "../src/cli/registry"; -import { DISPATCH_ALIASES, DISPATCH_COMMANDS, dispatchCommand } from "../src/cli/dispatch"; +import { DISPATCH_ALIASES, DISPATCH_COMMANDS, dispatchCommand, resolveDispatchCommand } from "../src/cli/dispatch"; import type { CliDispatchDeps } from "../src/cli/dispatch"; /** Minimal fake deps. dispatchCommand only touches deps for real command @@ -34,12 +34,26 @@ describe("CLI dispatch aliases", () => { expect(DISPATCH_ALIASES.get("remove")).toBe("uninstall"); expect(DISPATCH_ALIASES.get("model")).toBe("models"); }); + + test("resolveDispatchCommand maps each alias to its canonical runner key", () => { + // The same resolver dispatchCommand uses for runner selection, exercised + // at the resolution level so a regression in the lookup is caught. + expect(resolveDispatchCommand("setup")).toBe("init"); + expect(resolveDispatchCommand("eject")).toBe("restore"); + expect(resolveDispatchCommand("remove")).toBe("uninstall"); + expect(resolveDispatchCommand("model")).toBe("models"); + // Canonical names resolve to themselves; unknown names resolve undefined. + expect(resolveDispatchCommand("init")).toBe("init"); + expect(resolveDispatchCommand("definitely-not-a-command")).toBeUndefined(); + expect(resolveDispatchCommand(undefined)).toBeUndefined(); + }); }); describe("dispatchCommand exit codes", () => { test("returns 0 for help forms", async () => { expect(await dispatchCommand({ kind: "help", command: "help", args: ["help"] }, fakeDeps)).toBe(0); expect(await dispatchCommand({ kind: "help", command: "--help", args: ["--help"] }, fakeDeps)).toBe(0); + expect(await dispatchCommand({ kind: "help", command: "-h", args: ["-h"] }, fakeDeps)).toBe(0); expect(await dispatchCommand({ kind: "command", command: undefined, args: [] }, fakeDeps)).toBe(0); }); From cddd3759be6456b9dd9e2086fd8e710f58ba9e7f Mon Sep 17 00:00:00 2001 From: Wibias <37517432+Wibias@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:22:25 +0200 Subject: [PATCH 3/3] fix(cli): own-property check for dispatch command lookup Addresses CodeRabbit finding on #1457 (stability): commandRunners is a normal object, so 'in' accepted inherited names (__proto__, constructor, toString), reaching a non-callable or inherited function. Use Object.prototype.hasOwnProperty for the lookup and add regression tests asserting dispatchCommand returns exit code 1 for those names. --- src/cli/dispatch.ts | 2 +- tests/cli-dispatch.test.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index 99a552b5a8..d84d6b7151 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -512,7 +512,7 @@ export const DISPATCH_ALIASES: ReadonlyMap = aliasTargets; * canonical runner. Returns undefined when the command is unknown. */ export function resolveDispatchCommand(command: string | undefined): string | undefined { if (command === undefined) return undefined; - if (command in commandRunners) return command; + if (Object.prototype.hasOwnProperty.call(commandRunners, command)) return command; return aliasTargets.get(command); } diff --git a/tests/cli-dispatch.test.ts b/tests/cli-dispatch.test.ts index ba60076104..0fa501494d 100644 --- a/tests/cli-dispatch.test.ts +++ b/tests/cli-dispatch.test.ts @@ -47,6 +47,14 @@ describe("CLI dispatch aliases", () => { expect(resolveDispatchCommand("definitely-not-a-command")).toBeUndefined(); expect(resolveDispatchCommand(undefined)).toBeUndefined(); }); + + test("resolveDispatchCommand rejects inherited Object property names", () => { + // commandRunners is a normal object; inherited names (__proto__, + // constructor, toString) must not resolve as valid commands. + expect(resolveDispatchCommand("__proto__")).toBeUndefined(); + expect(resolveDispatchCommand("constructor")).toBeUndefined(); + expect(resolveDispatchCommand("toString")).toBeUndefined(); + }); }); describe("dispatchCommand exit codes", () => { @@ -61,4 +69,11 @@ describe("dispatchCommand exit codes", () => { const head = { kind: "command" as const, command: "definitely-not-a-command", args: ["definitely-not-a-command"] }; expect(await dispatchCommand(head, fakeDeps)).toBe(1); }); + + test("returns 1 for inherited Object property names", async () => { + for (const name of ["__proto__", "constructor", "toString"]) { + const head = { kind: "command" as const, command: name, args: [name] }; + expect(await dispatchCommand(head, fakeDeps), `${name} must be unknown`).toBe(1); + } + }); });