test: Remove 5 duplicate and theatrical test files (#1943)

* test: remove 5 duplicate and theatrical test files

Remove test files that are fully duplicated by more comprehensive
counterparts, plus one theatrical test that only grep-checks shell
script text without testing behavior.

Duplicates removed:
- manifest-validation.test.ts (subset of manifest-cache-lifecycle.test.ts)
- matrix-compact-footer.test.ts (subset of commands-exported-utils.test.ts)
- commands-output.test.ts (subset of commands-display.test.ts)
- cloud-info.test.ts (subset of commands-cloud-info.test.ts)

Theatrical test removed:
- install-script-validation.test.ts (reads install.sh as string, checks
  substring presence -- tests that functions "exist" not that they work)

All 1657 remaining tests pass. Zero regressions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: Fix always-pass pattern and stale comments

- integration.test.ts: remove conditional `if (cacheExists)` block that
  silently skipped the cache-file assertion when the file wasn't written;
  the second loadManifest() call already exercises in-memory caching
  without needing the conditional; remove now-unused readFileSync/existsSync
  imports
- commands.test.ts: remove stale references to cloud-info.test.ts and
  commands-output.test.ts (deleted in prior commit) from inline comment;
  remove unused createMockManifest import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-26 00:54:15 -08:00 committed by GitHub
parent fe6fd20143
commit 433708709c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 5 additions and 1728 deletions

View file

@ -1,348 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { loadManifest } from "../manifest";
import type { Manifest } from "../manifest";
/**
* Tests for cmdCloudInfo in commands.ts.
*
* cmdCloudInfo is the only major command function with zero test coverage.
* It handles "spawn <cloud>" to show available agents for a cloud provider.
*
* Covers:
* - Happy path: display cloud name, description, available agents
* - Cloud with notes field
* - Cloud with no implemented agents
* - Error paths: invalid identifier, unknown cloud, empty/whitespace name
* - Typo suggestion for unknown cloud names
*
* Agent: test-engineer
*/
const mockManifest = createMockManifest();
// Extended manifest with a cloud that has notes and a cloud with no agents
const extendedManifest: Manifest = {
agents: mockManifest.agents,
clouds: {
...mockManifest.clouds,
railway: {
name: "Railway",
description: "Container platform",
url: "https://railway.app",
type: "container",
auth: "token",
provision_method: "cli",
exec_method: "exec",
interactive_method: "exec",
notes: "Requires Railway CLI installed locally",
},
emptycloud: {
name: "Empty Cloud",
description: "No agents here",
url: "https://empty.example.com",
type: "cloud",
auth: "token",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
local: {
name: "Local Machine",
description: "Run agents locally",
url: "https://github.com/OpenRouterTeam/spawn",
type: "local",
auth: "none",
provision_method: "none",
exec_method: "bash -c",
interactive_method: "bash -c",
},
authcloud: {
name: "Auth Cloud",
description: "Cloud with env var auth",
url: "https://auth.example.com",
type: "cloud",
auth: "AUTH_TOKEN",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
},
matrix: {
...mockManifest.matrix,
"railway/claude": "implemented",
"railway/codex": "missing",
"local/claude": "implemented",
"local/codex": "implemented",
"authcloud/claude": "implemented",
// emptycloud has no matrix entries at all
},
};
// Mock @clack/prompts
const mockLogError = mock(() => {});
const mockLogInfo = mock(() => {});
const mockLogStep = mock(() => {});
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mockLogStep,
info: mockLogInfo,
warn: mock(() => {}),
error: mockLogError,
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
// Import commands after mock setup
const { cmdCloudInfo } = await import("../commands.js");
describe("cmdCloudInfo", () => {
let consoleMocks: ReturnType<typeof createConsoleMocks>;
let originalFetch: typeof global.fetch;
let processExitSpy: ReturnType<typeof spyOn>;
let savedORKey: string | undefined;
beforeEach(async () => {
consoleMocks = createConsoleMocks();
mockLogError.mockClear();
mockLogInfo.mockClear();
mockLogStep.mockClear();
mockSpinnerStart.mockClear();
mockSpinnerStop.mockClear();
processExitSpy = spyOn(process, "exit").mockImplementation((_code?: number): never => {
throw new Error("process.exit");
});
savedORKey = process.env.OPENROUTER_API_KEY;
delete process.env.OPENROUTER_API_KEY;
originalFetch = global.fetch;
global.fetch = mock(async () => new Response(JSON.stringify(extendedManifest)));
await loadManifest(true);
});
afterEach(() => {
global.fetch = originalFetch;
processExitSpy.mockRestore();
restoreMocks(consoleMocks.log, consoleMocks.error);
if (savedORKey !== undefined) {
process.env.OPENROUTER_API_KEY = savedORKey;
} else {
delete process.env.OPENROUTER_API_KEY;
}
});
// ── Happy path ──────────────────────────────────────────────────────────
describe("display output for valid cloud", () => {
it("should show cloud name and description for sprite", async () => {
await cmdCloudInfo("sprite");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Sprite");
expect(output).toContain("Lightweight VMs");
});
it("should show Available agents header", async () => {
await cmdCloudInfo("sprite");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Available agents");
});
it("should list implemented agents for sprite", async () => {
await cmdCloudInfo("sprite");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("claude");
expect(output).toContain("codex");
});
it("should show launch command hint for each agent", async () => {
await cmdCloudInfo("sprite");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("spawn claude sprite");
expect(output).toContain("spawn codex sprite");
});
it("should only show implemented agents for hetzner", async () => {
await cmdCloudInfo("hetzner");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("claude");
// hetzner/codex is "missing" in mock manifest
expect(output).not.toContain("spawn codex hetzner");
});
it("should show hetzner name and description", async () => {
await cmdCloudInfo("hetzner");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Hetzner Cloud");
expect(output).toContain("European cloud provider");
});
it("should use spinner while loading manifest", async () => {
await cmdCloudInfo("sprite");
expect(mockSpinnerStart).toHaveBeenCalled();
expect(mockSpinnerStop).toHaveBeenCalled();
});
});
// ── Cloud with notes field ──────────────────────────────────────────────
describe("cloud with notes", () => {
it("should display notes when the cloud has them", async () => {
await cmdCloudInfo("railway");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Requires Railway CLI installed locally");
});
it("should show railway name and description", async () => {
await cmdCloudInfo("railway");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Railway");
expect(output).toContain("Container platform");
});
it("should show only implemented agents for railway", async () => {
await cmdCloudInfo("railway");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("spawn claude railway");
expect(output).not.toContain("spawn codex railway");
});
});
// ── Cloud with no implemented agents ───────────────────────────────────
describe("cloud with no implemented agents", () => {
it("should show 'No implemented agents' message", async () => {
await cmdCloudInfo("emptycloud");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("No implemented agents");
});
it("should still show cloud name and description", async () => {
await cmdCloudInfo("emptycloud");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Empty Cloud");
expect(output).toContain("No agents here");
});
it("should not show any spawn commands", async () => {
await cmdCloudInfo("emptycloud");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).not.toContain("spawn claude emptycloud");
expect(output).not.toContain("spawn codex emptycloud");
});
});
// ── Quick-start auth display ────────────────────────────────────────────
describe("quick-start auth display", () => {
it("should show OPENROUTER_API_KEY for cloud with 'none' auth", async () => {
await cmdCloudInfo("local");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("OPENROUTER_API_KEY");
});
it("should not show 'none' as a command for cloud with 'none' auth", async () => {
await cmdCloudInfo("local");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
// "none" should not appear as an auth instruction in quick-start
const quickStartLines = output.split("\n");
const noneAsCommand = quickStartLines.some(
(line: string) => line.includes("Quick start") === false && line.trim() === "none",
);
expect(noneAsCommand).toBe(false);
});
it("should show cloud-specific auth env var for cloud with env var auth", async () => {
await cmdCloudInfo("authcloud");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("AUTH_TOKEN");
expect(output).toContain("OPENROUTER_API_KEY");
});
});
// ── Error paths ─────────────────────────────────────────────────────────
describe("error paths", () => {
it("should exit with error for unknown cloud", async () => {
await expect(cmdCloudInfo("nonexistent")).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
const errorCalls = mockLogError.mock.calls.map((c: any[]) => c.join(" "));
expect(errorCalls.some((msg: string) => msg.includes("Unknown cloud"))).toBe(true);
});
it("should suggest spawn clouds command for unknown cloud", async () => {
await expect(cmdCloudInfo("nonexistent")).rejects.toThrow("process.exit");
const infoCalls = mockLogInfo.mock.calls.map((c: any[]) => c.join(" "));
expect(infoCalls.some((msg: string) => msg.includes("spawn clouds"))).toBe(true);
});
it("should reject cloud with invalid identifier characters", async () => {
await expect(cmdCloudInfo("../hack")).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it("should reject cloud with uppercase letters", async () => {
await expect(cmdCloudInfo("Sprite")).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it("should reject empty cloud name", async () => {
await expect(cmdCloudInfo("")).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it("should reject whitespace-only cloud name", async () => {
await expect(cmdCloudInfo(" ")).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it("should reject cloud name with shell metacharacters", async () => {
await expect(cmdCloudInfo("sprite;rm")).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
});
it("should reject cloud name exceeding 64 characters", async () => {
const longName = "a".repeat(65);
await expect(cmdCloudInfo(longName)).rejects.toThrow("process.exit");
expect(processExitSpy).toHaveBeenCalledWith(1);
});
});
// ── Typo suggestions ───────────────────────────────────────────────────
describe("typo suggestions", () => {
it("should suggest closest cloud name for typo", async () => {
// "sprit" is distance 1 from "sprite"
await expect(cmdCloudInfo("sprit")).rejects.toThrow("process.exit");
const infoCalls = mockLogInfo.mock.calls.map((c: any[]) => c.join(" "));
expect(infoCalls.some((msg: string) => msg.includes("Did you mean"))).toBe(true);
});
it("should not suggest when input is very different", async () => {
// "kubernetes" is far from any cloud name
await expect(cmdCloudInfo("kubernetes")).rejects.toThrow("process.exit");
const infoCalls = mockLogInfo.mock.calls.map((c: any[]) => c.join(" "));
expect(infoCalls.every((msg: string) => !msg.includes("Did you mean"))).toBe(true);
});
});
});

View file

@ -1,319 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { loadManifest } from "../manifest";
/**
* Tests for command output functions (cmdList, cmdAgents, cmdClouds, cmdAgentInfo, cmdHelp).
*
* Strategy: mock @clack/prompts to prevent TTY output, and mock global.fetch
* so that loadManifest returns controlled test data. Before each test, we call
* loadManifest(true) to force a cache refresh through our mocked fetch.
*
* Agent: test-engineer
*/
const mockManifest = createMockManifest();
// Mock @clack/prompts to prevent TTY output
const mockSpinnerStart = mock(() => {});
const mockSpinnerStop = mock(() => {});
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mockSpinnerStart,
stop: mockSpinnerStop,
message: mock(() => {}),
}),
log: {
step: mock(() => {}),
info: mock(() => {}),
warn: mock(() => {}),
error: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
autocomplete: mock(async () => "claude"),
text: mock(async () => undefined),
isCancel: () => false,
}));
// Import commands after @clack/prompts mock is set up
const { cmdMatrix, cmdAgents, cmdClouds, cmdAgentInfo, cmdHelp } = await import("../commands.js");
describe("Command Output Functions", () => {
let consoleMocks: ReturnType<typeof createConsoleMocks>;
let originalFetch: typeof global.fetch;
beforeEach(async () => {
consoleMocks = createConsoleMocks();
mockSpinnerStart.mockClear();
mockSpinnerStop.mockClear();
// Mock fetch to return our controlled manifest data
originalFetch = global.fetch;
global.fetch = mock(async () => new Response(JSON.stringify(mockManifest)));
// Force-refresh the manifest cache so it picks up our mocked fetch data.
// This ensures the in-memory _cached in manifest.ts is set to our test data,
// regardless of what other test files may have loaded before us.
await loadManifest(true);
});
afterEach(() => {
global.fetch = originalFetch;
restoreMocks(consoleMocks.log, consoleMocks.error);
});
// ── cmdList ─────────────────────────────────────────────────────────────
describe("cmdMatrix", () => {
it("should load manifest and display matrix table", async () => {
await cmdMatrix();
expect(consoleMocks.log).toHaveBeenCalled();
});
it("should show agent names in the matrix", async () => {
await cmdMatrix();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Claude Code");
expect(output).toContain("Codex");
});
it("should show cloud names in the header", async () => {
await cmdMatrix();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Sprite");
expect(output).toContain("Hetzner Cloud");
});
it("should show implementation count", async () => {
await cmdMatrix();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
// 3 implemented out of 4 total (2 agents x 2 clouds)
expect(output).toContain("3/4");
});
it("should show legend with implemented and not-yet-available labels", async () => {
await cmdMatrix();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("implemented");
expect(output).toContain("not yet available");
});
it("should use spinner while loading", async () => {
await cmdMatrix();
expect(mockSpinnerStart).toHaveBeenCalled();
expect(mockSpinnerStop).toHaveBeenCalled();
});
});
// ── cmdAgents ───────────────────────────────────────────────────────────
describe("cmdAgents", () => {
it("should load manifest and display agents", async () => {
await cmdAgents();
expect(consoleMocks.log).toHaveBeenCalled();
});
it("should show all agent keys", async () => {
await cmdAgents();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("claude");
expect(output).toContain("codex");
});
it("should show agent display names", async () => {
await cmdAgents();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Claude Code");
expect(output).toContain("Codex");
});
it("should show cloud count per agent", async () => {
await cmdAgents();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
// claude has 2 clouds (sprite, hetzner), codex has 1 (sprite)
expect(output).toContain("2 clouds");
expect(output).toContain("1 cloud");
expect(output).not.toContain("1 clouds");
});
it("should show agent descriptions", async () => {
await cmdAgents();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("AI coding assistant");
expect(output).toContain("AI pair programmer");
});
it("should show usage hint at bottom", async () => {
await cmdAgents();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("spawn <agent>");
});
it("should show Agents header", async () => {
await cmdAgents();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Agents");
});
});
// ── cmdClouds ───────────────────────────────────────────────────────────
describe("cmdClouds", () => {
it("should load manifest and display clouds", async () => {
await cmdClouds();
expect(consoleMocks.log).toHaveBeenCalled();
});
it("should show all cloud keys", async () => {
await cmdClouds();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("sprite");
expect(output).toContain("hetzner");
});
it("should show cloud display names", async () => {
await cmdClouds();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Sprite");
expect(output).toContain("Hetzner Cloud");
});
it("should show agent count per cloud", async () => {
await cmdClouds();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
// sprite has 2 agents (claude, codex), hetzner has 1 (claude) - shown as X/Y ratio
expect(output).toContain("2/2");
expect(output).toContain("1/2");
});
it("should show cloud descriptions", async () => {
await cmdClouds();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Lightweight VMs");
expect(output).toContain("European cloud provider");
});
it("should show Cloud Providers header", async () => {
await cmdClouds();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Cloud Providers");
});
it("should show usage hint at bottom", async () => {
await cmdClouds();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("spawn <agent> <cloud>");
});
});
// ── cmdAgentInfo ────────────────────────────────────────────────────────
describe("cmdAgentInfo", () => {
it("should show agent name and description", async () => {
await cmdAgentInfo("claude");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Claude Code");
expect(output).toContain("AI coding assistant");
});
it("should show Available clouds header", async () => {
await cmdAgentInfo("claude");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Available clouds");
});
it("should list implemented clouds for claude", async () => {
await cmdAgentInfo("claude");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("sprite");
expect(output).toContain("hetzner");
});
it("should show launch command hint for each cloud", async () => {
await cmdAgentInfo("claude");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("spawn claude sprite");
expect(output).toContain("spawn claude hetzner");
});
it("should only show implemented clouds for codex", async () => {
await cmdAgentInfo("codex");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("sprite");
// hetzner/codex is "missing" in mock manifest
expect(output).not.toContain("spawn codex hetzner");
});
it("should show codex description", async () => {
await cmdAgentInfo("codex");
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("Codex");
expect(output).toContain("AI pair programmer");
});
});
// ── cmdHelp ─────────────────────────────────────────────────────────────
describe("cmdHelp output content", () => {
it("should include all subcommand names", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("list");
expect(output).toContain("agents");
expect(output).toContain("clouds");
expect(output).toContain("update");
expect(output).toContain("version");
expect(output).toContain("help");
});
it("should include USAGE section", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("USAGE");
});
it("should include EXAMPLES section", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("EXAMPLES");
});
it("should include AUTHENTICATION section", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("AUTHENTICATION");
expect(output).toContain("OPENROUTER_API_KEY");
});
it("should include TROUBLESHOOTING section", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("TROUBLESHOOTING");
});
it("should include --prompt and --prompt-file usage", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("--prompt");
expect(output).toContain("--prompt-file");
});
it("should include repo and OpenRouter links", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("openrouter.ai");
expect(output).toContain("github.com");
});
it("should include install command", () => {
cmdHelp();
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
expect(output).toContain("curl -fsSL");
expect(output).toContain("install.sh");
});
});
});

