mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 11:59:29 +00:00
- delete commands-update-download.test.ts (7 tests): superseded by cmd-update-cov.test.ts which has 13 tests with better fallback URL coverage and uses clack mocks properly - remove saveSpawnRecord id generation describe from history-cov.test.ts (1 test): superseded by history-spawn-id.test.ts which has 3 more thorough tests covering the same scenario - remove 4 describe blocks from cmd-run-cov.test.ts (18 tests): getSignalGuidance, getScriptFailureGuidance, getScriptFailureGuidance additional, and getSignalGuidance additional are all covered more thoroughly by the dedicated script-failure-guidance.test.ts; the "additional" blocks were theatrical (only checked joined.length > 0) - delete picker.test.ts and merge its 8 parsePickerInput tests into picker-cov.test.ts to eliminate duplicate describe name collision 2063 -> 2036 tests (-27), 0 failures Co-authored-by: spawn-qa-bot <qa@openrouter.ai> Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
210 lines
8.3 KiB
TypeScript
210 lines
8.3 KiB
TypeScript
/**
|
|
* cmd-run-cov.test.ts — Coverage tests for commands/run.ts
|
|
*
|
|
* Focuses on uncovered helper functions: resolveAndLog, detectAndFixSwappedArgs,
|
|
* dry-run helpers (buildAgentLines, buildCloudLines, buildCredentialStatusLines,
|
|
* buildEnvironmentLines, buildPromptLines), showDryRunPreview, classifyNetworkError,
|
|
* isRetryableExitCode, and headless output/error paths.
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
|
|
import { isString } from "@openrouter/spawn-shared";
|
|
import { _resetCacheForTesting, loadManifest } from "../manifest";
|
|
import { createConsoleMocks, createMockManifest, mockClackPrompts, restoreMocks } from "./test-helpers";
|
|
|
|
const clack = mockClackPrompts();
|
|
|
|
const { cmdRun, cmdRunHeadless, isRetryableExitCode } = await import("../commands/index.js");
|
|
const { showDryRunPreview } = await import("../commands/run.js");
|
|
|
|
describe("commands/run.ts coverage", () => {
|
|
let consoleMocks: ReturnType<typeof createConsoleMocks>;
|
|
let originalFetch: typeof global.fetch;
|
|
let processExitSpy: ReturnType<typeof spyOn>;
|
|
const mockManifest = createMockManifest();
|
|
|
|
beforeEach(async () => {
|
|
consoleMocks = createConsoleMocks();
|
|
originalFetch = global.fetch;
|
|
processExitSpy = spyOn(process, "exit").mockImplementation(() => {
|
|
throw new Error("process.exit called");
|
|
});
|
|
_resetCacheForTesting();
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
processExitSpy.mockRestore();
|
|
restoreMocks(consoleMocks.log, consoleMocks.error);
|
|
});
|
|
|
|
// ── isRetryableExitCode ───────────────────────────────────────────────
|
|
|
|
describe("isRetryableExitCode", () => {
|
|
it("returns true for exit code 255 (SSH failure)", () => {
|
|
expect(isRetryableExitCode("Script exited with code 255")).toBe(true);
|
|
});
|
|
|
|
it("returns false for exit code 1", () => {
|
|
expect(isRetryableExitCode("Script exited with code 1")).toBe(false);
|
|
});
|
|
|
|
it("returns false for exit code 130", () => {
|
|
expect(isRetryableExitCode("Script exited with code 130")).toBe(false);
|
|
});
|
|
|
|
it("returns false when no exit code found", () => {
|
|
expect(isRetryableExitCode("some random error")).toBe(false);
|
|
});
|
|
|
|
it("returns false for empty string", () => {
|
|
expect(isRetryableExitCode("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ── showDryRunPreview ─────────────────────────────────────────────────
|
|
|
|
describe("showDryRunPreview", () => {
|
|
it("prints agent, cloud, script sections", () => {
|
|
showDryRunPreview(mockManifest, "claude", "sprite");
|
|
expect(clack.logInfo).toHaveBeenCalled();
|
|
expect(clack.logSuccess).toHaveBeenCalled();
|
|
});
|
|
|
|
it("prints prompt section when provided", () => {
|
|
showDryRunPreview(mockManifest, "claude", "sprite", "Fix all bugs");
|
|
// prompt section is rendered via printDryRunSection which calls p.log.step
|
|
expect(clack.logStep).toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles long prompts with truncation", () => {
|
|
const longPrompt = "A".repeat(200);
|
|
showDryRunPreview(mockManifest, "claude", "sprite", longPrompt);
|
|
// Check that console.log was called (printDryRunSection outputs to console)
|
|
expect(consoleMocks.log).toHaveBeenCalled();
|
|
});
|
|
|
|
it("shows environment variables section when agent has env", () => {
|
|
showDryRunPreview(mockManifest, "claude", "sprite");
|
|
const allCalls = consoleMocks.log.mock.calls.flat().map(String);
|
|
const hasEnvLine = allCalls.some((c) => c.includes("ANTHROPIC_API_KEY") || c.includes("OpenRouter"));
|
|
expect(hasEnvLine).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── cmdRun with dry run ────────────────────────────────────────────────
|
|
|
|
describe("cmdRun dry run", () => {
|
|
it("shows dry run preview and returns", async () => {
|
|
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
|
|
await loadManifest(true);
|
|
await cmdRun("claude", "sprite", undefined, true);
|
|
expect(clack.logSuccess).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ── cmdRun with swapped arguments ─────────────────────────────────────
|
|
|
|
describe("cmdRun detectAndFixSwappedArgs", () => {
|
|
it("detects and fixes swapped agent/cloud arguments in dry run", async () => {
|
|
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
|
|
await loadManifest(true);
|
|
// Pass cloud name as agent, agent name as cloud
|
|
await cmdRun("sprite", "claude", undefined, true);
|
|
// Should still succeed as dry run (swap detection fixes it)
|
|
expect(clack.logInfo).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ── cmdRun additional ─────────────────────────────────────────────
|
|
|
|
describe("cmdRun validation", () => {
|
|
it("validates agent and cloud names exist", async () => {
|
|
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
|
|
await loadManifest(true);
|
|
await expect(cmdRun("nonexistent", "sprite")).rejects.toThrow("process.exit");
|
|
});
|
|
|
|
it("validates implementation status", async () => {
|
|
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
|
|
await loadManifest(true);
|
|
// hetzner/codex is "missing" in mock manifest
|
|
await expect(cmdRun("codex", "hetzner")).rejects.toThrow("process.exit");
|
|
});
|
|
});
|
|
|
|
// ── cmdRunHeadless ─────────────────────────────────────────────────────
|
|
|
|
describe("cmdRunHeadless", () => {
|
|
it("exits with code 3 for invalid agent name", async () => {
|
|
await expect(
|
|
cmdRunHeadless("../bad", "sprite", {
|
|
outputFormat: "json",
|
|
}),
|
|
).rejects.toThrow("process.exit");
|
|
expect(processExitSpy).toHaveBeenCalledWith(3);
|
|
});
|
|
|
|
it("exits with code 3 for invalid cloud name", async () => {
|
|
await expect(
|
|
cmdRunHeadless("claude", "../bad", {
|
|
outputFormat: "json",
|
|
}),
|
|
).rejects.toThrow("process.exit");
|
|
expect(processExitSpy).toHaveBeenCalledWith(3);
|
|
});
|
|
|
|
it("exits with code 3 when manifest fetch fails", async () => {
|
|
global.fetch = mock(
|
|
async () =>
|
|
new Response("error", {
|
|
status: 500,
|
|
}),
|
|
);
|
|
await expect(
|
|
cmdRunHeadless("claude", "sprite", {
|
|
outputFormat: "json",
|
|
}),
|
|
).rejects.toThrow("process.exit");
|
|
});
|
|
|
|
it("outputs JSON for errors when outputFormat is json", async () => {
|
|
await expect(
|
|
cmdRunHeadless("../bad", "sprite", {
|
|
outputFormat: "json",
|
|
}),
|
|
).rejects.toThrow("process.exit");
|
|
const jsonCalls = consoleMocks.log.mock.calls.flat().filter((c) => isString(c) && c.includes("VALIDATION_ERROR"));
|
|
expect(jsonCalls.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("outputs plain text for errors without json format", async () => {
|
|
await expect(cmdRunHeadless("../bad", "sprite")).rejects.toThrow("process.exit");
|
|
const errorCalls = consoleMocks.error.mock.calls.flat().map(String);
|
|
const hasError = errorCalls.some((c) => c.includes("Error"));
|
|
expect(hasError).toBe(true);
|
|
});
|
|
|
|
it("exits with code 3 for unknown agent", async () => {
|
|
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
|
|
await loadManifest(true);
|
|
await expect(
|
|
cmdRunHeadless("nonexistent", "sprite", {
|
|
outputFormat: "json",
|
|
}),
|
|
).rejects.toThrow("process.exit");
|
|
expect(processExitSpy).toHaveBeenCalledWith(3);
|
|
});
|
|
|
|
it("exits with code 3 for not-implemented matrix entry", async () => {
|
|
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
|
|
await loadManifest(true);
|
|
await expect(
|
|
cmdRunHeadless("codex", "hetzner", {
|
|
outputFormat: "json",
|
|
}),
|
|
).rejects.toThrow("process.exit");
|
|
expect(processExitSpy).toHaveBeenCalledWith(3);
|
|
});
|
|
});
|
|
});
|