import { describe, expect, test } from "bun:test"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { Box, visibleWidth } from "@earendil-works/pi-tui"; import { renderMcpToolCall, renderMcpToolResult } from "./rendering.ts"; const WIDTH = 48; const THEME = { bold: (value: string) => value, fg: (_name: string, value: string) => value, }; const MARKED_THEME = { bold: (value: string) => `${value}`, fg: (name: string, value: string) => `<${name}>${value}`, }; describe("mcp-wrapper rendering", () => { test("renders call rows with the Pi tool name", () => { const component = new Box(1, 1); component.addChild( renderMcpToolCall( "fetch_fetch", { url: "https://pi.dev/docs/latest/extensions" }, THEME as never, ), ); const output = component.render(WIDTH).join("\n"); expect(output).toContain("fetch_fetch:"); expect(output).not.toContain("MCP:"); for (const line of component.render(WIDTH)) { expect(visibleWidth(line)).toBeLessThanOrEqual(WIDTH); } }); test("dims call arguments while keeping the Pi tool name prominent", () => { const output = renderMcpToolCall( "fetch_fetch", { url: "https://pi.dev/docs/latest/extensions" }, MARKED_THEME as never, ) .render(120) .join("\n"); expect(output).toStartWith( "fetch_fetch: ", ); expect(output).toContain( '{"url":"https://pi.dev/docs/latest/extensions"}', ); }); test("wraps long call arguments within the default Pi tool shell width", () => { const component = new Box(1, 1); component.addChild( renderMcpToolCall( "team_message_create", { topic_id: "17537fcd-7230-49bf-ab1d-a78b5ff4ab30", title: "Audit task context", content: "Task: audit implementation against pricing rules", }, THEME as never, { expanded: true } as never, ), ); const lines = component.render(WIDTH); const output = lines.join("\n"); expect(lines.length).toBeGreaterThan(3); expect(output).toContain("team_message_create:"); expect(output).toContain("pricing rules"); expect(output).not.toContain("…"); for (const line of lines) { expect(visibleWidth(line)).toBeLessThanOrEqual(WIDTH); } }); test("counts wrapped call arguments against the call preview line budget", () => { // Purpose: mcp-wrapper call rendering must not let huge wrapped arguments consume unbounded TUI height. // Input and expected output: huge arguments are capped when collapsed and show a standard hidden-line hint. // Edge case: the limit is checked through the default Box shell, whose padding adds two outer rows. // Dependencies: this test uses the public Pi Box shell and visible-width measurement. const component = new Box(1, 1); component.addChild( renderMcpToolCall( "team_message_create", { content: Array.from( { length: 80 }, (_, index) => `argument-token-${index}`, ).join(" "), }, THEME as never, ), ); const lines = component.render(WIDTH); const output = lines.join("\n"); expect(lines).toHaveLength(6); expect(output).toContain("team_message_create:"); expect(output).toContain("more lines"); expect(output).toContain("total"); expect(output).toContain("to expand"); expect(output).not.toContain("argument-token-79"); for (const line of lines) { expect(visibleWidth(line)).toBeLessThanOrEqual(WIDTH); } }); test("shows full wrapped call arguments when the call header is expanded", () => { // Purpose: mcp-wrapper must not lose hidden argument text when the user expands the call header. // Input and expected output: huge arguments are capped when collapsed and fully visible when expanded. // Edge case: the final token is beyond the collapsed line budget. // Dependencies: this test uses the renderCall expanded flag from Pi's ToolRenderContext. const args = { content: Array.from( { length: 80 }, (_, index) => `argument-token-${index}`, ).join(" "), }; const collapsedLines = renderMcpToolCall( "team_message_create", args, THEME as never, { expanded: false } as never, ).render(WIDTH); const expandedLines = renderMcpToolCall( "team_message_create", args, THEME as never, { expanded: true } as never, ).render(WIDTH); expect(collapsedLines.join("\n")).toContain("more lines"); expect(collapsedLines.join("\n")).not.toContain("argument-token-79"); expect(expandedLines.length).toBeGreaterThan(collapsedLines.length); expect(expandedLines.join("")).toContain("argument-token-79"); expect(expandedLines.join("\n")).not.toContain("more lines"); for (const line of expandedLines) { expect(visibleWidth(line)).toBeLessThanOrEqual(WIDTH); } }); test("renders collapsed successful result with a prominent TUI-only header", () => { const result: AgentToolResult = { content: [{ type: "text", text: "result text" }], details: {}, }; expect( renderMcpToolResult(result, {}, MARKED_THEME as never, { widgetLineBudget: 5, }) .render(WIDTH) .join("\n"), ).toBe( "Result: result text", ); }); test("keeps collapsed error result text styled as error", () => { const result: AgentToolResult = { content: [{ type: "text", text: "error text" }], details: {}, }; expect( renderMcpToolResult(result, {}, MARKED_THEME as never, { isError: true, widgetLineBudget: 5, }) .render(WIDTH) .join("\n"), ).toBe( "Result: error text", ); }); test("renders collapsed result with bounded preview and segmented expand hint colors", () => { const result: AgentToolResult = { content: [ { type: "text", text: Array.from({ length: 20 }, (_, index) => `line ${index}`).join( "\n", ), }, ], details: {}, }; const markedOutput = renderMcpToolResult( result, {}, MARKED_THEME as never, { widgetLineBudget: 2 }, ) .render(200) .join("\n"); const component = new Box(1, 1); component.addChild( renderMcpToolResult(result, {}, THEME as never, { widgetLineBudget: 2 }), ); const lines = component.render(WIDTH); expect(lines.length).toBeLessThanOrEqual(5); expect(markedOutput).toContain("... (18 more lines, 20 total, "); expect(markedOutput).toContain(" to expand)"); for (const line of lines) { expect(visibleWidth(line)).toBeLessThanOrEqual(WIDTH); } }); test("renders expanded full result content with a prominent TUI-only header", () => { const result: AgentToolResult = { content: [{ type: "text", text: "full result" }], details: {}, }; const lines = renderMcpToolResult( result, { expanded: true }, MARKED_THEME as never, { widgetLineBudget: 5 }, ).render(WIDTH); const output = lines.join("\n"); expect(output).toContain("Result:"); expect(output).toContain("full result"); }); });