test: add 34 tests for cmdList table rendering and resolveDisplayName (#515)

Cover the integration of cmdList with manifest-resolved display names,
table header/separator rendering, prompt preview truncation in rows
vs footer, filtered result counts, timestamp formatting, and the
resolveDisplayName exported utility.

Agent: test-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-11 13:26:21 -08:00 committed by GitHub
parent 66b890493a
commit 9a5502906d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,526 @@
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { mkdirSync, writeFileSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { createMockManifest, createConsoleMocks, restoreMocks } from "./test-helpers";
import { loadManifest } from "../manifest";
import type { Manifest } from "../manifest";
/**
* Tests for cmdList table rendering with manifest-resolved display names.
*
* cmdList (commands.ts:845-882) renders spawn history as a formatted table:
* - Resolves agent/cloud keys to display names via the manifest
* - Falls back to raw keys when manifest is unavailable
* - Shows table header: AGENT, CLOUD, WHEN
* - Renders rows with agent name (green), cloud name, timestamp, optional prompt
* - Truncates prompt previews > 40 chars in rows
* - Delegates footer to showListFooter (tested in list-empty-footer.test.ts)
*
* resolveDisplayName (commands.ts:839-843) is exported but has no direct tests:
* - Returns entry.name when key exists in manifest
* - Returns raw key when key is not in manifest
* - Returns raw key when manifest is null
*
* Agent: test-engineer
*/
const mockManifest = createMockManifest();
// 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,
error: mockLogError,
warn: mock(() => {}),
success: mock(() => {}),
},
intro: mock(() => {}),
outro: mock(() => {}),
cancel: mock(() => {}),
select: mock(() => {}),
isCancel: () => false,
}));
// Import after mock setup
const { cmdList, resolveDisplayName } = await import("../commands.js");
// ── resolveDisplayName direct tests ──────────────────────────────────────────
describe("resolveDisplayName", () => {
it("should return agent display name when key exists", () => {
expect(resolveDisplayName(mockManifest, "claude", "agent")).toBe("Claude Code");
});
it("should return cloud display name when key exists", () => {
expect(resolveDisplayName(mockManifest, "sprite", "cloud")).toBe("Sprite");
});
it("should return raw key when agent key is not in manifest", () => {
expect(resolveDisplayName(mockManifest, "unknown-agent", "agent")).toBe("unknown-agent");
});
it("should return raw key when cloud key is not in manifest", () => {
expect(resolveDisplayName(mockManifest, "unknown-cloud", "cloud")).toBe("unknown-cloud");
});
it("should return raw key when manifest is null", () => {
expect(resolveDisplayName(null, "claude", "agent")).toBe("claude");
});
it("should return raw key when manifest is null for cloud", () => {
expect(resolveDisplayName(null, "sprite", "cloud")).toBe("sprite");
});
it("should handle second agent key correctly", () => {
expect(resolveDisplayName(mockManifest, "aider", "agent")).toBe("Aider");
});
it("should handle second cloud key correctly", () => {
expect(resolveDisplayName(mockManifest, "hetzner", "cloud")).toBe("Hetzner Cloud");
});
});
// ── cmdList table rendering integration ──────────────────────────────────────
describe("cmdList table rendering", () => {
let consoleMocks: ReturnType<typeof createConsoleMocks>;
let originalFetch: typeof global.fetch;
let testDir: string;
let originalEnv: NodeJS.ProcessEnv;
beforeEach(async () => {
consoleMocks = createConsoleMocks();
mockLogError.mockClear();
mockLogInfo.mockClear();
mockLogStep.mockClear();
mockSpinnerStart.mockClear();
mockSpinnerStop.mockClear();
originalFetch = global.fetch;
originalEnv = { ...process.env };
// Set up temp history dir
testDir = join(tmpdir(), `spawn-list-table-test-${Date.now()}-${Math.random()}`);
mkdirSync(testDir, { recursive: true });
process.env.SPAWN_HOME = testDir;
});
afterEach(() => {
global.fetch = originalFetch;
process.env = originalEnv;
restoreMocks(consoleMocks.log, consoleMocks.error);
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
function setManifest(manifest: any) {
global.fetch = mock(async () => ({
ok: true,
json: async () => manifest,
text: async () => JSON.stringify(manifest),
})) as any;
return loadManifest(true);
}
function writeHistory(records: Array<{ agent: string; cloud: string; timestamp: string; prompt?: string }>) {
writeFileSync(join(testDir, "history.json"), JSON.stringify(records));
}
function getOutput(): string {
return consoleMocks.log.mock.calls
.map((c: any[]) => c.join(" "))
.join("\n");
}
// ── Table header ───────────────────────────────────────────────────────
describe("table header", () => {
it("should render AGENT, CLOUD, WHEN header columns", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("AGENT");
expect(output).toContain("CLOUD");
expect(output).toContain("WHEN");
});
it("should render a separator line with dashes", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("----");
});
});
// ── Display name resolution in rows ────────────────────────────────────
describe("display name resolution", () => {
it("should show agent display name from manifest", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("Claude Code");
});
it("should show cloud display name from manifest", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "hetzner", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("Hetzner Cloud");
});
it("should show raw key when agent is not in manifest", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "unknown-agent", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("unknown-agent");
});
it("should show raw key when cloud is not in manifest", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "unknown-cloud", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("unknown-cloud");
});
it("should fall back to raw keys when manifest fetch fails", async () => {
global.fetch = mock(async () => {
throw new Error("Network error");
}) as any;
// Force manifest cache to be cleared
try { await loadManifest(true); } catch { /* expected */ }
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
// When manifest is unavailable, the raw keys should be shown
// Either "claude" or "Claude Code" is acceptable depending on cache
expect(output).toMatch(/claude|Claude Code/);
});
});
// ── Multiple rows ──────────────────────────────────────────────────────
describe("multiple rows", () => {
it("should render all records in reverse chronological order", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
{ agent: "claude", cloud: "hetzner", timestamp: "2026-01-03T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
// All agents should appear
expect(output).toContain("Claude Code");
expect(output).toContain("Aider");
// All clouds should appear
expect(output).toContain("Sprite");
expect(output).toContain("Hetzner Cloud");
});
it("should show rerun hint with most recent record", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
// Most recent is aider/hetzner (reversed = first), so rerun hint should use it
expect(output).toContain("spawn aider hetzner");
});
});
// ── Prompt preview in rows ─────────────────────────────────────────────
describe("prompt preview in rows", () => {
it("should show short prompt inline with row", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z", prompt: "Fix bugs" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("Fix bugs");
});
it("should truncate prompt > 40 chars in row with ellipsis", async () => {
await setManifest(mockManifest);
const longPrompt = "A".repeat(50);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z", prompt: longPrompt },
]);
await cmdList();
const output = getOutput();
// Should show first 40 chars + "..."
expect(output).toContain("A".repeat(40) + "...");
// Should NOT show the full 50-char prompt
expect(output).not.toContain("A".repeat(50));
});
it("should show exactly 40-char prompt without truncation", async () => {
await setManifest(mockManifest);
const exactPrompt = "B".repeat(40);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z", prompt: exactPrompt },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("B".repeat(40));
// Should not have ellipsis since it's exactly 40
expect(output).not.toContain("B".repeat(40) + "...");
});
it("should not show --prompt in row when no prompt given", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).not.toContain("--prompt");
});
});
// ── Prompt in footer rerun hint ────────────────────────────────────────
describe("prompt in footer rerun hint", () => {
it("should include --prompt in rerun hint when latest has prompt", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z", prompt: "Fix bugs" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain('--prompt "Fix bugs"');
});
it("should truncate long prompt in rerun hint (> 30 chars)", async () => {
await setManifest(mockManifest);
const longPrompt = "C".repeat(35);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z", prompt: longPrompt },
]);
await cmdList();
const output = getOutput();
// Footer truncates at 30 chars
expect(output).toContain("C".repeat(30) + "...");
});
it("should not include --prompt in rerun hint when latest has no prompt", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
// Rerun line should not have --prompt
const lines = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" "));
const rerunLine = lines.find((l: string) => l.includes("Rerun last"));
expect(rerunLine).toBeDefined();
expect(rerunLine!).not.toContain("--prompt");
});
});
// ── Filtered results display ───────────────────────────────────────────
describe("filtered results", () => {
it("should show only matching records with agent filter", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
{ agent: "claude", cloud: "hetzner", timestamp: "2026-01-03T10:00:00.000Z" },
]);
await cmdList("claude");
const output = getOutput();
expect(output).toContain("Claude Code");
// Aider should not appear in filtered results
expect(output).not.toContain("Aider");
});
it("should show only matching records with cloud filter", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
]);
await cmdList(undefined, "sprite");
const output = getOutput();
expect(output).toContain("Sprite");
// Hetzner should not appear
expect(output).not.toContain("Hetzner Cloud");
});
it("should show 'Showing N of M' when filters are active", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
{ agent: "claude", cloud: "hetzner", timestamp: "2026-01-03T10:00:00.000Z" },
]);
await cmdList("claude");
const output = getOutput();
expect(output).toContain("Showing 2 of 3");
});
it("should show 'Clear filter' hint when filters are active", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
]);
await cmdList("claude");
const output = getOutput();
expect(output).toContain("Clear filter");
expect(output).toContain("spawn list");
});
it("should show filter hint when no filters are active", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("Filter");
expect(output).toContain("spawn list -a");
});
it("should show total count without 'Showing' when unfiltered", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-01-01T10:00:00.000Z" },
{ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("2 spawns recorded");
expect(output).not.toContain("Showing");
});
it("should use singular 'spawn' for single record", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
expect(output).toContain("1 spawn recorded");
// Should not say "1 spawns"
expect(output).not.toContain("1 spawns");
});
});
// ── Timestamp formatting in rows ───────────────────────────────────────
describe("timestamp display", () => {
it("should show formatted date for valid ISO timestamp", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T14:30:00.000Z" },
]);
await cmdList();
const output = getOutput();
// Should contain a formatted date, not the raw ISO string
expect(output).toContain("2026");
// The exact format depends on locale, but should contain month/day
expect(output).toContain("Feb");
});
it("should handle invalid timestamp gracefully", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "not-a-date" },
]);
await cmdList();
const output = getOutput();
// formatTimestamp returns the raw string for invalid dates
expect(output).toContain("not-a-date");
});
});
// ── Single record edge case ────────────────────────────────────────────
describe("single record", () => {
it("should render a complete table for a single record", async () => {
await setManifest(mockManifest);
writeHistory([
{ agent: "claude", cloud: "sprite", timestamp: "2026-02-11T10:00:00.000Z" },
]);
await cmdList();
const output = getOutput();
// Header
expect(output).toContain("AGENT");
expect(output).toContain("CLOUD");
expect(output).toContain("WHEN");
// Data
expect(output).toContain("Claude Code");
expect(output).toContain("Sprite");
// Footer
expect(output).toContain("Rerun last");
expect(output).toContain("spawn claude sprite");
});
});
});