diff --git a/packages/cli/src/__tests__/cmd-run-cov.test.ts b/packages/cli/src/__tests__/cmd-run-cov.test.ts index df88ec94..b47680ec 100644 --- a/packages/cli/src/__tests__/cmd-run-cov.test.ts +++ b/packages/cli/src/__tests__/cmd-run-cov.test.ts @@ -4,7 +4,7 @@ * Focuses on uncovered helper functions: resolveAndLog, detectAndFixSwappedArgs, * dry-run helpers (buildAgentLines, buildCloudLines, buildCredentialStatusLines, * buildEnvironmentLines, buildPromptLines), showDryRunPreview, classifyNetworkError, - * isRetryableExitCode, headless output/error, and execScript paths. + * isRetryableExitCode, and headless output/error paths. */ import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; @@ -14,14 +14,8 @@ import { createConsoleMocks, createMockManifest, mockClackPrompts, restoreMocks const clack = mockClackPrompts(); -const { cmdRun, cmdRunHeadless, getScriptFailureGuidance, getSignalGuidance, isRetryableExitCode } = await import( - "../commands/index.js" -); -const { showDryRunPreview, execScript } = await import("../commands/run.js"); - -function stripAnsi(s: string): string { - return s.replace(/\x1b\[[0-9;]*m/g, ""); -} +const { cmdRun, cmdRunHeadless, isRetryableExitCode } = await import("../commands/index.js"); +const { showDryRunPreview } = await import("../commands/run.js"); describe("commands/run.ts coverage", () => { let consoleMocks: ReturnType; @@ -122,162 +116,6 @@ describe("commands/run.ts coverage", () => { }); }); - // ── getSignalGuidance ────────────────────────────────────────────────── - - describe("getSignalGuidance", () => { - it("returns guidance for SIGKILL", () => { - const lines = getSignalGuidance("SIGKILL").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("killed"); - }); - - it("returns default guidance for unknown signal", () => { - const lines = getSignalGuidance("SIGUSR1").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("SIGUSR1"); - expect(joined).toContain("terminated"); - }); - - it("includes dashboard hint when provided", () => { - const lines = getSignalGuidance("SIGUSR1", "https://dash.example.com").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("https://dash.example.com"); - }); - }); - - // ── getScriptFailureGuidance ────────────────────────────────────────── - - describe("getScriptFailureGuidance", () => { - it("returns default guidance for null exit code", () => { - const lines = getScriptFailureGuidance(null, "hetzner").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("Common causes"); - }); - - it("returns default guidance for unknown exit codes", () => { - const lines = getScriptFailureGuidance(99, "hetzner").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("Common causes"); - }); - - it("includes dashboard hint for exit code 130", () => { - const lines = getScriptFailureGuidance(130, "hetzner", undefined, "https://dash.test").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("interrupted"); - }); - - it("includes credential hints for exit code 1", () => { - const lines = getScriptFailureGuidance(1, "hetzner", "Set HCLOUD_TOKEN").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("HCLOUD_TOKEN"); - }); - - it("handles exit code 137 (OOM/killed)", () => { - const lines = getScriptFailureGuidance(137, "sprite").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("killed"); - }); - }); - - // ── classifyNetworkError (via reportDownloadError path) ────────────── - - describe("classifyNetworkError", () => { - it("handles timeout error in dry run preview", () => { - showDryRunPreview(mockManifest, "claude", "sprite"); - // Just verify it completes without throwing - expect(clack.logInfo).toHaveBeenCalled(); - }); - }); - - // ── showDryRunPreview with cloud defaults ────────────────────────── - - describe("showDryRunPreview edge cases", () => { - it("shows credential status for sprite (token auth)", () => { - showDryRunPreview(mockManifest, "claude", "sprite"); - const allCalls = consoleMocks.log.mock.calls.flat().map(String); - expect(allCalls.some((c) => c.includes("OPENROUTER_API_KEY"))).toBe(true); - }); - - it("shows credential status for hetzner (token auth)", () => { - showDryRunPreview(mockManifest, "claude", "hetzner"); - const allCalls = consoleMocks.log.mock.calls.flat().map(String); - expect(allCalls.some((c) => c.includes("OPENROUTER_API_KEY"))).toBe(true); - }); - - it("shows no environment lines when agent has no env", () => { - const noEnvManifest = { - ...mockManifest, - agents: { - ...mockManifest.agents, - noenv: { - name: "NoEnv Agent", - description: "Agent without env", - url: "https://example.com", - install: "npm install noenv", - launch: "noenv", - }, - }, - matrix: { - ...mockManifest.matrix, - "sprite/noenv": "implemented", - }, - }; - global.fetch = mock(async () => new Response(JSON.stringify(noEnvManifest))); - showDryRunPreview(noEnvManifest, "noenv", "sprite"); - expect(clack.logSuccess).toHaveBeenCalled(); - }); - }); - - // ── getScriptFailureGuidance edge cases ─────────────────────────── - - describe("getScriptFailureGuidance additional", () => { - it("handles exit code 2 (misuse of shell builtin)", () => { - const lines = getScriptFailureGuidance(2, "sprite").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined.length).toBeGreaterThan(0); - }); - - it("handles exit code 126 (permission denied)", () => { - const lines = getScriptFailureGuidance(126, "hetzner").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined.length).toBeGreaterThan(0); - }); - - it("handles exit code 127 (command not found)", () => { - const lines = getScriptFailureGuidance(127, "hetzner").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined.length).toBeGreaterThan(0); - }); - - it("handles exit code 255 (SSH error)", () => { - const lines = getScriptFailureGuidance(255, "aws").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined.length).toBeGreaterThan(0); - }); - - it("handles exit code 1 with no auth hint", () => { - const lines = getScriptFailureGuidance(1, "sprite").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("Cloud provider API error"); - }); - }); - - // ── getSignalGuidance additional ────────────────────────────────── - - describe("getSignalGuidance additional", () => { - it("returns guidance for SIGTERM", () => { - const lines = getSignalGuidance("SIGTERM").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("terminated"); - }); - - it("returns guidance for unknown signal without dashboard", () => { - const lines = getSignalGuidance("SIGXCPU").map(stripAnsi); - const joined = lines.join("\n"); - expect(joined).toContain("SIGXCPU"); - }); - }); - // ── cmdRun additional ───────────────────────────────────────────── describe("cmdRun validation", () => { diff --git a/packages/cli/src/__tests__/commands-update-download.test.ts b/packages/cli/src/__tests__/commands-update-download.test.ts deleted file mode 100644 index d8c5a16a..00000000 --- a/packages/cli/src/__tests__/commands-update-download.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; -import { isString } from "@openrouter/spawn-shared"; -import pkg from "../../package.json" with { type: "json" }; -import { createConsoleMocks, mockClackPrompts, restoreMocks } from "./test-helpers"; - -const VERSION = pkg.version; - -/** - * Tests for cmdUpdate (commands/update.ts). - * - * Uses dependency injection (UpdateOptions.runUpdate) instead of mock.module - * for node:child_process to avoid process-global mock pollution. - */ - -const { spinnerStart: mockSpinnerStart, spinnerStop: mockSpinnerStop } = mockClackPrompts(); - -// ── Import commands directly (no mock.module needed) ────────────────────── -import { cmdUpdate } from "../commands/index.js"; - -/** No-op runUpdate to prevent real subprocess calls in tests. */ -const mockRunUpdate = mock(() => {}); - -describe("cmdUpdate", () => { - let consoleMocks: ReturnType; - let originalFetch: typeof global.fetch; - let processExitSpy: ReturnType; - - beforeEach(async () => { - consoleMocks = createConsoleMocks(); - mockSpinnerStart.mockClear(); - mockSpinnerStop.mockClear(); - mockRunUpdate.mockClear(); - - processExitSpy = spyOn(process, "exit").mockImplementation(() => { - throw new Error("process.exit"); - }); - - originalFetch = global.fetch; - }); - - afterEach(() => { - global.fetch = originalFetch; - processExitSpy.mockRestore(); - restoreMocks(consoleMocks.log, consoleMocks.error); - }); - - it("should report up-to-date when remote version matches current", async () => { - global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("/version")) { - return new Response(`${VERSION}\n`); - } - return new Response("Not Found", { - status: 404, - }); - }); - - await cmdUpdate({ - runUpdate: mockRunUpdate, - }); - - expect(mockSpinnerStart).toHaveBeenCalled(); - expect(mockSpinnerStop).toHaveBeenCalled(); - // The spinner stop message should indicate up-to-date - const stopCalls = mockSpinnerStop.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(stopCalls.some((msg: string) => msg.includes("up to date"))).toBe(true); - }); - - it("should report available update when remote version differs", async () => { - global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("/version")) { - return new Response("99.99.99\n"); - } - return new Response("Not Found", { - status: 404, - }); - }); - - await cmdUpdate({ - runUpdate: mockRunUpdate, - }); - - expect(mockSpinnerStart).toHaveBeenCalled(); - // Should show update message with version transition - const stopCalls = mockSpinnerStop.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(stopCalls.some((msg: string) => msg.includes("99.99.99"))).toBe(true); - }); - - it("should handle package.json fetch failure gracefully", async () => { - global.fetch = mock( - async () => - new Response("Internal Server Error", { - status: 500, - }), - ); - - await cmdUpdate({ - runUpdate: mockRunUpdate, - }); - - expect(mockSpinnerStart).toHaveBeenCalled(); - // Should show failed message - const stopCalls = mockSpinnerStop.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(stopCalls.some((msg: string) => msg.includes("Failed"))).toBe(true); - // Should output error details - const errorOutput = consoleMocks.error.mock.calls.map((c: unknown[]) => c.join(" ")).join("\n"); - expect(errorOutput).toContain("Error:"); - }); - - it("should handle network error gracefully", async () => { - global.fetch = mock(async () => { - throw new TypeError("Failed to fetch"); - }); - - await cmdUpdate({ - runUpdate: mockRunUpdate, - }); - - expect(mockSpinnerStart).toHaveBeenCalled(); - const stopCalls = mockSpinnerStop.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(stopCalls.some((msg: string) => msg.includes("Failed"))).toBe(true); - }); - - it("should handle update failure gracefully", async () => { - global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("/version")) { - return new Response("99.99.99\n"); - } - return new Response("Not Found", { - status: 404, - }); - }); - - // Mock runUpdate that throws to simulate failure - const failingRunUpdate = mock(() => { - throw new Error("curl failed"); - }); - - await cmdUpdate({ - runUpdate: failingRunUpdate, - }); - - // Should show the update version in spinner stop - const stopCalls = mockSpinnerStop.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(stopCalls.some((msg: string) => msg.includes("99.99.99"))).toBe(true); - }); - - it("should start spinner with checking message", async () => { - global.fetch = mock( - async () => - new Response( - JSON.stringify({ - version: VERSION, - }), - ), - ); - - await cmdUpdate({ - runUpdate: mockRunUpdate, - }); - - const startCalls = mockSpinnerStart.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(startCalls.some((msg: string) => msg.includes("Checking"))).toBe(true); - }); - - it("should show version in spinner stop during update", async () => { - global.fetch = mock(async (url: string) => { - if (isString(url) && url.includes("/version")) { - return new Response("2.0.0\n"); - } - return new Response("Error", { - status: 500, - }); - }); - - await cmdUpdate({ - runUpdate: mockRunUpdate, - }); - - // cmdUpdate now uses s.stop() with version info instead of s.message() - const stopCalls = mockSpinnerStop.mock.calls.map((c: unknown[]) => c.join(" ")); - expect(stopCalls.some((msg: string) => msg.includes("2.0.0"))).toBe(true); - }); -}); diff --git a/packages/cli/src/__tests__/history-cov.test.ts b/packages/cli/src/__tests__/history-cov.test.ts index 9975b44a..279f1db3 100644 --- a/packages/cli/src/__tests__/history-cov.test.ts +++ b/packages/cli/src/__tests__/history-cov.test.ts @@ -662,24 +662,6 @@ describe("history.ts coverage", () => { }); }); - // ── saveSpawnRecord auto-generates id ───────────────────────────────── - - describe("saveSpawnRecord id generation", () => { - it("auto-generates id if not provided", () => { - const recordWithoutId: SpawnRecord = { - id: "", - agent: "claude", - cloud: "sprite", - timestamp: "2026-01-01T00:00:00Z", - }; - saveSpawnRecord(recordWithoutId); - - const data = JSON.parse(readFileSync(join(testDir, "history.json"), "utf-8")); - expect(data.records[0].id).toBeTruthy(); - expect(typeof data.records[0].id).toBe("string"); - }); - }); - // ── Trimming with archiving ─────────────────────────────────────────── describe("trimming and archiving", () => { diff --git a/packages/cli/src/__tests__/picker-cov.test.ts b/packages/cli/src/__tests__/picker-cov.test.ts index f0dd9dec..04907903 100644 --- a/packages/cli/src/__tests__/picker-cov.test.ts +++ b/packages/cli/src/__tests__/picker-cov.test.ts @@ -14,7 +14,108 @@ describe("picker.ts coverage", () => { // ── parsePickerInput extended ───────────────────────────────────────── describe("parsePickerInput", () => { - it("handles tabs within values", () => { + it("parses three-field tab-separated lines (value, label, hint)", () => { + const result = parsePickerInput("us-east-1\tVirginia\tRecommended"); + expect(result).toEqual([ + { + value: "us-east-1", + label: "Virginia", + hint: "Recommended", + }, + ]); + }); + + it("parses two-field lines (value, label) with no hint", () => { + const result = parsePickerInput("us-east-1\tVirginia"); + expect(result).toEqual([ + { + value: "us-east-1", + label: "Virginia", + }, + ]); + }); + + it("uses value as label when only value is provided", () => { + const result = parsePickerInput("us-east-1"); + expect(result).toEqual([ + { + value: "us-east-1", + label: "us-east-1", + }, + ]); + }); + + it("filters empty and whitespace-only lines", () => { + const result = parsePickerInput("a\tAlpha\n\n \nb\tBeta\n"); + expect(result).toEqual([ + { + value: "a", + label: "Alpha", + }, + { + value: "b", + label: "Beta", + }, + ]); + }); + + it("handles mixed field counts in a single input", () => { + const input = [ + "val1\tLabel1\tHint1", + "val2\tLabel2", + "val3", + ].join("\n"); + const result = parsePickerInput(input); + expect(result).toEqual([ + { + value: "val1", + label: "Label1", + hint: "Hint1", + }, + { + value: "val2", + label: "Label2", + }, + { + value: "val3", + label: "val3", + }, + ]); + }); + + it("returns empty array for empty input", () => { + expect(parsePickerInput("")).toEqual([]); + expect(parsePickerInput(" ")).toEqual([]); + expect(parsePickerInput("\n\n")).toEqual([]); + }); + + it("trims whitespace from fields", () => { + const result = parsePickerInput(" val \t Label \t Hint "); + expect(result).toEqual([ + { + value: "val", + label: "Label", + hint: "Hint", + }, + ]); + }); + + it("parses multiple lines correctly", () => { + const input = "us-central1-a\tIowa\nus-east1-b\tVirginia"; + const result = parsePickerInput(input); + expect(result).toEqual([ + { + value: "us-central1-a", + label: "Iowa", + }, + { + value: "us-east1-b", + label: "Virginia", + }, + ]); + }); + + it("handles tabs within values (extra fields beyond 3 are ignored)", () => { const result = parsePickerInput("a\tb\tc\td"); expect(result).toHaveLength(1); expect(result[0].value).toBe("a"); diff --git a/packages/cli/src/__tests__/picker.test.ts b/packages/cli/src/__tests__/picker.test.ts deleted file mode 100644 index 810e5f94..00000000 --- a/packages/cli/src/__tests__/picker.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { parsePickerInput } from "../picker"; - -describe("parsePickerInput", () => { - it("parses three-field tab-separated lines (value, label, hint)", () => { - const result = parsePickerInput("us-east-1\tVirginia\tRecommended"); - expect(result).toEqual([ - { - value: "us-east-1", - label: "Virginia", - hint: "Recommended", - }, - ]); - }); - - it("parses two-field lines (value, label) with no hint", () => { - const result = parsePickerInput("us-east-1\tVirginia"); - expect(result).toEqual([ - { - value: "us-east-1", - label: "Virginia", - }, - ]); - }); - - it("uses value as label when only value is provided", () => { - const result = parsePickerInput("us-east-1"); - expect(result).toEqual([ - { - value: "us-east-1", - label: "us-east-1", - }, - ]); - }); - - it("filters empty and whitespace-only lines", () => { - const result = parsePickerInput("a\tAlpha\n\n \nb\tBeta\n"); - expect(result).toEqual([ - { - value: "a", - label: "Alpha", - }, - { - value: "b", - label: "Beta", - }, - ]); - }); - - it("handles mixed field counts in a single input", () => { - const input = [ - "val1\tLabel1\tHint1", - "val2\tLabel2", - "val3", - ].join("\n"); - const result = parsePickerInput(input); - expect(result).toEqual([ - { - value: "val1", - label: "Label1", - hint: "Hint1", - }, - { - value: "val2", - label: "Label2", - }, - { - value: "val3", - label: "val3", - }, - ]); - }); - - it("returns empty array for empty input", () => { - expect(parsePickerInput("")).toEqual([]); - expect(parsePickerInput(" ")).toEqual([]); - expect(parsePickerInput("\n\n")).toEqual([]); - }); - - it("trims whitespace from fields", () => { - const result = parsePickerInput(" val \t Label \t Hint "); - expect(result).toEqual([ - { - value: "val", - label: "Label", - hint: "Hint", - }, - ]); - }); - - it("parses multiple lines correctly", () => { - const input = "us-central1-a\tIowa\nus-east1-b\tVirginia"; - const result = parsePickerInput(input); - expect(result).toEqual([ - { - value: "us-central1-a", - label: "Iowa", - }, - { - value: "us-east1-b", - label: "Virginia", - }, - ]); - }); -});