#!/usr/bin/env npx tsx /** * Tests for the output abstraction layer. * * Verifies that renderers correctly format structured data. */ import assert from "node:assert"; import { render, renderTable, renderJson, renderYaml, renderQuiet, renderError, toCommandError, createOutputOptions, type ListResult, type SingleResult, type OutputSchema, type CommandError, } from "../src/output/index.js"; // Test data types interface Agent { id: string; title: string; status: "running" | "idle" | "error"; provider: string; } interface ChatMessage { id: string; author: string; authorName: string | null; createdAt: string; replyTo: string; mentionAgentIds: string[]; mentionLabels: string[]; body: string; } // Schema for agents const agentSchema: OutputSchema = { idField: "id", columns: [ { header: "ID", field: "id", width: 8 }, { header: "TITLE", field: "title", width: 20 }, { header: "STATUS", field: "status", color: (value) => { switch (value) { case "running": return "green"; case "idle": return "dim"; case "error": return "red"; default: return undefined; } }, }, { header: "PROVIDER", field: "provider" }, ], }; // Test data const testAgents: Agent[] = [ { id: "abc123", title: "Feature Implementation", status: "running", provider: "claude" }, { id: "def456", title: "Bug Fix", status: "idle", provider: "codex" }, { id: "ghi789", title: "Failed Task", status: "error", provider: "claude" }, ]; const listResult: ListResult = { type: "list", data: testAgents, schema: agentSchema, }; const singleResult: SingleResult = { type: "single", data: testAgents[0], schema: agentSchema, }; // Test utilities let passed = 0; let failed = 0; function test(name: string, fn: () => void): void { try { fn(); console.log(`✓ ${name}`); passed++; } catch (error) { console.error(`✗ ${name}`); console.error(` ${error instanceof Error ? error.message : error}`); failed++; } } // Table renderer tests console.log("\n=== Table Renderer ===\n"); test("renderTable formats list data with headers", () => { const output = renderTable(listResult, createOutputOptions({ noColor: true })); assert.ok(output.includes("ID"), "Should include ID header"); assert.ok(output.includes("TITLE"), "Should include TITLE header"); assert.ok(output.includes("STATUS"), "Should include STATUS header"); assert.ok(output.includes("abc123"), "Should include first agent ID"); assert.ok(output.includes("Feature Implementation"), "Should include first agent title"); }); test("renderTable omits headers when noHeaders is true", () => { const output = renderTable(listResult, createOutputOptions({ noHeaders: true, noColor: true })); assert.ok(!output.includes("ID "), "Should not include header row"); assert.ok(output.includes("abc123"), "Should still include data"); }); test("renderTable handles empty data", () => { const emptyResult: ListResult = { type: "list", data: [], schema: agentSchema, }; const output = renderTable(emptyResult, createOutputOptions()); assert.strictEqual(output, "", "Should return empty string for empty data"); }); test("renderTable handles single result", () => { const output = renderTable(singleResult, createOutputOptions({ noColor: true })); assert.ok(output.includes("abc123"), "Should include agent ID"); assert.ok(output.includes("Feature Implementation"), "Should include agent title"); }); // JSON renderer tests console.log("\n=== JSON Renderer ===\n"); test("renderJson outputs valid JSON for list", () => { const output = renderJson(listResult, createOutputOptions()); const parsed = JSON.parse(output); assert.ok(Array.isArray(parsed), "Should be an array"); assert.strictEqual(parsed.length, 3, "Should have 3 items"); assert.strictEqual(parsed[0].id, "abc123", "Should include correct data"); }); test("renderJson outputs valid JSON for single item", () => { const output = renderJson(singleResult, createOutputOptions()); const parsed = JSON.parse(output); assert.strictEqual(parsed.id, "abc123", "Should include correct data"); assert.strictEqual(parsed.title, "Feature Implementation", "Should include correct title"); }); test("renderJson uses custom serializer when provided", () => { const customSchema: OutputSchema = { ...agentSchema, serialize: (agent) => ({ agentId: agent.id, name: agent.title }), }; const customResult: SingleResult = { type: "single", data: testAgents[0], schema: customSchema, }; const output = renderJson(customResult, createOutputOptions()); const parsed = JSON.parse(output); assert.ok("agentId" in parsed, "Should use custom serialization"); assert.ok("name" in parsed, "Should use custom field names"); }); // YAML renderer tests console.log("\n=== YAML Renderer ===\n"); test("renderYaml outputs valid YAML for list", () => { const output = renderYaml(listResult, createOutputOptions()); assert.ok(output.includes("- id: abc123"), "Should format as YAML list"); assert.ok(output.includes("title: Feature Implementation"), "Should include title"); }); test("renderYaml outputs valid YAML for single item", () => { const output = renderYaml(singleResult, createOutputOptions()); assert.ok(output.includes("id: abc123"), "Should include ID"); assert.ok(!output.startsWith("-"), "Should not be a list"); }); // Quiet renderer tests console.log("\n=== Quiet Renderer ===\n"); test("renderQuiet outputs only IDs for list", () => { const output = renderQuiet(listResult, createOutputOptions()); const lines = output.split("\n"); assert.strictEqual(lines.length, 3, "Should have 3 lines"); assert.strictEqual(lines[0], "abc123", "First line should be first ID"); assert.strictEqual(lines[1], "def456", "Second line should be second ID"); }); test("renderQuiet outputs only ID for single item", () => { const output = renderQuiet(singleResult, createOutputOptions()); assert.strictEqual(output, "abc123", "Should output only the ID"); }); test("renderQuiet supports function idField", () => { const customSchema: OutputSchema = { ...agentSchema, idField: (agent) => `${agent.provider}/${agent.id}`, }; const customResult: SingleResult = { type: "single", data: testAgents[0], schema: customSchema, }; const output = renderQuiet(customResult, createOutputOptions()); assert.strictEqual(output, "claude/abc123", "Should use function to extract ID"); }); // Main render dispatcher tests console.log("\n=== Render Dispatcher ===\n"); test("render uses table format by default", () => { const output = render(listResult, { noColor: true }); assert.ok(output.includes("ID"), "Should use table format with headers"); }); test("render uses custom human renderer when provided", () => { const chatSchema: OutputSchema = { idField: "id", columns: [{ header: "ID", field: "id" }], renderHuman: (result) => { const data = result.type === "list" ? result.data : [result.data]; return data.map((message) => `msg ${message.id}: ${message.body}`).join("\n"); }, }; const chatResult: ListResult = { type: "list", data: [ { id: "m1", author: "agent-1", authorName: "Planner", createdAt: "2026-03-29T10:00:00Z", replyTo: "-", mentionAgentIds: [], mentionLabels: [], body: "hello", }, ], schema: chatSchema, }; const output = render(chatResult, { noColor: true }); assert.strictEqual(output, "msg m1: hello", "Should use custom human renderer"); }); test("render uses json format when specified", () => { const output = render(listResult, { format: "json" }); const parsed = JSON.parse(output); assert.ok(Array.isArray(parsed), "Should be valid JSON array"); }); test("render uses yaml format when specified", () => { const output = render(listResult, { format: "yaml" }); assert.ok(output.includes("- id:"), "Should be valid YAML"); }); test("render uses quiet mode when quiet is true", () => { const output = render(listResult, { quiet: true }); assert.strictEqual(output, "abc123\ndef456\nghi789", "Should output only IDs"); }); test("quiet mode takes precedence over format", () => { const output = render(listResult, { format: "json", quiet: true }); assert.strictEqual(output, "abc123\ndef456\nghi789", "Quiet should override format"); }); // Error rendering tests console.log("\n=== Error Rendering ===\n"); test("renderError formats error for table format", () => { const error: CommandError = { code: "NOT_FOUND", message: "Agent not found" }; const output = renderError(error, { noColor: true }); assert.ok(output.includes("Error:"), "Should include Error prefix"); assert.ok(output.includes("Agent not found"), "Should include message"); }); test("renderError formats error as JSON", () => { const error: CommandError = { code: "NOT_FOUND", message: "Agent not found" }; const output = renderError(error, { format: "json" }); const parsed = JSON.parse(output); assert.ok("error" in parsed, "Should have error property"); assert.strictEqual(parsed.error.code, "NOT_FOUND", "Should include error code"); }); test("renderError formats error as YAML", () => { const error: CommandError = { code: "NOT_FOUND", message: "Agent not found" }; const output = renderError(error, { format: "yaml" }); assert.ok(output.includes("error:"), "Should have error key"); assert.ok(output.includes("code: NOT_FOUND"), "Should include error code"); }); test("toCommandError converts Error to CommandError", () => { const error = new Error("Something went wrong"); const commandError = toCommandError(error); assert.strictEqual(commandError.code, "UNKNOWN_ERROR", "Should use UNKNOWN_ERROR code"); assert.strictEqual(commandError.message, "Something went wrong", "Should preserve message"); }); test("toCommandError passes through CommandError", () => { const error: CommandError = { code: "CUSTOM", message: "Custom error" }; const commandError = toCommandError(error); assert.strictEqual(commandError.code, "CUSTOM", "Should preserve code"); assert.strictEqual(commandError.message, "Custom error", "Should preserve message"); }); // Summary console.log("\n=== Summary ===\n"); console.log(`Passed: ${passed}`); console.log(`Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);