import type { ExtensionAPI, ToolRendererResolver, ToolRenderers, } from "@selesai/code"; /** * Fake ExtensionAPI for renderer-resolver tests. * * It records every `registerToolRenderer` resolver and every `registerTool` * call, so a test can prove built-ins are rendered, not re-registered. */ export interface ExtensionApiHarness { api: ExtensionAPI; registeredTools: Array<{ name?: string } & Record>; resolvers: ToolRendererResolver[]; handlers: Map unknown>>; commands: Array<{ name: string; handler?: (...args: any[]) => unknown }>; } export interface ExtensionApiHarnessOptions { tools?: unknown[]; commands?: Array<{ name: string }>; } export function createExtensionApiHarness( options: ExtensionApiHarnessOptions = {}, ): ExtensionApiHarness { const registeredTools: Array<{ name?: string } & Record> = []; const resolvers: ToolRendererResolver[] = []; const handlers = new Map unknown>>(); const commands: Array<{ name: string; handler?: (...args: any[]) => unknown }> = []; const api = { registerTool(tool: { name?: string } & Record): void { registeredTools.push(tool); }, registerToolRenderer(resolver: ToolRendererResolver): void { resolvers.push(resolver); }, registerCommand(name: string, command: { handler?: (...args: any[]) => unknown }): void { commands.push({ name, ...command }); }, on(event: string, handler: (...args: any[]) => unknown): void { const existing = handlers.get(event) ?? []; existing.push(handler); handlers.set(event, existing); }, getAllTools(): unknown[] { return options.tools ?? []; }, getCommands(): Array<{ name: string }> { return options.commands ?? []; }, } as unknown as ExtensionAPI; return { api, registeredTools, resolvers, handlers, commands }; } export function getEventHandlers( harness: ExtensionApiHarness, event: string, ): Array<(...args: any[]) => unknown> { return harness.handlers.get(event) ?? []; } export async function runEvent( harness: ExtensionApiHarness, event: string, ...args: unknown[] ): Promise { const results: unknown[] = []; for (const handler of getEventHandlers(harness, event)) { results.push(await handler(...args)); } return results; } /** Resolve renderers exactly as ExtensionRunner does: resolvers in load order, then `next`. */ export function resolveToolRenderers( resolvers: readonly ToolRendererResolver[], toolName: string, next: () => ToolRenderers | undefined = () => undefined, ): ToolRenderers | undefined { const resolve = (index: number): ToolRenderers | undefined => index < resolvers.length ? resolvers[index](toolName, () => resolve(index + 1)) : next(); return resolve(0); } export interface TestRenderTheme { fg(color: string, value: string): string; bold(value: string): string; } export function createTestTheme(): TestRenderTheme { return { fg: (_color: string, value: string): string => value, bold: (value: string): string => value, }; } export interface RenderedComponent { render(width: number): string[]; invalidate?(): void; } export function renderComponentText(component: unknown, width = 120): string { const rendered = component as RenderedComponent | undefined; if (typeof rendered?.render !== "function") { throw new Error("expected a renderable component"); } return rendered .render(width) .map((line) => line.trimEnd()) .join("\n") .trim(); } const ANSI_PATTERN = /\x1b\[[0-9;]*m/g; export function stripAnsi(value: string): string { return value.replace(ANSI_PATTERN, ""); }