import type { Theme } from "@earendil-works/pi-coding-agent"; import { Effect, Layer } from "effect"; import { describe, expect, it } from "vitest"; import type { ExecuteDetails } from "../schemas/execute.ts"; import type { SearchDetails } from "../schemas/search.ts"; import { type ExecutorSettings, DefaultExecutorSettings } from "../schemas/settings.ts"; import { ConfigService } from "./config.ts"; import { RenderService } from "./render.ts"; const theme = { bold: (value: string) => value, fg: (color: string, value: string) => value, } as Theme; const renderText = (text: { render: (width: number) => string[] }): string => text.render(220).join("\n"); const mockConfigService = (settings: ExecutorSettings) => Layer.succeed(ConfigService)({ resolve: (cwd) => Effect.succeed({ cwd, settings }), saveGlobal: () => Effect.void, saveProject: () => Effect.void, formatSettingsSummary: () => "", formatStatusBar: () => "executor: test", }); const renderLayer = (settings: ExecutorSettings) => RenderService.Default.pipe(Layer.provideMerge(mockConfigService(settings))); const runRender = (effect: Effect.Effect): A => Effect.runSync(effect.pipe(Effect.provide(renderLayer(DefaultExecutorSettings)))); const runRenderWithSettings = ( settings: ExecutorSettings, effect: Effect.Effect, ): A => Effect.runSync(effect.pipe(Effect.provide(renderLayer(settings)))); describe("renderSearchResult", () => { it("hides the sources footer when disabled", () => { const details: SearchDetails = { total: 1, hasMore: false, nextOffset: null, items: [ { path: "tools.github.search", name: "search", description: "Search GitHub", sourceId: "github", score: 1, }, ], }; const text = runRenderWithSettings( { ...DefaultExecutorSettings, search: { ...DefaultExecutorSettings.search, showSourcesFooter: false }, }, Effect.flatMap(RenderService.asEffect(), (render) => render.renderSearchResult("/tmp", details, "", {}, theme), ), ); expect(text).not.toContain("Sources"); }); it("renders compact snippets and a source footer", () => { const details: SearchDetails = { total: 29, hasMore: true, nextOffset: 3, items: [ { path: "posthog.query_error_tracking_issues_list", name: "query_error_tracking_issues_list", sourceId: "posthog", score: 1, description: "List and filter Error tracking issues. Returns compact issue rows with aggregate impact counts and optional volume buckets.\n\nUse this first when the user asks which errors are happening.", }, { path: "sentry.search_issues", name: "search_issues", sourceId: "sentry", score: 0.9, details: { path: "sentry.search_issues", name: "search_issues", description: "Search for grouped issues/problems in Sentry - returns a LIST of issues, NOT counts or aggregations.\n\nProvide query as natural language or Sentry issue search syntax.", inputTypeScript: "type Input = { organizationSlug: string; query: string }", outputTypeScript: "type Output = { issues: Array<{ id: string; title: string }> }", }, }, ], }; const output = renderText( runRender( Effect.flatMap(RenderService.asEffect(), (render) => render.renderSearchResult("/tmp/project", details, "", { expanded: false }, theme), ), ), ); expect(output).toContain("29 result(s)"); expect(output).toContain("Tools"); expect(output).toContain("posthog.query_error_tracking_issues_list"); expect(output).toContain("[posthog]"); expect(output).toContain("sentry.search_issues"); expect(output).toContain("Sources"); expect(output).toContain("posthog, sentry"); expect(output).toContain("More results at offset 3"); expect(output).toContain("to expand descriptions"); expect(output).not.toContain("Use this first when the user asks"); expect(output).not.toContain("type Input ="); }); it("renders full descriptions and type details when expanded", () => { const details: SearchDetails = { total: 1, hasMore: false, nextOffset: null, items: [ { path: "sentry.search_issues", name: "search_issues", sourceId: "sentry", score: 0.9, details: { path: "sentry.search_issues", name: "search_issues", description: "Search for grouped issues/problems in Sentry - returns a LIST of issues, NOT counts or aggregations.\n\nProvide query as natural language or Sentry issue search syntax.", inputTypeScript: "type Input = { organizationSlug: string; query: string }", outputTypeScript: "type Output = { issues: Array<{ id: string; title: string }> }", }, }, ], }; const output = renderText( runRender( Effect.flatMap(RenderService.asEffect(), (render) => render.renderSearchResult("/tmp/project", details, "", { expanded: true }, theme), ), ), ); expect(output).toContain("Provide query as natural language"); expect(output).toContain("Input"); expect(output).toContain("type Input ="); expect(output).toContain("Output"); expect(output).toContain("type Output ="); expect(output).not.toContain("to expand descriptions"); }); }); describe("renderExecuteResult", () => { it("renders code, pretty structured output, and log state without completion chrome", () => { const details: ExecuteDetails = { status: "completed", result: { value: 3, source: "executor-pi-dogfood" }, logs: [], }; const callOutput = renderText( runRender( Effect.flatMap(RenderService.asEffect(), (render) => render.renderExecuteCall( "/tmp/project", { code: "return { value: 1 + 2, source: 'executor-pi-dogfood' };" }, theme, ), ), ), ); const output = renderText( runRender( Effect.flatMap(RenderService.asEffect(), (render) => render.renderExecuteResult("/tmp/project", details, "", { expanded: false }, theme), ), ), ); expect(callOutput).toContain("Code"); expect(output).toContain("Output"); expect(output).toContain('"value": 3'); expect(output).toContain('"source": "executor-pi-dogfood"'); expect(output).toContain("Logs: none"); expect(output).not.toContain("Executor completed"); }); it("applies configured render limits", () => { const settings: ExecutorSettings = { ...DefaultExecutorSettings, displayMode: "custom", render: { maxCodePreviewLines: 1, maxJsonBytes: 20, maxLogLines: 1, }, }; const details: ExecuteDetails = { status: "completed", result: { value: "abcdefghijklmnopqrstuvwxyz" }, logs: ["first\nsecond"], }; const callOutput = renderText( runRenderWithSettings( settings, Effect.flatMap(RenderService.asEffect(), (render) => render.renderExecuteCall("/tmp/project", { code: "const x = 1;\nreturn x;" }, theme), ), ), ); const output = renderText( runRenderWithSettings( settings, Effect.flatMap(RenderService.asEffect(), (render) => render.renderExecuteResult("/tmp/project", details, "", { expanded: true }, theme), ), ), ); expect(callOutput).toContain("... truncated 1 line(s)"); expect(output).toContain("... truncated"); expect(output).toContain("first"); expect(output).toContain("... 1 more log line(s)"); expect(output).not.toContain("second"); }); });