test: Add 49 tests for command utility functions (commands-utils.test.ts) (#372)

Export and test 7 previously-unexported utility functions from commands.ts:
getTerminalWidth, getMissingClouds, getImplementedAgents, getImplementedClouds,
getErrorMessage, calculateColumnWidth, getStatusDescription.

Agent: test-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-11 00:10:04 -08:00 committed by GitHub
parent b04ff38c70
commit cff08eb624
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 450 additions and 7 deletions

View file

@ -0,0 +1,443 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import type { Manifest } from "../manifest";
/**
* Tests for utility functions exported from commands.ts.
*
* These are pure/near-pure functions that were previously not exported and
* had zero direct test coverage:
* - getTerminalWidth() - terminal width detection with fallback
* - getMissingClouds() - filter missing implementations for an agent
* - getImplementedAgents() - filter implemented agents for a cloud
* - getImplementedClouds() - filter implemented clouds for an agent
* - getErrorMessage() - duck-typed error message extraction
* - calculateColumnWidth() - column width calculation with padding
* - getStatusDescription() - HTTP status to human-readable string
*
* Agent: test-engineer
*/
// Mock @clack/prompts before importing commands
mock.module("@clack/prompts", () => ({
spinner: () => ({
start: mock(() => {}),
stop: mock(() => {}),
message: mock(() => {}),
}),
log: {
step: mock(() => {}),
info: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
isCancel: () => false,
}));
const {
getTerminalWidth,
getMissingClouds,
getImplementedAgents,
getImplementedClouds,
getErrorMessage,
calculateColumnWidth,
getStatusDescription,
} = await import("../commands.js");
const mockManifest = createMockManifest();
// Extended manifest with more clouds/agents for thorough testing
const extendedManifest: Manifest = {
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" },
},
aider: {
name: "Aider",
description: "AI pair programmer",
url: "https://aider.chat",
install: "pip install aider-chat",
launch: "aider",
env: { OPENAI_API_KEY: "test" },
},
codex: {
name: "Codex",
description: "OpenAI CLI agent",
url: "https://openai.com",
install: "npm install -g codex",
launch: "codex",
env: { OPENAI_API_KEY: "test" },
},
},
clouds: {
sprite: {
name: "Sprite",
description: "Lightweight VMs",
url: "https://sprite.sh",
type: "vm",
auth: "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: "token",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
vultr: {
name: "Vultr",
description: "Cloud compute",
url: "https://vultr.com",
type: "cloud",
auth: "token",
provision_method: "api",
exec_method: "ssh",
interactive_method: "ssh",
},
},
matrix: {
"sprite/claude": "implemented",
"sprite/aider": "implemented",
"sprite/codex": "missing",
"hetzner/claude": "implemented",
"hetzner/aider": "missing",
"hetzner/codex": "missing",
"vultr/claude": "implemented",
"vultr/aider": "missing",
"vultr/codex": "implemented",
},
};
// All-missing manifest
const allMissingManifest: Manifest = {
...mockManifest,
matrix: {
"sprite/claude": "missing",
"sprite/aider": "missing",
"hetzner/claude": "missing",
"hetzner/aider": "missing",
},
};
// All-implemented manifest
const allImplementedManifest: Manifest = {
...mockManifest,
matrix: {
"sprite/claude": "implemented",
"sprite/aider": "implemented",
"hetzner/claude": "implemented",
"hetzner/aider": "implemented",
},
};
describe("Command Utility Functions", () => {
// ── getTerminalWidth ──────────────────────────────────────────────
describe("getTerminalWidth", () => {
let originalColumns: number | undefined;
beforeEach(() => {
originalColumns = process.stdout.columns;
});
afterEach(() => {
process.stdout.columns = originalColumns!;
});
it("should return process.stdout.columns when defined", () => {
process.stdout.columns = 120;
expect(getTerminalWidth()).toBe(120);
});
it("should return 80 when process.stdout.columns is undefined", () => {
(process.stdout as any).columns = undefined;
expect(getTerminalWidth()).toBe(80);
});
it("should return 80 when process.stdout.columns is 0", () => {
(process.stdout as any).columns = 0;
expect(getTerminalWidth()).toBe(80);
});
it("should return the exact column count for narrow terminals", () => {
process.stdout.columns = 40;
expect(getTerminalWidth()).toBe(40);
});
it("should return the exact column count for very wide terminals", () => {
process.stdout.columns = 300;
expect(getTerminalWidth()).toBe(300);
});
});
// ── getMissingClouds ──────────────────────────────────────────────
describe("getMissingClouds", () => {
it("should return missing clouds for a partially implemented agent", () => {
const clouds = Object.keys(mockManifest.clouds);
const missing = getMissingClouds(mockManifest, "aider", clouds);
expect(missing).toContain("hetzner");
expect(missing).not.toContain("sprite");
});
it("should return empty array for fully implemented agent", () => {
const clouds = Object.keys(mockManifest.clouds);
const missing = getMissingClouds(mockManifest, "claude", clouds);
expect(missing).toEqual([]);
});
it("should return all clouds when agent has no implementations", () => {
const clouds = Object.keys(allMissingManifest.clouds);
const missing = getMissingClouds(allMissingManifest, "claude", clouds);
expect(missing).toEqual(["sprite", "hetzner"]);
});
it("should return empty array when clouds list is empty", () => {
const missing = getMissingClouds(mockManifest, "claude", []);
expect(missing).toEqual([]);
});
it("should handle extended manifest with multiple missing clouds", () => {
const clouds = Object.keys(extendedManifest.clouds);
const missing = getMissingClouds(extendedManifest, "aider", clouds);
expect(missing).toContain("hetzner");
expect(missing).toContain("vultr");
expect(missing).not.toContain("sprite");
expect(missing).toHaveLength(2);
});
it("should only filter from the provided clouds list", () => {
// Pass only a subset of clouds
const missing = getMissingClouds(extendedManifest, "aider", ["sprite"]);
expect(missing).toEqual([]);
});
});
// ── getImplementedAgents ──────────────────────────────────────────
describe("getImplementedAgents", () => {
it("should return all agents for a cloud where all are implemented", () => {
const agents = getImplementedAgents(mockManifest, "sprite");
expect(agents).toContain("claude");
expect(agents).toContain("aider");
expect(agents).toHaveLength(2);
});
it("should return only implemented agents for partially implemented cloud", () => {
const agents = getImplementedAgents(mockManifest, "hetzner");
expect(agents).toContain("claude");
expect(agents).not.toContain("aider");
expect(agents).toHaveLength(1);
});
it("should return empty array when cloud has no implementations", () => {
const agents = getImplementedAgents(allMissingManifest, "sprite");
expect(agents).toEqual([]);
});
it("should return empty array for nonexistent cloud", () => {
const agents = getImplementedAgents(mockManifest, "nonexistent");
expect(agents).toEqual([]);
});
it("should handle extended manifest correctly", () => {
const agents = getImplementedAgents(extendedManifest, "vultr");
expect(agents).toContain("claude");
expect(agents).toContain("codex");
expect(agents).not.toContain("aider");
expect(agents).toHaveLength(2);
});
it("should return all agents when all are implemented", () => {
const agents = getImplementedAgents(allImplementedManifest, "sprite");
expect(agents).toContain("claude");
expect(agents).toContain("aider");
expect(agents).toHaveLength(2);
});
});
// ── getImplementedClouds ──────────────────────────────────────────
describe("getImplementedClouds", () => {
it("should return all clouds for a fully implemented agent", () => {
const clouds = getImplementedClouds(mockManifest, "claude");
expect(clouds).toContain("sprite");
expect(clouds).toContain("hetzner");
expect(clouds).toHaveLength(2);
});
it("should return only implemented clouds for partially implemented agent", () => {
const clouds = getImplementedClouds(mockManifest, "aider");
expect(clouds).toContain("sprite");
expect(clouds).not.toContain("hetzner");
expect(clouds).toHaveLength(1);
});
it("should return empty array when agent has no implementations", () => {
const clouds = getImplementedClouds(allMissingManifest, "claude");
expect(clouds).toEqual([]);
});
it("should return empty array for nonexistent agent", () => {
const clouds = getImplementedClouds(mockManifest, "nonexistent");
expect(clouds).toEqual([]);
});
it("should handle extended manifest with three clouds", () => {
const clouds = getImplementedClouds(extendedManifest, "claude");
expect(clouds).toContain("sprite");
expect(clouds).toContain("hetzner");
expect(clouds).toContain("vultr");
expect(clouds).toHaveLength(3);
});
it("should handle agent with sparse implementations", () => {
const clouds = getImplementedClouds(extendedManifest, "codex");
expect(clouds).toContain("vultr");
expect(clouds).not.toContain("sprite");
expect(clouds).not.toContain("hetzner");
expect(clouds).toHaveLength(1);
});
});
// ── getErrorMessage ───────────────────────────────────────────────
describe("getErrorMessage", () => {
it("should extract message from Error objects", () => {
expect(getErrorMessage(new Error("test error"))).toBe("test error");
});
it("should extract message from Error subclasses", () => {
expect(getErrorMessage(new TypeError("type error"))).toBe("type error");
expect(getErrorMessage(new RangeError("range error"))).toBe("range error");
});
it("should handle objects with message property (duck typing)", () => {
expect(getErrorMessage({ message: "custom error" })).toBe("custom error");
});
it("should handle objects with numeric message", () => {
expect(getErrorMessage({ message: 42 })).toBe("42");
});
it("should stringify string values", () => {
expect(getErrorMessage("string error")).toBe("string error");
});
it("should stringify numbers", () => {
expect(getErrorMessage(42)).toBe("42");
});
it("should stringify null", () => {
expect(getErrorMessage(null)).toBe("null");
});
it("should stringify undefined", () => {
expect(getErrorMessage(undefined)).toBe("undefined");
});
it("should stringify boolean", () => {
expect(getErrorMessage(false)).toBe("false");
});
it("should handle empty Error message", () => {
expect(getErrorMessage(new Error(""))).toBe("");
});
it("should handle object without message property", () => {
const result = getErrorMessage({ code: "ERR" });
expect(result).toBe("[object Object]");
});
});
// ── calculateColumnWidth ──────────────────────────────────────────
describe("calculateColumnWidth", () => {
it("should respect minimum width when items are short", () => {
expect(calculateColumnWidth(["a", "b"], 15)).toBe(15);
});
it("should expand for items longer than minimum", () => {
// "Hello World" (11 chars) + COL_PADDING (2) = 13
expect(calculateColumnWidth(["Hello World"], 10)).toBe(13);
});
it("should use the longest item to determine width", () => {
// "very long name" (14 chars) + padding (2) = 16
expect(calculateColumnWidth(["short", "very long name"], 10)).toBe(16);
});
it("should return minimum width for empty array", () => {
expect(calculateColumnWidth([], 20)).toBe(20);
});
it("should handle single-character items", () => {
// "a" (1) + padding (2) = 3, but min is 10
expect(calculateColumnWidth(["a"], 10)).toBe(10);
});
it("should handle very long items", () => {
const longItem = "A".repeat(100);
// 100 + 2 = 102
expect(calculateColumnWidth([longItem], 10)).toBe(102);
});
it("should handle items exactly at minimum width", () => {
// Item of length 8 + padding 2 = 10, which equals minimum
expect(calculateColumnWidth(["12345678"], 10)).toBe(10);
});
it("should handle items one character over minimum", () => {
// Item of length 9 + padding 2 = 11, exceeds minimum of 10
expect(calculateColumnWidth(["123456789"], 10)).toBe(11);
});
});
// ── getStatusDescription ──────────────────────────────────────────
describe("getStatusDescription", () => {
it("should return 'not found' for 404", () => {
expect(getStatusDescription(404)).toBe("not found");
});
it("should return HTTP code string for 200", () => {
expect(getStatusDescription(200)).toBe("HTTP 200");
});
it("should return HTTP code string for 500", () => {
expect(getStatusDescription(500)).toBe("HTTP 500");
});
it("should return HTTP code string for 403", () => {
expect(getStatusDescription(403)).toBe("HTTP 403");
});
it("should return HTTP code string for 401", () => {
expect(getStatusDescription(401)).toBe("HTTP 401");
});
it("should return HTTP code string for 502", () => {
expect(getStatusDescription(502)).toBe("HTTP 502");
});
it("should return HTTP code string for 503", () => {
expect(getStatusDescription(503)).toBe("HTTP 503");
});
});
});

View file

@ -20,7 +20,7 @@ import { validateIdentifier, validateScriptContent, validatePrompt } from "./sec
const FETCH_TIMEOUT = 10_000; // 10 seconds
function getErrorMessage(err: unknown): string {
export function getErrorMessage(err: unknown): string {
// Use duck typing instead of instanceof to avoid prototype chain issues
return err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
}
@ -71,7 +71,7 @@ function mapToSelectOptions<T extends { name: string; description: string }>(
}));
}
function getImplementedClouds(manifest: Manifest, agent: string): string[] {
export function getImplementedClouds(manifest: Manifest, agent: string): string[] {
return cloudKeys(manifest).filter(
(c: string): boolean => matrixStatus(manifest, c, agent) === "implemented"
);
@ -317,7 +317,7 @@ export async function cmdRun(agent: string, cloud: string, prompt?: string): Pro
await execScript(cloud, agent, prompt);
}
function getStatusDescription(status: number): string {
export function getStatusDescription(status: number): string {
return status === 404 ? "not found" : `HTTP ${status}`;
}
@ -439,11 +439,11 @@ const NAME_COLUMN_WIDTH = 18;
const COMPACT_NAME_WIDTH = 20;
const COMPACT_COUNT_WIDTH = 10;
function getTerminalWidth(): number {
export function getTerminalWidth(): number {
return process.stdout.columns || 80;
}
function calculateColumnWidth(items: string[], minWidth: number): number {
export function calculateColumnWidth(items: string[], minWidth: number): number {
let maxWidth = minWidth;
for (const item of items) {
const width = item.length + COL_PADDING;
@ -481,7 +481,7 @@ function renderMatrixRow(agent: string, clouds: string[], manifest: Manifest, ag
return row;
}
function getMissingClouds(manifest: Manifest, agent: string, clouds: string[]): string[] {
export function getMissingClouds(manifest: Manifest, agent: string, clouds: string[]): string[] {
return clouds.filter((c) => matrixStatus(manifest, c, agent) !== "implemented");
}
@ -557,7 +557,7 @@ export async function cmdList(): Promise<void> {
// ── Agents ─────────────────────────────────────────────────────────────────────
function getImplementedAgents(manifest: Manifest, cloud: string): string[] {
export function getImplementedAgents(manifest: Manifest, cloud: string): string[] {
return agentKeys(manifest).filter(
(a: string): boolean => matrixStatus(manifest, cloud, a) === "implemented"
);