import { describe, expect, test } from "bun:test"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { COUNCIL_PROGRESS_DETAILS_TYPE, type CouncilRunDetails, } from "./progress.ts"; import { renderConveneCouncilCall, renderConveneCouncilResult, } from "./rendering.ts"; const THEME = { bold: (value: string) => value, fg: (_name: string, value: string) => value, }; /** Renders one council result through the public test boundary at a stable width. */ function renderCouncilResult( result: AgentToolResult, expanded: boolean, ): string { return renderConveneCouncilResult( result, { expanded }, THEME as never, { isError: false } as never, ) .render(200) .join("\n"); } describe("convene-council collapsed text normalization", () => { test("normalizes only the collapsed question", () => { // Purpose: compact council questions must remove layout whitespace while expansion preserves the submitted question. // Input and expected output: a multiline question becomes one spaced preview and remains multiline when expanded. // Edge case: duplicate spaces after a tab remain observable only in the expanded view. // Dependencies: the public call renderer selects presentation from the expansion flag. const question = "first\n\tsecond third"; const collapsed = renderConveneCouncilCall( { question }, THEME as never, { expanded: false } as never, ).render(200); const expanded = renderConveneCouncilCall( { question }, THEME as never, { expanded: true } as never, ).render(200); expect(collapsed).toEqual(["convene_council: first second third"]); expect(expanded).toEqual(["convene_council: first", "\tsecond third"]); }); test("normalizes JSON escapes only in collapsed answers", () => { // Purpose: council previews must use the package-wide JSON-aware normalization contract. // Input and expected output: escaped formatting collapses in preview while expanded answers keep the original JSON text. // Edge case: a literal escaped newline used as data remains unchanged. // Dependencies: the public result renderer owns collapsed Text and expanded Markdown paths. const text = String.raw`{"body":"first\n\tsecond third","regex":"\\n+"}`; const result: AgentToolResult = { content: [{ type: "text", text }], details: undefined, }; const collapsed = renderCouncilResult(result, false); const expanded = renderCouncilResult(result, true); expect(collapsed).toContain('"body":"first second third"'); expect(collapsed).toContain(String.raw`"regex":"\\n+"`); expect(collapsed).not.toContain(String.raw`first\n\tsecond`); expect(expanded).toContain(String.raw`first\n\tsecond third`); }); test("combines participant projection savings with context usage", () => { // Purpose: council participant rows must preserve the shared positive projection status beside current context usage. // Inputs and expected output: 65k savings and 120k/372k usage render as one ~65k/120k/372k value. // Edge case: omitting projection savings leaves the existing current/window value without a leading separator. // Dependencies: serialized council progress details and the public collapsed result renderer. // ARRANGE: build equivalent participant details with and without positive projection savings. const participant = { label: "A", displayName: "Socrates", participantId: "llm1", modelId: "test-model", thinking: "medium", display: "test-model/medium", contextWindow: 372_000, status: "running", elapsedMs: 1_000, contextUsage: { tokens: 120_000, contextWindow: 372_000, percent: 32, }, } as const; const details: CouncilRunDetails = { type: COUNCIL_PROGRESS_DETAILS_TYPE, runId: "run-1", question: "Compare", status: "running", phase: "Collecting opinions", elapsedMs: 1_000, iteration: 1, iterationLimit: 1, participants: [{ ...participant, contextProjectionStatus: "~65k" }], events: [], omittedEventCount: 0, }; const projectedResult: AgentToolResult = { content: [{ type: "text", text: "" }], details, }; const plainResult: AgentToolResult = { content: [{ type: "text", text: "" }], details: { ...details, participants: [participant] }, }; // ACT: render both participant states through the public collapsed result path. const projected = renderCouncilResult(projectedResult, false); const plain = renderCouncilResult(plainResult, false); // ASSERT: only the positive status adds the savings prefix. expect(projected).toContain("~65k/120k/372k"); expect(plain).toContain("120k/372k"); expect(plain).not.toContain("~65k/"); }); });