diff --git a/cli/src/__tests__/cmd-help-content.test.ts b/cli/src/__tests__/cmd-help-content.test.ts new file mode 100644 index 00000000..dd9fbcca --- /dev/null +++ b/cli/src/__tests__/cmd-help-content.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"; +import { createConsoleMocks, restoreMocks } from "./test-helpers"; + +/** + * Tests for cmdHelp output completeness in commands.ts. + * + * Existing tests only verify cmdHelp produces some output containing "USAGE" + * and "EXAMPLES" (commands.test.ts). This file verifies that all documented + * subcommands, flags, sections, and key content are present in the help text. + * + * This is important because: + * - Users rely on `spawn help` as the primary reference + * - Missing subcommands or flags lead to support requests + * - The help text must stay in sync with actual CLI capabilities + * + * Agent: test-engineer + */ + +// Mock @clack/prompts to prevent side effects +mock.module("@clack/prompts", () => ({ + spinner: () => ({ + start: mock(() => {}), + stop: mock(() => {}), + message: mock(() => {}), + }), + log: { + step: mock(() => {}), + info: mock(() => {}), + warn: mock(() => {}), + error: mock(() => {}), + success: mock(() => {}), + }, + intro: mock(() => {}), + outro: mock(() => {}), + cancel: mock(() => {}), + select: mock(() => {}), + isCancel: () => false, +})); + +const { cmdHelp } = await import("../commands.js"); + +describe("cmdHelp - content completeness", () => { + let consoleMocks: ReturnType; + + beforeEach(() => { + consoleMocks = createConsoleMocks(); + }); + + afterEach(() => { + restoreMocks(consoleMocks.log, consoleMocks.error); + }); + + function getHelpOutput(): string { + cmdHelp(); + return consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); + } + + // ── Required sections ────────────────────────────────────────────── + + describe("required sections", () => { + it("should have a USAGE section", () => { + expect(getHelpOutput()).toContain("USAGE"); + }); + + it("should have an EXAMPLES section", () => { + expect(getHelpOutput()).toContain("EXAMPLES"); + }); + + it("should have an AUTHENTICATION section", () => { + expect(getHelpOutput()).toContain("AUTHENTICATION"); + }); + + it("should have an INSTALL section", () => { + expect(getHelpOutput()).toContain("INSTALL"); + }); + + it("should have a TROUBLESHOOTING section", () => { + expect(getHelpOutput()).toContain("TROUBLESHOOTING"); + }); + + it("should have a MORE INFO section", () => { + expect(getHelpOutput()).toContain("MORE INFO"); + }); + }); + + // ── Documented subcommands ───────────────────────────────────────── + + describe("subcommands in USAGE", () => { + it("should document spawn list subcommand", () => { + expect(getHelpOutput()).toContain("spawn list"); + }); + + it("should document spawn agents subcommand", () => { + expect(getHelpOutput()).toContain("spawn agents"); + }); + + it("should document spawn clouds subcommand", () => { + expect(getHelpOutput()).toContain("spawn clouds"); + }); + + it("should document spawn update subcommand", () => { + expect(getHelpOutput()).toContain("spawn update"); + }); + + it("should document spawn version subcommand", () => { + expect(getHelpOutput()).toContain("spawn version"); + }); + + it("should document spawn help subcommand", () => { + expect(getHelpOutput()).toContain("spawn help"); + }); + + it("should document the ls alias for list", () => { + const output = getHelpOutput(); + expect(output).toContain("ls"); + }); + }); + + // ── Documented flags ─────────────────────────────────────────────── + + describe("flags in USAGE", () => { + it("should document --prompt flag", () => { + expect(getHelpOutput()).toContain("--prompt"); + }); + + it("should document -p short form for --prompt", () => { + expect(getHelpOutput()).toContain("-p"); + }); + + it("should document --prompt-file flag", () => { + expect(getHelpOutput()).toContain("--prompt-file"); + }); + }); + + // ── Examples ─────────────────────────────────────────────────────── + + describe("examples", () => { + it("should show interactive usage example", () => { + const output = getHelpOutput(); + // "spawn" alone for interactive + expect(output).toMatch(/spawn\s+#.*[Ii]nteractive/); + }); + + it("should show direct launch example with agent and cloud", () => { + const output = getHelpOutput(); + expect(output).toContain("spawn claude sprite"); + }); + + it("should show --prompt example", () => { + const output = getHelpOutput(); + expect(output).toContain("--prompt"); + }); + + it("should show --prompt-file example", () => { + const output = getHelpOutput(); + expect(output).toContain("--prompt-file"); + }); + + it("should show agent info example", () => { + const output = getHelpOutput(); + // e.g., "spawn claude # Show which clouds support Claude" + expect(output).toMatch(/spawn claude\s+#/); + }); + }); + + // ── Authentication info ──────────────────────────────────────────── + + describe("authentication information", () => { + it("should mention OpenRouter", () => { + expect(getHelpOutput()).toContain("OpenRouter"); + }); + + it("should include OpenRouter API key URL", () => { + expect(getHelpOutput()).toContain("openrouter.ai/settings/keys"); + }); + + it("should mention OPENROUTER_API_KEY env var", () => { + expect(getHelpOutput()).toContain("OPENROUTER_API_KEY"); + }); + }); + + // ── Install section ──────────────────────────────────────────────── + + describe("install instructions", () => { + it("should include curl install command", () => { + expect(getHelpOutput()).toContain("curl -fsSL"); + }); + + it("should include install.sh path", () => { + expect(getHelpOutput()).toContain("install.sh"); + }); + }); + + // ── Troubleshooting ──────────────────────────────────────────────── + + describe("troubleshooting tips", () => { + it("should mention spawn list for script not found", () => { + const output = getHelpOutput(); + expect(output).toContain("spawn list"); + }); + + it("should mention SPAWN_NO_UNICODE for garbled output", () => { + expect(getHelpOutput()).toContain("SPAWN_NO_UNICODE"); + }); + + it("should mention SPAWN_NO_UPDATE_CHECK for slow startup", () => { + expect(getHelpOutput()).toContain("SPAWN_NO_UPDATE_CHECK"); + }); + }); + + // ── Links ────────────────────────────────────────────────────────── + + describe("repository links", () => { + it("should include GitHub repository URL", () => { + expect(getHelpOutput()).toContain("github.com"); + }); + + it("should include OpenRouter URL", () => { + expect(getHelpOutput()).toContain("openrouter.ai"); + }); + }); +}); diff --git a/cli/src/__tests__/commands-update-download.test.ts b/cli/src/__tests__/commands-update-download.test.ts index 01dae46c..5e3b3441 100644 --- a/cli/src/__tests__/commands-update-download.test.ts +++ b/cli/src/__tests__/commands-update-download.test.ts @@ -281,8 +281,8 @@ describe("Script download and execution", () => { await expect(cmdRun("claude", "sprite")).rejects.toThrow("process.exit"); expect(processExitSpy).toHaveBeenCalledWith(1); - const errorOutput = consoleMocks.error.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); - expect(errorOutput).toContain("HTTP 500"); + const logErrorOutput = mockLogError.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); + expect(logErrorOutput).toContain("HTTP 500"); }); it("should show troubleshooting info when download throws network error", async () => { @@ -307,7 +307,7 @@ describe("Script download and execution", () => { } const errorOutput = consoleMocks.error.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); - expect(errorOutput).toContain("Troubleshooting"); + expect(errorOutput).toContain("How to fix"); }); it("should use fallback URL when primary returns non-OK status", async () => { diff --git a/cli/src/__tests__/commands.test.ts b/cli/src/__tests__/commands.test.ts index 6d41ab57..d2bb1a05 100644 --- a/cli/src/__tests__/commands.test.ts +++ b/cli/src/__tests__/commands.test.ts @@ -43,60 +43,10 @@ describe("commands", () => { }); }); - // TODO: These tests need refactoring - bun doesn't support module mocking - // Commands.ts should be refactored to use dependency injection for testability - - describe.skip("cmdList - needs dependency injection", () => { - it("should display matrix table with all agents and clouds", async () => { - // Skipped: requires module mocking unsupported by bun - }); - }); - - describe.skip("cmdAgents - needs dependency injection", () => { - it("should list all agents with descriptions", async () => { - // Skipped: requires module mocking unsupported by bun - }); - }); - - describe.skip("cmdClouds - needs dependency injection", () => { - it("should list all clouds with descriptions", async () => { - // Skipped: requires module mocking unsupported by bun - }); - }); - - describe.skip("cmdAgentInfo - needs dependency injection", () => { - it("should show info for a valid agent with implemented clouds", async () => { - // Skipped: requires module mocking unsupported by bun - }); - - it("should show no clouds message when agent has no implementations", async () => { - // Skipped: requires module mocking unsupported by bun - }); - - it("should exit with error for unknown agent", async () => { - // Skipped: requires module mocking unsupported by bun - }); - }); - - describe.skip("cmdRun - needs dependency injection", () => { - it("should launch script for valid agent and cloud", async () => { - // Skipped: requires module mocking unsupported by bun - }); - - it("should exit with error for unknown agent", async () => { - // Skipped: requires module mocking unsupported by bun - }); - - it("should exit with error for unknown cloud", async () => { - // Skipped: requires module mocking unsupported by bun - }); - - it("should exit with error for unimplemented combination", async () => { - // Skipped: requires module mocking unsupported by bun - }); - - it("should fallback to GitHub raw URL when primary URL fails", async () => { - // Skipped: requires module mocking unsupported by bun - }); - }); + // These functions are tested in dedicated files using mock.module(): + // - cmdList: commands-list-grid.test.ts, commands-compact-list.test.ts + // - cmdAgents/cmdClouds: commands-display.test.ts, commands-output.test.ts, commands-list-grid.test.ts + // - cmdAgentInfo/cmdCloudInfo: commands-info-details.test.ts, commands-display.test.ts, cloud-info.test.ts + // - cmdRun: commands-resolve-run.test.ts, commands-swap-resolve.test.ts, cmdrun-resolution.test.ts + // - Download/failure: download-and-failure.test.ts, commands-update-download.test.ts }); diff --git a/cli/src/__tests__/download-and-failure.test.ts b/cli/src/__tests__/download-and-failure.test.ts index 352caa88..3bbb726e 100644 --- a/cli/src/__tests__/download-and-failure.test.ts +++ b/cli/src/__tests__/download-and-failure.test.ts @@ -279,8 +279,8 @@ describe("Download and Failure Pipeline", () => { // Expected } - const errorOutput = consoleMocks.error.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); - expect(errorOutput).toContain("HTTP 500"); + const logErrorOutput = mockLogError.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); + expect(logErrorOutput).toContain("HTTP 500"); }); it("should mention temporary server issues on 500 errors", async () => { @@ -313,9 +313,10 @@ describe("Download and Failure Pipeline", () => { } // Should show HTTP error (not the "script not found" path) - const errorOutput = consoleMocks.error.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); - expect(errorOutput).toContain("HTTP 404"); + const logErrorOutput = mockLogError.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); + expect(logErrorOutput).toContain("HTTP 404"); // 500 from fallback should mention temporary issues + const errorOutput = consoleMocks.error.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); expect(errorOutput).toContain("temporary issues"); }); }); @@ -352,7 +353,7 @@ describe("Download and Failure Pipeline", () => { } const errorOutput = consoleMocks.error.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); - expect(errorOutput).toContain("Troubleshooting"); + expect(errorOutput).toContain("How to fix"); expect(errorOutput).toContain("internet connection"); });