import { stripVTControlCharacters } from "node:util"; import { initTheme, type Theme, ToolExecutionComponent, } from "@earendil-works/pi-coding-agent"; import { type TUI, visibleWidth } from "@earendil-works/pi-tui"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { renderUltraCall } from "../call.ts"; import { initialReducerState, reduceProgress } from "../progress.ts"; import { renderUltraResult } from "../ui.ts"; beforeAll(() => initTheme()); const theme = { fg: (_color: string, text: string) => text, bold: (text: string) => text, } as Theme; const drafting = { expanded: false, argsComplete: false, executionStarted: false, isPartial: true, isError: false, }; const request = { spec: { name: "auth-review", description: "Review authentication boundaries.", phases: [ { id: "inspect", kind: "fanout", over: ["tokens", "sessions", "permissions"], step: { summary: "Inspect authentication boundaries", prompt: "FIRST-PROMPT\nCheck {item}.\nReport evidence-backed findings.", model: "small", tools: ["read", "grep", "ls"], }, }, { id: "synthesize", kind: "single", step: { summary: "Consolidate verified findings", prompt: "Combine independent findings. Preserve evidence.", model: "large", thinkingLevel: "high", tools: ["read", "grep"], }, }, ], return: "{synthesize.results}", }, }; function lines(args: unknown, options = drafting, width = 80) { return renderUltraCall(args, theme, options) .render(width) .map(stripVTControlCharacters); } function host(args: unknown = {}) { return new ToolExecutionComponent( "run_workflow", "call-1", args, {}, { renderCall: renderUltraCall, renderResult: renderUltraResult }, { requestRender: vi.fn() } as unknown as TUI, process.cwd(), ); } function screen(component: ToolExecutionComponent) { return component.render(100).map(stripVTControlCharacters).join("\n"); } describe("streaming workflow call", () => { it("renders each partial argument update, without validation or pretending to run agents", () => { const row = host(); expect(screen(row)).toContain("Receiving workflow arguments…"); row.updateArgs({ spec: { name: "auth-" } }); expect(screen(row)).toContain("run_workflow auth-"); expect(screen(row)).toContain("0 phases received"); row.updateArgs({ spec: { name: "auth-review", phases: [{ id: "inspect" }] }, }); expect(screen(row)).toContain("1 phase received"); expect(screen(row)).toContain("kind not received"); expect(screen(row)).toContain("Summary not received"); row.updateArgs(request); const text = screen(row); expect(text).toContain("2 phases received · no agents running"); expect(text).toContain("inspect · fanout · 3 items received · small"); expect(text).toContain("Consolidate verified findings"); expect(text).not.toContain("Prompt tail · synthesize"); expect(text).not.toContain( "Combine independent findings. Preserve evidence.", ); expect(text).not.toContain("0/0 done"); expect(text).not.toContain("FIRST-PROMPT"); }); it("keeps arriving prompt chunks expansion-only and never schedules animation", () => { const interval = vi.spyOn(globalThis, "setInterval"); const timeout = vi.spyOn(globalThis, "setTimeout"); try { const args = { spec: { phases: [{ step: { prompt: "Inspect" } }] } }; expect(lines(args).join("\n")).not.toContain("Inspect"); expect(lines(args, { ...drafting, expanded: true }).join("\n")).toContain( "Inspect", ); args.spec.phases[0].step.prompt += " authentication"; const component = renderUltraCall(args, theme, { ...drafting, expanded: true, }); expect(component.render(80).join("\n")).toContain( "Inspect authentication", ); component.invalidate(); expect(component.render(80)).toEqual(component.render(80)); expect(interval).not.toHaveBeenCalled(); expect(timeout).not.toHaveBeenCalled(); } finally { interval.mockRestore(); timeout.mockRestore(); } }); it("distinguishes complete arguments from validated or running work", () => { const row = host(request); row.setArgsComplete(); const text = screen(row); expect(text).toContain("Arguments received · 2 phases received"); expect(text).toContain("Awaiting validation and execution."); expect(text).not.toContain("▎"); expect(text).not.toContain("Drafting"); }); it("hands off to the real result board and keeps received prompts available on expansion", () => { const row = host(request); row.setArgsComplete(); row.markExecutionStarted(); expect(screen(row)).not.toContain("Prompt tail"); expect(screen(row)).not.toContain("Consolidate verified findings"); const details = reduceProgress( initialReducerState({ workflowName: "auth-review" }), { kind: "start", phase: "inspect", agentId: "inspect#0", }, ); row.updateResult({ content: [], details, isError: false }, true); expect(screen(row)).toContain("1 active"); expect(screen(row)).not.toContain("Drafting workflow"); row.setExpanded(true); const expanded = screen(row); expect(expanded).toContain("Received workflow"); expect(expanded).toContain("FIRST-PROMPT"); expect(expanded).toContain("Check {item}."); expect(expanded).toContain("tools: read, grep, ls"); expect(expanded).toContain("thinking: high"); expect(expanded).toContain("return: {synthesize.results}"); row.updateResult({ content: [], details, isError: false }); expect(screen(row)).toContain("FIRST-PROMPT"); row.setExpanded(false); expect(screen(row)).not.toContain("FIRST-PROMPT"); expect(screen(row)).not.toContain("Combine independent findings"); }); it.each([true, false])( "clears drafting on failure, including pre-execution validation (started: %s)", (started) => { const row = host(request); if (started) row.markExecutionStarted(); row.updateResult({ content: [ { type: "text", text: "Invalid workflow spec: missing summary" }, ], isError: true, }); const text = screen(row); expect(text).toContain( "ultra · failed: Invalid workflow spec: missing summary", ); expect(text).not.toContain("Drafting workflow"); expect(text).not.toContain("Prompt tail"); expect(text).not.toContain("0/0 done"); row.setExpanded(true); expect(screen(row)).toContain("FIRST-PROMPT"); expect(screen(row)).toContain("Invalid workflow spec: missing summary"); }, ); it("restores a completed call without reviving the drafting preview", () => { const row = host(request); row.updateResult({ content: [], details: initialReducerState(), isError: false, }); expect(screen(row)).not.toContain("Drafting workflow"); expect(screen(row)).not.toContain("Prompt tail"); row.setExpanded(true); expect(screen(row)).toContain("Received workflow"); expect(screen(row)).toContain("FIRST-PROMPT"); }); it("bounds collapsed phases and keeps prompt bodies expansion-only", () => { const phases = Array.from({ length: 20 }, (_, index) => ({ id: `phase_${index}`, kind: "single", step: { summary: `Summary ${index}`, prompt: `OLD-PROMPT-${index}\n${"x".repeat(100_000)}\nLATEST-TAIL`, }, })); const output = lines({ spec: { name: "large", phases } }, drafting, 240); expect(output.length).toBeLessThanOrEqual(13); const text = output.join("\n"); expect(text).toContain("20 phases received"); expect(text).toContain("17 earlier phases"); expect(text).not.toContain("Summary 16"); expect(text).toContain("Summary 17"); expect(text).toContain("Summary 19"); expect(text).not.toContain("LATEST-TAIL"); expect(text).not.toContain("OLD-PROMPT"); expect(text).toContain("to expand"); const expanded = lines( { spec: { phases } }, { ...drafting, expanded: true }, 240, ).join("\n"); expect(expanded).toContain("Summary 0"); expect(expanded).toContain("OLD-PROMPT-0"); expect(expanded).toContain("OLD-PROMPT-19"); expect(expanded).toContain("LATEST-TAIL"); }); it("labels dynamic and conditional fanouts without evaluating selectors", () => { const args = { spec: { phases: [ { id: "verify", kind: "fanout", over: "{inspect.results}", when: "{inspect.results}", step: { summary: "Verify {item}" }, }, ], }, }; const text = lines(args).join("\n"); expect(text).toContain("dynamic items · conditional"); expect(text).toContain("Verify {item}"); expect(text).not.toContain("0 items"); const expanded = lines(args, { ...drafting, expanded: true }).join("\n"); expect(expanded).toContain("over: {inspect.results}"); expect(expanded).toContain("when: {inspect.results}"); }); it("renders saved names and input counts without loading a workflow or exposing input bodies", () => { const text = lines({ name: "does-not-exist", args: { question: "PRIVATE-INPUT", source: "repo" }, }).join("\n"); expect(text).toContain("run_workflow does-not-exist"); expect(text).toContain("Saved workflow · 2 inputs received"); expect(text).not.toContain("PRIVATE-INPUT"); expect(text).not.toContain("unknown workflow"); expect(lines({ name: "ignored", ...request }).join("\n")).not.toContain( "ignored", ); }); it.each([ undefined, null, [], "broken", 42, {}, { spec: null }, { spec: [] }, { spec: { name: {}, phases: "not-an-array" } }, { spec: { phases: [ null, 1, "partial", [], {}, { id: [], kind: {}, step: null }, { step: { summary: {}, prompt: [], tools: [null, {}] } }, ], }, }, ])( "tolerates incomplete and malformed args without renderer fallback: %j", (args) => { for (const expanded of [true, false]) { expect(() => lines(args, { ...drafting, expanded })).not.toThrow(); expect(lines(args, { ...drafting, expanded })[0]).toContain( "run_workflow", ); } }, ); it("stays within narrow terminal widths and sanitizes control bytes from all fields", () => { const args = { spec: { name: "name\n\x1b[31mred\x1b[0m", phases: [ { id: "phase\nnext", kind: "single", step: { summary: "wide 界界 and emoji 🔍🔍\nsummary", model: "small\r\x00", tools: ["read\x07"], prompt: "line\twith\x1b[2Jcontrols\nUnicode 界界🔍🔍\x07", }, }, ], }, }; for (const width of [1, 2, 8, 24, 80]) { for (const expanded of [true, false]) { const output = lines(args, { ...drafting, expanded }, width); expect(output.every((text) => visibleWidth(text) <= width)).toBe(true); expect(output.join("\n")).not.toMatch(/[\x00\x07\r\t]/); } } expect(lines(args)[0]).toBe("run_workflow name red"); }); });