import assert from "node:assert/strict"; import test from "node:test"; import { createExtensionRuntime, ExtensionRunner, type Extension, type ToolDefinition, type ToolRendererResolver, type ToolRenderers, } from "@selesai/code"; import { Text } from "@earendil-works/pi-tui"; import { normalizeToolDisplayConfig } from "../src/config-store.ts"; import { registerToolDisplayOverrides } from "../src/tool-overrides.ts"; import { DEFAULT_TOOL_DISPLAY_CONFIG, type ToolDisplayConfig } from "../src/types.ts"; import { createExtensionApiHarness, createTestTheme, renderComponentText, resolveToolRenderers, } from "./tool-renderer-harness.ts"; interface NormalizedCustomToolOverride { enabled: boolean; kind: "generic" | "mcp"; outputMode: "hidden" | "summary" | "preview"; } interface ToolDisplayConfigWithCustomOverrides extends ToolDisplayConfig { customToolOverrides: Record; } function buildConfigWithCustomOverrides( customToolOverrides: Record, overrides: Partial = {}, ): ToolDisplayConfig { return { ...DEFAULT_TOOL_DISPLAY_CONFIG, ...overrides, boxedToolCalls: overrides.boxedToolCalls ?? false, registerToolOverrides: { ...DEFAULT_TOOL_DISPLAY_CONFIG.registerToolOverrides, ...overrides.registerToolOverrides, }, customToolOverrides, } as ToolDisplayConfig; } function createDisplayResolver(config: ToolDisplayConfig): ToolRendererResolver { const harness = createExtensionApiHarness(); registerToolDisplayOverrides(harness.api, () => config); assert.equal(harness.resolvers.length, 1, "pi-tool-display registers one resolver"); assert.equal(harness.registeredTools.length, 0, "pi-tool-display registers no tools"); return harness.resolvers[0]!; } function createOwnerTool( name: string, options: { promptSnippet?: string; promptGuidelines?: string[] } = {}, ): { tool: ToolDefinition; execute: ToolDefinition["execute"] } { const execute = (async () => ({ content: [{ type: "text" as const, text: "executed" }], details: {}, })) as ToolDefinition["execute"]; const tool = { name, label: `Owner ${name}`, description: `Owner description for ${name}`, promptSnippet: options.promptSnippet ?? `Prompt snippet for ${name}`, promptGuidelines: options.promptGuidelines ?? [`Guideline for ${name}`], parameters: { type: "object", properties: { query: { type: "string" } } }, execute, renderCall: () => new Text("OWNER CALL", 0, 0), renderResult: () => new Text("OWNER RESULT", 0, 0), } as ToolDefinition; return { tool, execute }; } function createExtensionEntry( path: string, options: { tools?: ToolDefinition[]; resolvers?: ToolRendererResolver[] } = {}, ): Extension { const sourceInfo = { path, source: "local", scope: "user" as const, origin: "top-level" as const }; const tools = new Map( (options.tools ?? []).map((definition) => [definition.name, { definition, sourceInfo }]), ); return { path, resolvedPath: path, sourceInfo, handlers: new Map(), tools, messageRenderers: new Map(), toolRenderers: options.resolvers, commands: new Map(), flags: new Map(), shortcuts: new Map(), }; } /** * The renderer path the interactive TUI uses: extension resolvers in load order, * then the registered tool definition of the extension that owns the tool. */ function resolveThroughRunner( extensions: Extension[], toolName: string, definition: ToolDefinition, ): ToolRenderers | undefined { const runner = new ExtensionRunner( extensions, createExtensionRuntime(), process.cwd(), {} as never, {} as never, ); return runner.resolveToolRenderers(toolName, () => definition); } function renderCall(renderers: ToolRenderers | undefined, args: unknown): string { assert.ok(renderers?.renderCall, "expected renderCall"); return renderComponentText( renderers.renderCall(args as never, createTestTheme() as never, {} as never), ); } function renderToolRawResult( renderers: ToolRenderers | undefined, result: Record, options: Record = {}, ): string { assert.ok(renderers?.renderResult, "expected renderResult"); return renderComponentText( renderers.renderResult( result as never, { expanded: false, isPartial: false, ...options } as never, createTestTheme() as never, {} as never, ), ); } function renderToolResult( renderers: ToolRenderers | undefined, text: string, options: Record = {}, ): string { return renderToolRawResult( renderers, { content: [{ type: "text", text }], details: {}, isError: false }, options, ); } function snapshotDefinition(definition: ToolDefinition): Record { return { name: definition.name, label: definition.label, description: definition.description, promptSnippet: definition.promptSnippet, promptGuidelines: definition.promptGuidelines ? [...definition.promptGuidelines] : undefined, parameters: definition.parameters, renderCall: definition.renderCall, renderResult: definition.renderResult, execute: definition.execute, }; } test("normalizeToolDisplayConfig defaults customToolOverrides to an empty opt-in map", () => { const config = normalizeToolDisplayConfig({}) as unknown as ToolDisplayConfigWithCustomOverrides; assert.deepEqual(config.customToolOverrides, {}); }); test("normalizeToolDisplayConfig normalizes custom tool override shorthand, defaults, and invalid entries", () => { const config = normalizeToolDisplayConfig({ customToolOverrides: { ide_find_symbol: true, " agent_gateway ": { outputMode: "preview" }, mcp_gateway: { enabled: true, kind: "mcp", outputMode: "hidden" }, disabled_tool: false, invalid_kind: { enabled: true, kind: "terminal", outputMode: "verbose" }, read: { enabled: true, kind: "mcp", outputMode: "summary" }, "": { enabled: true }, " ": true, }, }) as unknown as ToolDisplayConfigWithCustomOverrides; assert.deepEqual(config.customToolOverrides, { ide_find_symbol: { enabled: true, kind: "generic", outputMode: "summary" }, agent_gateway: { enabled: true, kind: "generic", outputMode: "preview" }, mcp_gateway: { enabled: true, kind: "mcp", outputMode: "hidden" }, disabled_tool: { enabled: false, kind: "generic", outputMode: "summary" }, invalid_kind: { enabled: true, kind: "generic", outputMode: "summary" }, }); }); test("normalizeToolDisplayConfig treats malformed customToolOverrides containers as empty", () => { for (const rawCustomOverrides of [null, true, "ide_find_symbol", [], 42]) { const config = normalizeToolDisplayConfig({ customToolOverrides: rawCustomOverrides, }) as unknown as ToolDisplayConfigWithCustomOverrides; assert.deepEqual(config.customToolOverrides, {}); } }); test("normalizeToolDisplayConfig preserves supported custom output modes and drops unknown entry fields", () => { const config = normalizeToolDisplayConfig({ customToolOverrides: { hidden_tool: { enabled: true, outputMode: "hidden", label: "Ignored Label" }, summary_tool: { enabled: true, outputMode: "summary", pathFields: ["file_path"] }, preview_tool: { enabled: true, outputMode: "preview", renderShell: "self" }, }, }) as unknown as ToolDisplayConfigWithCustomOverrides; assert.deepEqual(config.customToolOverrides, { hidden_tool: { enabled: true, kind: "generic", outputMode: "hidden" }, summary_tool: { enabled: true, kind: "generic", outputMode: "summary" }, preview_tool: { enabled: true, kind: "generic", outputMode: "preview" }, }); }); test("an enabled generic custom override renders a tool owned by another extension", () => { const { tool } = createOwnerTool("ide_find_symbol"); const owner = createExtensionEntry("/extensions/owner", { tools: [tool] }); const display = createExtensionEntry("/extensions/pi-tool-display", { resolvers: [createDisplayResolver(buildConfigWithCustomOverrides({ ide_find_symbol: { enabled: true, outputMode: "summary" }, }))], }); const before = snapshotDefinition(tool); const renderers = resolveThroughRunner([display, owner], "ide_find_symbol", tool); assert.equal(renderCall(renderers, { query: "Widget", limit: 5 }), "ide_find_symbol (2 args)"); assert.equal(renderToolResult(renderers, "alpha\nbeta\ngamma\n"), "↳ 3 lines returned • Ctrl+O to expand"); assert.deepEqual(snapshotDefinition(tool), before, "the owning definition must not change"); assert.equal(owner.tools.get("ide_find_symbol")?.definition, tool); }); test("a disabled or unlisted custom override leaves the owning extension's renderers in charge", () => { const { tool } = createOwnerTool("disabled_noisy_tool"); const owner = createExtensionEntry("/extensions/owner", { tools: [tool] }); const display = createExtensionEntry("/extensions/pi-tool-display", { resolvers: [createDisplayResolver(buildConfigWithCustomOverrides({ disabled_noisy_tool: { enabled: false, outputMode: "summary" }, }))], }); const before = snapshotDefinition(tool); const disabled = resolveThroughRunner([display, owner], "disabled_noisy_tool", tool); assert.equal(renderCall(disabled, {}), "OWNER CALL"); assert.equal(renderToolResult(disabled, "ignored"), "OWNER RESULT"); const unlisted = resolveThroughRunner([display, owner], "unlisted_noisy_tool", tool); assert.equal(renderCall(unlisted, {}), "OWNER CALL"); assert.equal(renderToolResult(unlisted, "ignored"), "OWNER RESULT"); assert.deepEqual(snapshotDefinition(tool), before); }); test("custom overrides match the exact tool name only", () => { const resolver = createDisplayResolver(buildConfigWithCustomOverrides({ agent_gateway: { enabled: true, outputMode: "summary" }, })); const nextRenderers: ToolRenderers = { renderCall: () => new Text("OWNER CALL", 0, 0), renderResult: () => new Text("OWNER RESULT", 0, 0), }; const exact = resolveToolRenderers([resolver], "agent_gateway", () => nextRenderers); assert.equal(renderCall(exact, {}), "agent_gateway (no args)"); assert.equal(resolveToolRenderers([resolver], "agent_gateway_extra", () => nextRenderers), nextRenderers); }); test("custom tool override defaults kind to generic unless the user chooses mcp", () => { const resolver = createDisplayResolver(buildConfigWithCustomOverrides({ remote_gateway: { enabled: true }, remote_gateway_structured: { enabled: true, kind: "mcp" }, })); assert.equal( renderCall(resolveToolRenderers([resolver], "remote_gateway", () => undefined), { tool: "read_file", server: "filesystem" }), "remote_gateway (2 args)", ); assert.equal( renderCall(resolveToolRenderers([resolver], "remote_gateway_structured", () => undefined), { tool: "read_file", server: "filesystem" }), "MCP call filesystem:read_file (2 args)", ); }); test("custom generic tool override hides collapsed output and reveals it expanded", () => { const resolver = createDisplayResolver(buildConfigWithCustomOverrides({ large_payload_tool: { enabled: true, outputMode: "hidden" }, })); const renderers = resolveToolRenderers([resolver], "large_payload_tool", () => undefined); assert.equal(renderToolResult(renderers, "secret\nnoisy\noutput\n"), ""); assert.equal(renderToolResult(renderers, "secret\nnoisy\noutput\n", { expanded: true }), "secret\nnoisy\noutput"); }); test("custom tool overrides never register or add phantom tools", () => { const harness = createExtensionApiHarness(); registerToolDisplayOverrides(harness.api, () => buildConfigWithCustomOverrides({ missing_tool: { enabled: true, outputMode: "summary" }, })); // Name-based resolution keeps historical calls renderable without inventing a tool. assert.ok(resolveToolRenderers(harness.resolvers, "missing_tool", () => undefined)); assert.equal(harness.registeredTools.length, 0); assert.equal(harness.api.getAllTools().some((tool) => (tool as { name?: string }).name === "missing_tool"), false); }); test("generic custom tool renderCall handles absent, non-object, and nested arguments safely", () => { const resolver = createDisplayResolver(buildConfigWithCustomOverrides({ argument_probe: { enabled: true, outputMode: "summary" }, })); const renderers = resolveToolRenderers([resolver], "argument_probe", () => undefined); assert.equal(renderCall(renderers, undefined), "argument_probe (no args)"); assert.equal(renderCall(renderers, null), "argument_probe (no args)"); assert.equal(renderCall(renderers, "raw string args"), "argument_probe (no args)"); assert.equal(renderCall(renderers, ["array", "args"]), "argument_probe (no args)"); assert.equal( renderCall(renderers, { path: "src/index.ts", options: { recursive: true }, tags: ["a", "b"] }), "argument_probe (3 args)", ); }); test("generic custom tool preview mode supports collapsed previews, expanded previews, partial state, and empty text", () => { const resolver = createDisplayResolver(buildConfigWithCustomOverrides( { preview_payload_tool: { enabled: true, outputMode: "preview" } }, { previewLines: 2 }, )); const renderers = resolveToolRenderers([resolver], "preview_payload_tool", () => undefined); assert.equal( renderToolResult(renderers, "alpha\nbeta\ngamma\ndelta\n"), "alpha\nbeta\n... (2 more lines • Ctrl+O to expand)", ); assert.equal( renderToolResult(renderers, "alpha\nbeta\ngamma\ndelta\n", { expanded: true }), "alpha\nbeta\ngamma\ndelta", ); assert.equal(renderToolResult(renderers, "still running", { isPartial: true }), "running..."); assert.equal( renderToolRawResult(renderers, { content: [{ type: "image", data: "ignored" }], details: {} }), "↳ (no output)", ); assert.equal(renderToolRawResult(renderers, { details: {} }), "↳ (no output)"); }); test("explicit mcp custom tool override interprets MCP proxy argument variants", () => { const resolver = createDisplayResolver(buildConfigWithCustomOverrides({ custom_gateway: { enabled: true, kind: "mcp", outputMode: "summary" }, })); const renderers = resolveToolRenderers([resolver], "custom_gateway", () => undefined); assert.equal(renderCall(renderers, {}), "MCP status (no args)"); assert.equal(renderCall(renderers, { connect: "filesystem" }), "MCP connect filesystem (1 arg)"); assert.equal(renderCall(renderers, { describe: "read_file", server: "filesystem" }), "MCP describe read_file @filesystem (2 args)"); assert.equal(renderCall(renderers, { search: "browser", server: "exa" }), "MCP search \"browser\" @exa (2 args)"); assert.equal(renderCall(renderers, { server: "filesystem" }), "MCP tools filesystem (1 arg)"); assert.equal(renderCall(renderers, { tool: "read_file", server: "filesystem" }), "MCP call filesystem:read_file (2 args)"); }); test("custom tool override preserves execution contract, parameters, prepareArguments, and prompt metadata", () => { const prepareArguments = (args: unknown): unknown => args; const { tool, execute } = createOwnerTool("contract_tool", { promptSnippet: "Contract snippet", promptGuidelines: ["Contract guideline"], }); tool.prepareArguments = prepareArguments; const parameters = { type: "object", properties: { query: { type: "string" } } }; tool.parameters = parameters; const owner = createExtensionEntry("/extensions/owner", { tools: [tool] }); const display = createExtensionEntry("/extensions/pi-tool-display", { resolvers: [createDisplayResolver(buildConfigWithCustomOverrides({ contract_tool: { enabled: true, outputMode: "summary" }, }))], }); const before = snapshotDefinition(tool); const renderers = resolveThroughRunner([display, owner], "contract_tool", tool); assert.equal(renderCall(renderers, { query: "value" }), "contract_tool (1 arg)"); assert.deepEqual(snapshotDefinition(tool), before); assert.equal(tool.execute, execute); assert.equal(tool.prepareArguments, prepareArguments); assert.equal(tool.parameters, parameters); assert.equal(tool.promptSnippet, "Contract snippet"); assert.deepEqual(tool.promptGuidelines, ["Contract guideline"]); });