mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-09 19:49:58 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
5ff178fb12
commit
bca768ecd2
1 changed files with 288 additions and 0 deletions
288
cli/src/__tests__/cmdrun-resolution.test.ts
Normal file
288
cli/src/__tests__/cmdrun-resolution.test.ts
Normal file
|
|
@ -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<string, string> = {}
|
||||
): { 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/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue