From bca768ecd2b19412934282ca6905f0f249748443 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:00:14 -0800 Subject: [PATCH] test: Add E2E tests for cmdRun argument resolution and swapping (#311) Tests 5 critical untested paths in commands.ts and index.ts: - Argument swapping detection (spawn cloud agent -> spawn agent cloud) - Display name resolution (Claude Code -> claude, Hetzner Cloud -> hetzner) - Case-insensitive key resolution (CLAUDE -> claude, Sprite -> sprite) - showInfoOrError display name resolution for single-arg mode - Did-you-mean suggestions for typos in agent/cloud names 27 new tests, all passing. Agent: test-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 --- cli/src/__tests__/cmdrun-resolution.test.ts | 288 ++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 cli/src/__tests__/cmdrun-resolution.test.ts diff --git a/cli/src/__tests__/cmdrun-resolution.test.ts b/cli/src/__tests__/cmdrun-resolution.test.ts new file mode 100644 index 00000000..eaf710c0 --- /dev/null +++ b/cli/src/__tests__/cmdrun-resolution.test.ts @@ -0,0 +1,288 @@ +import { describe, it, expect } from "bun:test"; +import { execSync } from "child_process"; +import { resolve } from "path"; + +/** + * Tests for cmdRun argument resolution paths: + * - Display name resolution ("Claude Code" -> "claude") + * - Case-insensitive key resolution ("Claude" -> "claude") + * - Argument swapping detection (cloud/agent -> agent/cloud) + * - showInfoOrError display name resolution ("Hetzner Cloud" -> cloud info) + * + * These paths in commands.ts cmdRun() (lines 252-304) and index.ts + * showInfoOrError() (lines 87-128) have zero E2E test coverage. + * + * Uses subprocess approach since cmdRun calls process.exit on errors. + * + * Agent: test-engineer + */ + +const CLI_DIR = resolve(import.meta.dir, "../.."); +const PROJECT_ROOT = resolve(CLI_DIR, ".."); + +function runCli( + args: string[], + env: Record = {} +): { stdout: string; stderr: string; exitCode: number } { + const quotedArgs = args.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(" "); + const cmd = `bun run ${CLI_DIR}/src/index.ts ${quotedArgs}`; + try { + const stdout = execSync(cmd, { + cwd: PROJECT_ROOT, + env: { + PATH: process.env.PATH, + HOME: process.env.HOME, + SHELL: process.env.SHELL, + TERM: process.env.TERM || "xterm", + ...env, + SPAWN_NO_UPDATE_CHECK: "1", + NODE_ENV: "", + BUN_ENV: "", + }, + encoding: "utf-8", + timeout: 15000, + stdio: ["pipe", "pipe", "pipe"], + }); + return { stdout, stderr: "", exitCode: 0 }; + } catch (err: any) { + return { + stdout: err.stdout || "", + stderr: err.stderr || "", + exitCode: err.status ?? 1, + }; + } +} + +// ── cmdRun: argument swapping detection ─────────────────────────────────── + +describe("cmdRun argument swapping", () => { + it("should detect swapped cloud/agent and show swap warning", () => { + // "spawn sprite claude" should be detected as swapped -> "spawn claude sprite" + // cmdRun will swap and try to launch, which will fail at download (no network) + // but the swap message should appear in output + const result = runCli(["sprite", "claude"]); + const output = result.stdout + result.stderr; + expect(output).toContain("swapped"); + }); + + it("should show corrected command after swap detection", () => { + const result = runCli(["sprite", "claude"]); + const output = result.stdout + result.stderr; + expect(output).toContain("spawn claude sprite"); + }); + + it("should swap hetzner/aider to aider/hetzner", () => { + const result = runCli(["hetzner", "aider"]); + const output = result.stdout + result.stderr; + expect(output).toContain("swapped"); + }); + + it("should not swap when arguments are in correct order", () => { + // "spawn claude sprite" is correct order - no swap message + const result = runCli(["claude", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).not.toContain("swapped"); + }); + + it("should not swap when both args are unknown", () => { + const result = runCli(["fakething", "otherfake"]); + const output = result.stdout + result.stderr; + expect(output).not.toContain("swapped"); + }); +}); + +// ── cmdRun: display name resolution ─────────────────────────────────────── + +describe("cmdRun display name resolution", () => { + it("should resolve case-insensitive agent key", () => { + // "Claude" should resolve to "claude" + const result = runCli(["Claude", "sprite"]); + const output = result.stdout + result.stderr; + // Should resolve and proceed (may show "Resolved" message) + // Should NOT show "Unknown agent" error + expect(output).not.toContain("Unknown agent"); + }); + + it("should resolve case-insensitive cloud key", () => { + // "Sprite" should resolve to "sprite" + const result = runCli(["claude", "Sprite"]); + const output = result.stdout + result.stderr; + expect(output).not.toContain("Unknown cloud"); + }); + + it("should show resolution message when name is resolved", () => { + // "CLAUDE" -> "claude" should trigger "Resolved" message + const result = runCli(["CLAUDE", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Resolved"); + expect(output).toContain("claude"); + }); + + it("should resolve agent display name to key", () => { + // "Claude Code" is the display name for agent key "claude" + const result = runCli(["Claude Code", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Resolved"); + expect(output).toContain("claude"); + }); + + it("should resolve cloud display name to key", () => { + // "Hetzner Cloud" is the display name for cloud key "hetzner" + const result = runCli(["claude", "Hetzner Cloud"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Resolved"); + expect(output).toContain("hetzner"); + }); + + it("should not show resolution message for exact key match", () => { + // "claude" is already the exact key - no resolution needed + const result = runCli(["claude", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).not.toContain("Resolved"); + }); + + it("should show unknown agent error for truly invalid agent", () => { + const result = runCli(["notarealagent", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Unknown agent"); + expect(result.exitCode).not.toBe(0); + }); + + it("should show unknown cloud error for truly invalid cloud", () => { + const result = runCli(["claude", "notarealcloud"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Unknown cloud"); + expect(result.exitCode).not.toBe(0); + }); +}); + +// ── showInfoOrError: display name resolution ────────────────────────────── + +describe("showInfoOrError display name resolution", () => { + it("should resolve agent display name to agent info", () => { + // "Claude Code" -> resolves to "claude" via resolveAgentKey -> shows agent info + const result = runCli(["Claude Code"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available clouds"); + expect(result.exitCode).toBe(0); + }); + + it("should resolve cloud display name to cloud info", () => { + // "Hetzner Cloud" -> resolves to "hetzner" via resolveCloudKey -> shows cloud info + const result = runCli(["Hetzner Cloud"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available agents"); + expect(result.exitCode).toBe(0); + }); + + it("should resolve case-insensitive agent display name", () => { + // "claude code" (lowercase) -> resolves to agent info + const result = runCli(["claude code"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available clouds"); + expect(result.exitCode).toBe(0); + }); + + it("should resolve case-insensitive cloud display name", () => { + // "hetzner cloud" (lowercase) -> resolves to cloud info + const result = runCli(["hetzner cloud"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available agents"); + expect(result.exitCode).toBe(0); + }); + + it("should resolve uppercase agent key", () => { + // "CLAUDE" -> resolves to "claude" key + const result = runCli(["CLAUDE"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available clouds"); + expect(result.exitCode).toBe(0); + }); + + it("should resolve uppercase cloud key", () => { + // "HETZNER" -> resolves to "hetzner" key + const result = runCli(["HETZNER"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available agents"); + expect(result.exitCode).toBe(0); + }); + + it("should resolve mixed case agent key", () => { + const result = runCli(["Aider"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Available clouds"); + expect(result.exitCode).toBe(0); + }); +}); + +// ── cmdRun: "did you mean" suggestions ──────────────────────────────────── + +describe("cmdRun did-you-mean suggestions", () => { + it("should suggest closest agent match for typo", () => { + // "claud" is close to "claude" (distance 1) + const result = runCli(["claud", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Did you mean"); + expect(output).toContain("claude"); + expect(result.exitCode).not.toBe(0); + }); + + it("should suggest closest cloud match for typo", () => { + // "sprte" is close to "sprite" (distance 1) + const result = runCli(["claude", "sprte"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Did you mean"); + expect(output).toContain("sprite"); + expect(result.exitCode).not.toBe(0); + }); + + it("should not suggest anything for completely different agent", () => { + const result = runCli(["kubernetes", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).toContain("Unknown agent"); + expect(output).not.toContain("Did you mean"); + expect(result.exitCode).not.toBe(0); + }); + + it("should show spawn agents hint for unknown agent", () => { + const result = runCli(["notreal", "sprite"]); + const output = result.stdout + result.stderr; + expect(output).toContain("spawn agents"); + }); + + it("should show spawn clouds hint for unknown cloud", () => { + const result = runCli(["claude", "notreal"]); + const output = result.stdout + result.stderr; + expect(output).toContain("spawn clouds"); + }); +}); + +// ── validateImplementation: not-implemented error paths ─────────────────── + +describe("cmdRun not-implemented error", () => { + it("should show not implemented error for missing matrix entry", () => { + // Find a known missing combination from the manifest + // We check a combination that exists in the manifest as "missing" + // This tests validateImplementation's error messaging + const result = runCli(["claude", "cherry-servers"]); + const output = result.stdout + result.stderr; + // Should either succeed (if implemented) or show useful error + // The key thing is it doesn't crash + if (result.exitCode !== 0) { + // If not implemented, should show helpful alternatives + expect(output.length).toBeGreaterThan(0); + } + }); + + it("should suggest alternative clouds when agent is not on specified cloud", () => { + // We need a cloud that exists but doesn't have all agents + // Test the "available on N clouds" message path + // Using a known agent with a cloud that may not have it + const result = runCli(["claude", "cherry-servers"]); + const output = result.stdout + result.stderr; + if (output.includes("not yet implemented")) { + // Should suggest alternative clouds + expect(output).toMatch(/available on|Try one of these/); + } + }); +});