View file

@ -1,8 +1,6 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { cmdHelp } from "../commands";
import { createConsoleMocks, createProcessExitMock, restoreMocks, createMockManifest } from "./test-helpers";
const mockManifest = createMockManifest();
import { createConsoleMocks, createProcessExitMock, restoreMocks } from "./test-helpers";
// Note: Bun test doesn't support module mocking the same way as vitest
// These tests require refactoring commands.ts to use dependency injection
@ -33,8 +31,8 @@ describe("commands", () => {
// 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
// - cmdAgents/cmdClouds: commands-display.test.ts, commands-list-grid.test.ts
// - cmdAgentInfo/cmdCloudInfo: commands-info-details.test.ts, commands-display.test.ts, commands-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
});

View file

@ -1,435 +0,0 @@
import { describe, it, expect } from "bun:test";
import { readFileSync, existsSync } from "node:fs";
import { resolve, join } from "node:path";
import { isString } from "@openrouter/spawn-shared";
/**
* Validation tests for sh/cli/install.sh.
*
* install.sh is the critical entry point for all new users
* (curl -fsSL ... | bash). It has been modified in multiple recent PRs
* but had zero test coverage. These tests validate structure, conventions,
* security, curl|bash compatibility, and the source-mode fallback wrapper.
*
* Agent: test-engineer
*/
const REPO_ROOT = resolve(import.meta.dir, "../../../..");
const INSTALL_SH = join(REPO_ROOT, "sh", "cli", "install.sh");
const content = readFileSync(INSTALL_SH, "utf-8");
const lines = content.split("\n");
/** Get non-comment, non-empty lines */
function codeLines(): string[] {
return lines.filter((line) => line.trim() !== "" && !line.trimStart().startsWith("#"));
}
describe("install.sh validation", () => {
// ── File existence and structure ─────────────────────────────────────
describe("file structure", () => {
it("should exist on disk", () => {
expect(existsSync(INSTALL_SH)).toBe(true);
});
it("should start with #!/bin/bash shebang", () => {
expect(lines[0]).toBe("#!/bin/bash");
});
it("should use set -eo pipefail", () => {
expect(content).toContain("set -eo pipefail");
});
it("should not be empty", () => {
expect(content.trim().length).toBeGreaterThan(100);
});
it("should not use set -u or set -o nounset", () => {
const code = codeLines();
const hasSetU = code.some(
(l) => (/\bset\s+.*-.*u\b/.test(l) && !l.includes("pipefail")) || /set\s+-o\s+nounset/.test(l),
);
expect(hasSetU).toBe(false);
});
});
// ── Repository constants ─────────────────────────────────────────────
describe("repository constants", () => {
it("should define SPAWN_REPO pointing to OpenRouterTeam/spawn", () => {
expect(content).toContain('SPAWN_REPO="OpenRouterTeam/spawn"');
});
it("should define SPAWN_RAW_BASE using SPAWN_REPO", () => {
expect(content).toContain("SPAWN_RAW_BASE=");
expect(content).toContain("raw.githubusercontent.com");
expect(content).toContain("${SPAWN_REPO}");
});
it("should define MIN_BUN_VERSION", () => {
expect(content).toMatch(/MIN_BUN_VERSION="[0-9]+\.[0-9]+\.[0-9]+"/);
});
});
// ── Required functions ───────────────────────────────────────────────
describe("required functions", () => {
it("should define log_info function", () => {
expect(content).toMatch(/log_info\(\)/);
});
it("should define log_warn function", () => {
expect(content).toMatch(/log_warn\(\)/);
});
it("should define log_error function", () => {
expect(content).toMatch(/log_error\(\)/);
});
it("should define version_gte function", () => {
expect(content).toContain("version_gte()");
});
it("should define ensure_min_bun_version function", () => {
expect(content).toContain("ensure_min_bun_version()");
});
it("should define ensure_in_path function", () => {
expect(content).toContain("ensure_in_path()");
});
it("should define build_and_install function", () => {
expect(content).toContain("build_and_install()");
});
});
// ── curl|bash compatibility ──────────────────────────────────────────
describe("curl|bash compatibility", () => {
it("should not use source <(...) process substitution", () => {
const code = codeLines();
const hasProcessSub = code.some((l) => /source\s+<\(/.test(l));
expect(hasProcessSub).toBe(false);
});
it("should not rely on BASH_SOURCE for path resolution", () => {
// install.sh runs via curl|bash so BASH_SOURCE is meaningless
expect(content).not.toContain("BASH_SOURCE");
});
it("should not rely on dirname $0 for path resolution", () => {
expect(content).not.toContain('dirname "$0"');
expect(content).not.toContain("dirname $0");
});
it("should use SPAWN_INSTALL_DIR env var for override", () => {
expect(content).toContain("SPAWN_INSTALL_DIR");
});
it("should check for SPAWN_INSTALL_DIR before defaulting", () => {
// build_and_install should check SPAWN_INSTALL_DIR first
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("SPAWN_INSTALL_DIR");
});
});
// ── Bun installation ────────────────────────────────────────────────
describe("bun installation", () => {
it("should check if bun is available via command -v", () => {
expect(content).toContain("command -v bun");
});
it("should install bun from bun.sh if not found", () => {
expect(content).toContain("https://bun.sh/install");
});
it("should set BUN_INSTALL and PATH after installing bun", () => {
expect(content).toContain("BUN_INSTALL=");
expect(content).toContain("${BUN_INSTALL}/bin");
});
it("should show error and exit if bun installation fails", () => {
// After installing bun, should check again and show error if still not found
const afterInstall = content.slice(content.indexOf("https://bun.sh/install"));
expect(afterInstall).toContain("command -v bun");
expect(afterInstall).toContain("log_error");
expect(afterInstall).toContain("exit 1");
});
it("should call ensure_min_bun_version after bun is available", () => {
// ensure_min_bun_version should be called in the main flow
const mainFlow = content.slice(content.lastIndexOf("ensure_min_bun_version"));
expect(mainFlow).toContain("build_and_install");
});
});
// ── Version comparison ──────────────────────────────────────────────
describe("version_gte logic", () => {
// Extract the full function body by finding the next function or end of file
const fnStart = content.indexOf("version_gte()");
const fnEnd = content.indexOf("\n\n# ---", fnStart);
const fnBody = content.slice(fnStart, fnEnd > fnStart ? fnEnd : undefined);
it("should use IFS='.' to split semver", () => {
expect(fnBody).toContain("IFS='.'");
});
it("should handle missing segments with default 0", () => {
// ${a[$i]:-0} or similar default-to-zero pattern
expect(fnBody).toContain(":-0");
});
it("should return 1 (false) when version is less", () => {
expect(fnBody).toContain("return 1");
});
it("should return 0 (true) when version is greater or equal", () => {
expect(fnBody).toContain("return 0");
});
});
// ── Build and install logic ─────────────────────────────────────────
describe("build_and_install", () => {
it("should create a temp directory", () => {
expect(content).toContain("mktemp -d");
});
it("should clean up temp directory on exit via trap", () => {
expect(content).toContain("trap");
expect(content).toContain("rm -rf");
});
it("should download pre-built binary from GitHub releases", () => {
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("cli-latest");
expect(fnBody).toContain("cli.js");
});
it("should default install dir to ~/.local/bin", () => {
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("${HOME}/.local/bin");
});
it("should create the install directory if it does not exist", () => {
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain('mkdir -p "${INSTALL_DIR}"');
});
it("should set chmod +x on the spawn binary", () => {
expect(content).toContain('chmod +x "${INSTALL_DIR}/spawn"');
});
it("should call ensure_in_path at the end", () => {
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("ensure_in_path");
});
});
// ── Binary download ─────────────────────────────────────────────────
describe("binary download", () => {
it("should download from GitHub releases with cli-latest tag", () => {
expect(content).toContain("github.com");
expect(content).toContain("releases/download");
expect(content).toContain("cli-latest");
expect(content).toContain("cli.js");
});
it("should validate that downloaded binary is not empty", () => {
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("! -s");
});
it("should copy downloaded cli.js to install directory", () => {
const fnStart = content.indexOf("build_and_install()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("cli.js");
expect(fnBody).toContain("${INSTALL_DIR}/spawn");
});
it("should show helpful message after installation", () => {
const fnStart = content.indexOf("ensure_in_path()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain('spawn" version');
expect(fnBody).toContain("to get started");
});
it("should not use clone_cli or source builds (removed)", () => {
expect(content).not.toContain("clone_cli");
expect(content).not.toContain("api.github.com");
expect(content).not.toContain("bun run build");
});
});
// ── symlink into /usr/local/bin ────────────────────────────────────
describe("symlink to /usr/local/bin", () => {
it("should symlink spawn into /usr/local/bin for immediate availability", () => {
const fnStart = content.indexOf("ensure_in_path()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("/usr/local/bin/spawn");
expect(fnBody).toContain("ln -sf");
});
it("should try sudo if /usr/local/bin is not writable", () => {
const fnStart = content.indexOf("ensure_in_path()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("sudo ln -sf");
});
it("should gracefully handle symlink failure", () => {
// Should not hard-fail if symlink fails — falls back to exec $SHELL
const fnStart = content.indexOf("ensure_in_path()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("|| true");
expect(fnBody).toContain("exec");
});
it("should default install to ~/.local/bin", () => {
expect(content).toContain("${HOME}/.local/bin");
});
});
// ── ensure_in_path function ────────────────────────────────────────
describe("ensure_in_path", () => {
it("should run spawn version after install", () => {
expect(content).toContain('/spawn" version');
});
it("should patch zsh rc file for zsh users", () => {
expect(content).toContain(".zshrc");
});
it("should patch fish PATH for fish users", () => {
expect(content).toContain("fish_add_path");
});
it("should patch bashrc as default", () => {
expect(content).toContain(".bashrc");
});
it("should detect shell from SHELL env var", () => {
expect(content).toContain("${SHELL:-");
});
it("should show exec $SHELL fallback when symlink fails", () => {
const fnStart = content.indexOf("ensure_in_path()");
const fnBody = content.slice(fnStart);
expect(fnBody).toContain("exec");
});
});
// ── Security ─────────────────────────────────────────────────────────
describe("security", () => {
it("should not contain hardcoded API keys or tokens", () => {
const code = codeLines();
for (const line of code) {
expect(line).not.toMatch(/sk-[a-zA-Z0-9]{20,}/);
expect(line).not.toMatch(/token[=:]\s*[a-f0-9]{32,}/i);
}
});
it("should not use eval to execute downloaded content", () => {
const code = codeLines();
// eval should not be used to run arbitrary downloaded scripts
// (bun.sh/install is piped to bash, which is standard practice)
const evalLines = code.filter((l) => l.includes("eval") && !l.includes("2>/dev/null") && !l.includes("#"));
expect(evalLines).toEqual([]);
});
it("should quote variables in file operations", () => {
// Critical paths should quote variables
expect(content).toContain('"${INSTALL_DIR}/spawn"');
expect(content).toContain('"${tmpdir}"');
});
it("should use -fsSL flags for curl downloads", () => {
const curlLines = codeLines().filter((l) => l.includes("curl"));
for (const line of curlLines) {
expect(line).toMatch(/-fsSL/);
}
});
});
// ── Consistency with package.json ──────────────────────────────────
describe("consistency with package.json", () => {
it("should reference same repo as package.json", () => {
const pkgContent = readFileSync(join(REPO_ROOT, "packages", "cli", "package.json"), "utf-8");
const pkg = JSON.parse(pkgContent);
// install.sh uses OpenRouterTeam/spawn
expect(content).toContain("OpenRouterTeam/spawn");
// package.json should reference same repo
if (pkg.repository) {
const repo = isString(pkg.repository) ? pkg.repository : pkg.repository.url || "";
expect(repo.toLowerCase()).toContain("openrouterteam/spawn");
}
});
it("should download the correct files that exist in cli/src/", () => {
// The curl-based download path downloads .ts files from cli/src/
// Verify that the files listed in install.sh actually exist
const srcDir = join(REPO_ROOT, "packages", "cli", "src");
expect(existsSync(join(srcDir, "index.ts"))).toBe(true);
expect(existsSync(join(srcDir, "commands.ts"))).toBe(true);
expect(existsSync(join(srcDir, "manifest.ts"))).toBe(true);
});
});
// ── Main flow order ────────────────────────────────────────────────
describe("main execution flow", () => {
it("should check for bun before building", () => {
const bunCheck = content.indexOf("command -v bun");
const buildCall = content.lastIndexOf("build_and_install");
expect(bunCheck).toBeLessThan(buildCall);
});
it("should ensure min bun version before building", () => {
const versionCheck = content.lastIndexOf("ensure_min_bun_version");
const buildCall = content.lastIndexOf("build_and_install");
expect(versionCheck).toBeLessThan(buildCall);
});
it("should call build_and_install as the last major step", () => {
const lastFewLines = lines.slice(-5).join("\n");
expect(lastFewLines).toContain("build_and_install");
});
});
// ── Error handling ────────────────────────────────────────────────
describe("error handling", () => {
it("should show re-run instructions on bun install failure", () => {
expect(content).toContain("curl -fsSL ${SPAWN_RAW_BASE}/sh/cli/install.sh | bash");
});
it("should show manual bun install instructions on failure", () => {
expect(content).toContain("curl -fsSL https://bun.sh/install | bash");
});
it("should exit with code 1 on failures", () => {
const exitLines = codeLines().filter((l) => l.trim() === "exit 1");
expect(exitLines.length).toBeGreaterThanOrEqual(2);
});
it("should show upgrade instructions if bun version is too low", () => {
const fnStart = content.indexOf("ensure_min_bun_version()");
const fnEnd = content.indexOf("\n# --- Helper: ensure", fnStart);
const fnBody = content.slice(fnStart, fnEnd > fnStart ? fnEnd : undefined);
expect(fnBody).toContain("bun upgrade");
expect(fnBody).toContain("exit 1");
});
});
});

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { writeFileSync, mkdirSync } from "node:fs";
import type { Manifest } from "../manifest";
import type { TestEnvironment } from "./test-helpers";
import { mockSuccessfulFetch, mockFailedFetch, setupTestEnvironment, teardownTestEnvironment } from "./test-helpers";
@ -76,18 +76,8 @@ describe("CLI Integration Tests", () => {
const manifest1 = await loadManifest(true);
expect(manifest1).toEqual(mockManifest);
// Cache location depends on whether the test runs in the project directory
// In the spawn project root, it uses a local manifest.json, so cache may not be written
const cacheExists = existsSync(env.cacheFile);
if (cacheExists) {
const cachedData = JSON.parse(readFileSync(env.cacheFile, "utf-8"));
expect(cachedData).toEqual(mockManifest);
}
// Second load - should use cache
// Second load - in-memory cache should return same data
const manifest2 = await loadManifest();
// Note: Bun's in-memory caching may behave differently
expect(manifest2).toEqual(mockManifest);
});

View file

@ -1,270 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
import type { Manifest } from "../manifest";
import { loadManifest, agentKeys, cloudKeys, matrixStatus, countImplemented } from "../manifest";
import type { TestEnvironment } from "./test-helpers";
import { createMockManifest, setupTestEnvironment, teardownTestEnvironment } from "./test-helpers";
/**
* Tests for manifest.ts validation and edge cases that are not covered
* by the existing manifest.test.ts, focusing on:
* - isValidManifest with various invalid shapes
* - logError behavior
* - loadManifest caching edge cases
* - countImplemented with mixed statuses
*/
describe("Manifest Validation Edge Cases", () => {
describe("isValidManifest (via loadManifest)", () => {
let env: TestEnvironment;
beforeEach(() => {
env = setupTestEnvironment();
});
afterEach(() => {
teardownTestEnvironment(env);
});
it("should reject manifest missing agents field", async () => {
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
clouds: {},
matrix: {},
}),
),
),
);
try {
await loadManifest(true);
// If local manifest fallback kicks in, that's ok
} catch (err: any) {
expect(err.message).toContain("Cannot load manifest");
}
});
it("should reject manifest missing clouds field", async () => {
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
agents: {},
matrix: {},
}),
),
),
);
try {
await loadManifest(true);
} catch (err: any) {
expect(err.message).toContain("Cannot load manifest");
}
});
it("should reject manifest missing matrix field", async () => {
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
agents: {},
clouds: {},
}),
),
),
);
try {
await loadManifest(true);
} catch (err: any) {
expect(err.message).toContain("Cannot load manifest");
}
});
it("should reject null manifest data", async () => {
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(null))));
try {
await loadManifest(true);
} catch (err: any) {
expect(err.message).toContain("Cannot load manifest");
}
});
it("should reject empty object manifest data", async () => {
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({}))));
try {
await loadManifest(true);
} catch (err: any) {
expect(err.message).toContain("Cannot load manifest");
}
});
it("should accept valid manifest with empty collections", async () => {
const validEmpty = {
agents: {},
clouds: {},
matrix: {},
};
global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(validEmpty))));
const manifest = await loadManifest(true);
expect(manifest).toHaveProperty("agents");
expect(manifest).toHaveProperty("clouds");
expect(manifest).toHaveProperty("matrix");
});
it("should handle non-ok HTTP response from GitHub", async () => {
// When GitHub returns a non-ok response, fetchManifestFromGitHub returns null
// and loadManifest falls back to cache or throws
global.fetch = mock(() =>
Promise.resolve(
new Response("Internal Server Error", {
status: 500,
statusText: "Internal Server Error",
}),
),
);
try {
const manifest = await loadManifest(true);
// If it succeeds, it used a fallback (stale cache or local manifest)
expect(manifest).toHaveProperty("agents");
expect(manifest).toHaveProperty("clouds");
expect(manifest).toHaveProperty("matrix");
} catch (err: any) {
// Or it failed because no cache was available
expect(err.message).toContain("Cannot load manifest");
}
});
});
describe("matrixStatus edge cases", () => {
it("should return missing for undefined cloud/agent pair", () => {
const manifest = createMockManifest();
expect(matrixStatus(manifest, "", "")).toBe("missing");
});
it("should handle keys with special but valid characters", () => {
const manifest: Manifest = {
agents: {},
clouds: {},
matrix: {
"my-cloud/my-agent": "implemented",
"cloud_2/agent_2": "implemented",
},
};
expect(matrixStatus(manifest, "my-cloud", "my-agent")).toBe("implemented");
expect(matrixStatus(manifest, "cloud_2", "agent_2")).toBe("implemented");
expect(matrixStatus(manifest, "my-cloud", "agent_2")).toBe("missing");
});
});
describe("countImplemented edge cases", () => {
it("should only count exactly 'implemented' status", () => {
const manifest: Manifest = {
agents: {},
clouds: {},
matrix: {
"a/b": "implemented",
"c/d": "missing",
"e/f": "partial",
"g/h": "implemented",
"i/j": "IMPLEMENTED",
},
};
expect(countImplemented(manifest)).toBe(2);
});
it("should count correctly with large matrix", () => {
const matrix: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
matrix[`cloud${i}/agent${i}`] = i % 3 === 0 ? "implemented" : "missing";
}
const manifest: Manifest = {
agents: {},
clouds: {},
matrix,
};
// i=0,3,6,...,99 => 0,3,6,...,99 => count of multiples of 3 from 0-99
// 0,3,6,...,99 = 34 values
expect(countImplemented(manifest)).toBe(34);
});
});
describe("agentKeys and cloudKeys ordering", () => {
it("should preserve insertion order of agents", () => {
const manifest: Manifest = {
agents: {
zeta: {
name: "Zeta",
description: "",
url: "",
install: "",
launch: "",
env: {},
},
alpha: {
name: "Alpha",
description: "",
url: "",
install: "",
launch: "",
env: {},
},
mid: {
name: "Mid",
description: "",
url: "",
install: "",
launch: "",
env: {},
},
},
clouds: {},
matrix: {},
};
expect(agentKeys(manifest)).toEqual([
"zeta",
"alpha",
"mid",
]);
});
it("should preserve insertion order of clouds", () => {
const manifest: Manifest = {
agents: {},
clouds: {
zebra: {
name: "Zebra",
description: "",
url: "",
type: "",
auth: "",
provision_method: "",
exec_method: "",
interactive_method: "",
},
apple: {
name: "Apple",
description: "",
url: "",
type: "",
auth: "",
provision_method: "",
exec_method: "",
interactive_method: "",
},
},
matrix: {},
};
expect(cloudKeys(manifest)).toEqual([
"zebra",
"apple",
]);
});
});
});

View file

@ -1,339 +0,0 @@
import { describe, it, expect } from "bun:test";
import type { Manifest } from "../manifest";
import { getImplementedClouds, getMissingClouds, calculateColumnWidth, getTerminalWidth } from "../commands";
import { cloudKeys, agentKeys } from "../manifest";
/**
* Tests for exported matrix helpers in commands.ts:
* - getTerminalWidth: terminal width fallback to 80
* - calculateColumnWidth: dynamic column sizing with minimum width
* - getMissingClouds: clouds where an agent is NOT implemented
* - getImplementedClouds: clouds where an agent IS implemented
*/
// ── Test Manifests ────────────────────────────────────────────────────────────
function createTestManifest(): Manifest {
return {
agents: {
claude: {
name: "Claude Code",
description: "AI coding assistant",
url: "https://claude.ai",
install: "npm install -g claude",
launch: "claude",
env: {
ANTHROPIC_API_KEY: "test",
},
},
codex: {
name: "Codex",
description: "AI pair programmer",
url: "https://codex.dev",
install: "npm install -g codex",
launch: "codex",
env: {
OPENAI_API_KEY: "test",
},
},
cline: {
name: "Cline",
description: "AI developer agent",
url: "https://cline.dev",
install: "npm install -g cline",
launch: "cline",
env: {},
},
},
clouds: {
sprite: {
name: "Sprite",
description: "Lightweight VMs",
url: "https://sprite.sh",
type: "vm",
auth: "SPRITE_TOKEN",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
hetzner: {
name: "Hetzner Cloud",
description: "European cloud provider",
url: "https://hetzner.com",
type: "cloud",
auth: "HCLOUD_TOKEN",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
vultr: {
name: "Vultr",
description: "Cloud compute",
url: "https://vultr.com",
type: "cloud",
auth: "VULTR_API_KEY",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
},
matrix: {
"sprite/claude": "implemented",
"sprite/codex": "implemented",
"sprite/cline": "missing",
"hetzner/claude": "implemented",
"hetzner/codex": "missing",
"hetzner/cline": "missing",
"vultr/claude": "implemented",
"vultr/codex": "missing",
"vultr/cline": "missing",
},
};
}
const MIN_AGENT_COL_WIDTH = 16;
const MIN_CLOUD_COL_WIDTH = 10;
const COL_PADDING = 2;
// ── Tests for exported helpers ──────────────────────────────────────────────
describe("getTerminalWidth", () => {
it("should return a positive number", () => {
const width = getTerminalWidth();
expect(width).toBeGreaterThan(0);
});
it("should return at least 80 (fallback minimum)", () => {
// In test environments without a TTY, should fall back to 80
const width = getTerminalWidth();
expect(width).toBeGreaterThanOrEqual(80);
});
});
describe("calculateColumnWidth", () => {
it("should return minimum width when items are shorter", () => {
const width = calculateColumnWidth(
[
"ab",
"cd",
],
16,
);
expect(width).toBe(16);
});
it("should expand beyond minimum for long items", () => {
const width = calculateColumnWidth(
[
"a-very-long-cloud-name",
],
10,
);
expect(width).toBe("a-very-long-cloud-name".length + COL_PADDING);
});
it("should use the longest item to determine width", () => {
const items = [
"short",
"medium-length",
"the-longest-item-here",
];
const width = calculateColumnWidth(items, 10);
expect(width).toBe("the-longest-item-here".length + COL_PADDING);
});
it("should return minimum width for empty items list", () => {
const width = calculateColumnWidth([], 16);
expect(width).toBe(16);
});
it("should handle single item", () => {
const width = calculateColumnWidth(
[
"x",
],
16,
);
expect(width).toBe(16); // "x" + 2 padding = 3, less than min 16
});
it("should add COL_PADDING (2) to item length", () => {
// Item with length 15 + 2 padding = 17 > min 16
const item = "a".repeat(15);
const width = calculateColumnWidth(
[
item,
],
16,
);
expect(width).toBe(17);
});
it("should handle item at exactly minimum width - padding", () => {
// Item with length 14 + 2 padding = 16 = min
const item = "a".repeat(14);
const width = calculateColumnWidth(
[
item,
],
16,
);
expect(width).toBe(16);
});
it("should handle item at minimum width - padding + 1", () => {
// Item with length 15 + 2 padding = 17 > min 16
const item = "a".repeat(15);
const width = calculateColumnWidth(
[
item,
],
16,
);
expect(width).toBe(17);
});
});
describe("getMissingClouds", () => {
it("should return clouds where agent is not implemented", () => {
const manifest = createTestManifest();
const clouds = cloudKeys(manifest);
const missing = getMissingClouds(manifest, "codex", clouds);
expect(missing).toContain("hetzner");
expect(missing).toContain("vultr");
expect(missing).not.toContain("sprite");
});
it("should return empty array for fully implemented agent", () => {
const manifest = createTestManifest();
const clouds = cloudKeys(manifest);
const missing = getMissingClouds(manifest, "claude", clouds);
expect(missing).toEqual([]);
});
it("should return all clouds for unimplemented agent", () => {
const manifest = createTestManifest();
const clouds = cloudKeys(manifest);
const missing = getMissingClouds(manifest, "cline", clouds);
expect(missing).toHaveLength(3);
expect(missing).toContain("sprite");
expect(missing).toContain("hetzner");
expect(missing).toContain("vultr");
});
it("should return empty for empty clouds list", () => {
const manifest = createTestManifest();
const missing = getMissingClouds(manifest, "codex", []);
expect(missing).toEqual([]);
});
it("should return all for unknown agent (not in matrix)", () => {
const manifest = createTestManifest();
const clouds = cloudKeys(manifest);
const missing = getMissingClouds(manifest, "unknown", clouds);
expect(missing).toHaveLength(3); // All clouds are "missing" for unknown agent
});
it("should preserve cloud order from input", () => {
const manifest = createTestManifest();
const clouds = [
"vultr",
"sprite",
"hetzner",
];
const missing = getMissingClouds(manifest, "cline", clouds);
expect(missing).toEqual([
"vultr",
"sprite",
"hetzner",
]);
});
});
describe("getImplementedClouds", () => {
it("should return clouds where agent is implemented", () => {
const manifest = createTestManifest();
const impl = getImplementedClouds(manifest, "codex");
expect(impl).toEqual([
"sprite",
]);
});
it("should return all clouds for fully implemented agent", () => {
const manifest = createTestManifest();
const impl = getImplementedClouds(manifest, "claude");
expect(impl).toHaveLength(3);
expect(impl).toContain("sprite");
expect(impl).toContain("hetzner");
expect(impl).toContain("vultr");
});
it("should return empty array for unimplemented agent", () => {
const manifest = createTestManifest();
const impl = getImplementedClouds(manifest, "cline");
expect(impl).toEqual([]);
});
it("should return empty array for unknown agent", () => {
const manifest = createTestManifest();
const impl = getImplementedClouds(manifest, "nonexistent");
expect(impl).toEqual([]);
});
it("should preserve manifest cloud key order", () => {
const manifest = createTestManifest();
const impl = getImplementedClouds(manifest, "claude");
// Keys should be in the order they appear in manifest.clouds
const allClouds = cloudKeys(manifest);
const expected = allClouds.filter((c) => impl.includes(c));
expect(impl).toEqual(expected);
});
});
// ── Integration: compact view vs grid view decision ──────────────────────────
describe("compact vs grid view decision", () => {
it("should calculate grid width as agentColWidth + clouds * cloudColWidth", () => {
const manifest = createTestManifest();
const agents = agentKeys(manifest);
const clouds = cloudKeys(manifest);
const agentColWidth = calculateColumnWidth(
agents.map((a) => manifest.agents[a].name),
MIN_AGENT_COL_WIDTH,
);
const cloudColWidth = calculateColumnWidth(
clouds.map((c) => manifest.clouds[c].name),
MIN_CLOUD_COL_WIDTH,
);
const gridWidth = agentColWidth + clouds.length * cloudColWidth;
expect(gridWidth).toBeGreaterThan(0);
// With 3 clouds and reasonable names, grid should be moderate width
expect(gridWidth).toBeLessThan(200);
});
it("should use compact view when grid is wider than terminal", () => {
const manifest = createTestManifest();
const agents = agentKeys(manifest);
const clouds = cloudKeys(manifest);
const agentColWidth = calculateColumnWidth(
agents.map((a) => manifest.agents[a].name),
MIN_AGENT_COL_WIDTH,
);
const cloudColWidth = calculateColumnWidth(
clouds.map((c) => manifest.clouds[c].name),
MIN_CLOUD_COL_WIDTH,
);
const gridWidth = agentColWidth + clouds.length * cloudColWidth;
const termWidth = getTerminalWidth();
// This tests the decision logic: isCompact = gridWidth > termWidth
const isCompact = gridWidth > termWidth;
// With only 3 clouds and 80+ terminal, grid should fit
// But the key point is the logic is correct
expect(typeof isCompact).toBe("boolean");
});
});