import path from "node:path"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { visibleWidth } from "@earendil-works/pi-tui"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createTestSession, type TestSession } from "../../../test/harness"; import footerExtension, { __test } from "../index.ts"; describe("footer pi-test-harness", () => { let t: TestSession | undefined; afterEach(() => { vi.restoreAllMocks(); t?.dispose(); t = undefined; }); async function mountTui(session: TestSession): Promise { const runner = session.session.extensionRunner; runner.setUIContext(runner.getUIContext(), "tui"); await runner.emit({ type: "session_start", reason: "startup" }); } it("loads in real Pi extension runtime", async () => { const extensionPath = path.resolve(import.meta.dirname, "../index.ts"); t = await createTestSession({ extensions: [extensionPath] }); expect(t.session.extensionRunner.extensions).toHaveLength(1); expect(t.session.extensionRunner.extensions[0].path).toBe(extensionPath); }); it("acknowledges and retains pre-mount updates without polling", async () => { let accepted = 0; let offer: ((id: string) => void) | undefined; const producer = (pi: ExtensionAPI) => { offer = (id) => pi.events.emit("footer:segment", { id, text: "ready", zone: "llm", accept: () => accepted++, }); offer("early"); }; const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); t = await createTestSession({ extensionFactories: [footerExtension, producer], }); expect(accepted).toBe(1); expect(t.events.uiCallsFor("setFooter")).toHaveLength(0); const runner = t.session.extensionRunner; runner.setUIContext(runner.getUIContext(), "rpc"); await runner.emit({ type: "session_start", reason: "startup" }); expect(t.events.uiCallsFor("setFooter")).toHaveLength(0); offer?.("rpc"); expect(accepted).toBe(1); await mountTui(t); expect(accepted).toBe(1); expect(setTimeoutSpy.mock.calls.some(([, delay]) => delay === 50)).toBe( false, ); setTimeoutSpy.mockRestore(); const factory = t.events.uiCallsFor("setFooter").at(-1)?.args[0] as | (( tui: { requestRender(): void }, theme: { fg(style: string, text: string): string }, data: { onBranchChange(callback: () => void): () => void; getExtensionStatuses(): ReadonlyMap; }, ) => { render(width: number): string[] }) | undefined; const component = factory?.( { requestRender() {} }, { fg: (_style, text) => text }, { onBranchChange: () => () => {}, getExtensionStatuses: () => new Map(), }, ); expect(component?.render(80).join("\n")).toContain("ready"); }); it("isolates acknowledgements from a nested non-TUI runtime", async () => { let parentAccepted = 0; const producer = (pi: ExtensionAPI) => { pi.events.emit("footer:segment", { id: "parent", text: "ready", zone: "llm", accept: () => parentAccepted++, }); }; t = await createTestSession({ extensionFactories: [footerExtension, producer], }); await mountTui(t); expect(parentAccepted).toBe(1); const nested = await createTestSession({ extensionFactories: [footerExtension], }); try { const runner = nested.session.extensionRunner; runner.setUIContext(runner.getUIContext(), "rpc"); await runner.emit({ type: "session_start", reason: "startup" }); await runner.emit({ type: "session_shutdown", reason: "quit" }); expect(parentAccepted).toBe(1); } finally { nested.dispose(); } }); it("keeps valid segments when a producer emits malformed data", async () => { let emitSegment: ((segment: unknown) => void) | undefined; const producer = (pi: ExtensionAPI) => { emitSegment = (segment) => pi.events.emit("footer:segment", segment); }; t = await createTestSession({ extensionFactories: [footerExtension, producer], }); await mountTui(t); const factory = t.events.uiCallsFor("setFooter").at(-1)?.args[0] as | (( tui: { requestRender(): void }, theme: { fg(style: string, text: string): string }, data: { onBranchChange(callback: () => void): () => void; getExtensionStatuses(): ReadonlyMap; }, ) => { render(width: number): string[] }) | undefined; const component = factory?.( { requestRender() {} }, { fg: (style, text) => { if (style === "not-a-theme-color") throw new Error("bad color"); return text; }, }, { onBranchChange: () => () => {}, getExtensionStatuses: () => new Map(), }, ); let accepted = 0; emitSegment?.({ id: "good", text: "healthy", zone: "llm", accept: () => accepted++, }); emitSegment?.({ id: "good", text: 42, zone: "llm", accept: () => accepted++, }); expect(accepted).toBe(1); emitSegment?.({ id: "good", text: "poison", zone: "llm", color: "not-a-theme-color", accept: () => accepted++, }); expect(accepted).toBe(1); expect(component?.render(80).join("\n")).toContain("healthy"); expect(component?.render(80).join("\n")).not.toContain("poison"); }); it("keeps later LLM components visible as earlier values grow", async () => { let emitSegment: ((segment: unknown) => void) | undefined; const producer = (pi: ExtensionAPI) => { emitSegment = (segment) => pi.events.emit("footer:segment", segment); }; t = await createTestSession({ extensionFactories: [footerExtension, producer], }); await mountTui(t); const factory = t.events.uiCallsFor("setFooter").at(-1)?.args[0] as | (( tui: { requestRender(): void }, theme: { fg(style: string, text: string): string }, data: { onBranchChange(callback: () => void): () => void; getExtensionStatuses(): ReadonlyMap; }, ) => { render(width: number): string[]; dispose(): void; }) | undefined; expect(factory).toBeTypeOf("function"); const component = factory?.( { requestRender() {} }, { fg: (_style, text) => text }, { onBranchChange: () => () => {}, getExtensionStatuses: () => new Map(), }, ); expect(component).toBeDefined(); emitSegment?.({ id: "model", text: "sol:medium", zone: "llm", order: 0 }); emitSegment?.({ id: "tokens", text: "↑0", zone: "llm", order: 2 }); emitSegment?.({ id: "tps", text: "50.0/s", zone: "llm", order: 4 }); const initial = component?.render(32) ?? []; expect(initial).toHaveLength(2); expect(initial.join("\n")).toContain("50.0/s"); emitSegment?.({ id: "tokens", text: "↑12k ↓3.4k", zone: "llm", order: 2, }); const grown = component?.render(32) ?? []; expect(grown).toHaveLength(3); expect(grown.join("\n")).toContain("50.0/s"); expect(grown.every((line) => visibleWidth(line) <= 32)).toBe(true); emitSegment?.({ id: "tokens", text: "↑0", zone: "llm", order: 2 }); const shrunk = component?.render(32) ?? []; expect(shrunk).toHaveLength(2); expect(shrunk.join("\n")).toContain("50.0/s"); emitSegment?.({ id: "provider", text: "provider", zone: "llm", group: "model-route", order: -0.5, }); emitSegment?.({ id: "model", text: "sol:medium", zone: "llm", group: "model-route", gap: 2, order: 0, }); expect(component?.render(80).join("\n")).toContain("provider sol:medium"); component?.dispose(); }); it.each([ [true, "tui", true], [true, "rpc", false], [false, "json", false], [false, "print", false], [false, "tui", false], ] as const)( "requires UI and TUI mode when hasUI=%s and mode=%s", (hasUI, mode, expected) => { expect(__test.canRenderFooter({ hasUI, mode })).toBe(expected); }, ); it("keeps equal-priority order stable after removal and reinsertion", () => { const quota = { id: "quota:anthropic", text: "quota", zone: "llm", order: 4, } as const; const tps = { id: "tps", text: "tps", zone: "llm", order: 4, } as const; const segments = new Map([ [tps.id, tps], [quota.id, quota], ]); segments.delete(tps.id); segments.set(tps.id, tps); expect([...segments.values()].sort(__test.compareSegments)).toEqual([ quota, tps, ]); }); it("renders nothing and schedules no retries when hidden", async () => { let emitSegment: ((segment: unknown) => void) | undefined; const producer = (pi: ExtensionAPI) => { emitSegment = (segment) => pi.events.emit("footer:segment", segment); }; t = await createTestSession({ extensionFactories: [footerExtension, producer], }); await mountTui(t); const runner = t.session.extensionRunner; await runner.extensions[0].commands .get("footer") .handler("", runner.createContext()); const factory = t.events.uiCallsFor("setFooter").at(-1)?.args[0] as | ((tui: { requestRender(force?: boolean): void }) => { render(width: number): string[]; }) | undefined; expect(factory?.({ requestRender() {} }).render(80)).toEqual([]); const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); emitSegment?.({ id: "tps", text: "50/s", zone: "llm" }); expect(setTimeoutSpy).not.toHaveBeenCalled(); setTimeoutSpy.mockRestore(); }); it("right-aligns statuses beside the workspace row", () => { expect(__test.alignLeftAndRight("workspace", "busy", 16)).toBe( "workspace busy", ); }); it("preserves the workspace row when statuses exceed the remaining space", () => { const line = __test.alignLeftAndRight("workspace", "very long status", 12); expect(visibleWidth(line)).toBe(12); expect(line.startsWith("workspace ")).toBe(true); }); it("uses the first row when its first group is oversized", () => { const [first, second] = __test.flowLines( ["oversized-model-route", "tokens"], " | ", 8, ); expect(first).not.toBe(""); expect(second).toBe("tokens"); }); it("keeps adjacent members of a named segment group atomic", () => { const model = { id: "model", text: "sol:medium", zone: "llm" } as const; const fast = { id: "fast", text: "", zone: "llm", group: "model-route", } as const; const angel = { id: "angel", text: "astra:max", zone: "llm", group: "model-route", } as const; const tokens = { id: "tokens", text: "1k", zone: "llm" } as const; expect( __test.groupSegments([ { ...model, group: "model-route" }, fast, angel, tokens, ]), ).toEqual([[{ ...model, group: "model-route" }, fast, angel], [tokens]]); }); });