repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/footer/__tests__/contract.test.ts
Rawimport { fileURLToPath } from "node:url";
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
import { visibleWidth } from "@earendil-works/pi-tui";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPiSession,
type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import { __test } from "../index.ts";
import type { ProducerBridge } from "./producer.ts";
const footerPath = fileURLToPath(new URL("../index.ts", import.meta.url));
const producerPath = fileURLToPath(new URL("./producer.ts", import.meta.url));
interface UiCall {
args: unknown[];
}
/**
* Records `setFooter` factories by capturing each `setFooter` call, and stands
* in for the interactive UI so `hasUI` is true once mounted.
*/
function createRecordingUi() {
const setFooterCalls: UiCall[] = [];
const ui = {
setFooter: (factory: unknown) => {
setFooterCalls.push({ args: [factory] });
},
notify: () => {},
};
return { ui: ui as unknown as ExtensionUIContext, setFooterCalls };
}
function setBridge(bridge: ProducerBridge): void {
(globalThis as Record<string, unknown>)["__footerProducerBridge"] = bridge;
}
describe("footer contract", () => {
let session: PiSession | undefined;
afterEach(async () => {
vi.restoreAllMocks();
(globalThis as Record<string, unknown>)["__footerProducerBridge"] =
undefined;
await session?.dispose();
session = undefined;
});
async function mountTui(
active: PiSession,
ui: ExtensionUIContext,
): Promise<void> {
const runner = active.session.extensionRunner;
runner.setUIContext(ui, "tui");
await runner.emit({ type: "session_start", reason: "startup" });
}
it("loads in real Pi extension runtime", async () => {
session = await createPiSession({ extensionPaths: [footerPath] });
expect(session.extensions).toHaveLength(1);
expect(session.extensions[0]!.path).toBe(footerPath);
});
it("acknowledges and retains pre-mount updates without polling", async () => {
let accepted = 0;
let offer: ((id: string) => void) | undefined;
setBridge({
onLoad: (emit) => {
offer = (id) =>
emit({
id,
text: "ready",
zone: "llm",
accept: () => accepted++,
});
offer("early");
},
});
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
session = await createPiSession({
extensionPaths: [footerPath, producerPath],
});
const { ui, setFooterCalls } = createRecordingUi();
expect(accepted).toBe(1);
expect(setFooterCalls).toHaveLength(0);
const runner = session.session.extensionRunner;
runner.setUIContext(ui, "rpc");
await runner.emit({ type: "session_start", reason: "startup" });
expect(setFooterCalls).toHaveLength(0);
offer?.("rpc");
expect(accepted).toBe(1);
await mountTui(session, ui);
expect(accepted).toBe(1);
expect(setTimeoutSpy.mock.calls.some(([, delay]) => delay === 50)).toBe(
false,
);
setTimeoutSpy.mockRestore();
const factory = setFooterCalls.at(-1)?.args[0] as
| ((
tui: { requestRender(): void },
theme: { fg(style: string, text: string): string },
data: {
onBranchChange(callback: () => void): () => void;
getExtensionStatuses(): ReadonlyMap<string, string>;
},
) => { 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;
setBridge({
onLoad: (emit) => {
emit({
id: "parent",
text: "ready",
zone: "llm",
accept: () => parentAccepted++,
});
},
});
session = await createPiSession({
extensionPaths: [footerPath, producerPath],
});
const { ui } = createRecordingUi();
await mountTui(session, ui);
expect(parentAccepted).toBe(1);
const nested = await createPiSession({ extensionPaths: [footerPath] });
try {
const runner = nested.session.extensionRunner;
const { ui: nestedUi } = createRecordingUi();
runner.setUIContext(nestedUi, "rpc");
await runner.emit({ type: "session_start", reason: "startup" });
await runner.emit({ type: "session_shutdown", reason: "quit" });
expect(parentAccepted).toBe(1);
} finally {
await nested.dispose();
}
});
it("keeps valid segments when a producer emits malformed data", async () => {
const bridge: ProducerBridge = {};
setBridge(bridge);
session = await createPiSession({
extensionPaths: [footerPath, producerPath],
});
const emitSegment = bridge.emit;
const { ui, setFooterCalls } = createRecordingUi();
await mountTui(session, ui);
const factory = setFooterCalls.at(-1)?.args[0] as
| ((
tui: { requestRender(): void },
theme: { fg(style: string, text: string): string },
data: {
onBranchChange(callback: () => void): () => void;
getExtensionStatuses(): ReadonlyMap<string, string>;
},
) => { 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 () => {
const bridge: ProducerBridge = {};
setBridge(bridge);
session = await createPiSession({
extensionPaths: [footerPath, producerPath],
});
const emitSegment = bridge.emit;
const { ui, setFooterCalls } = createRecordingUi();
await mountTui(session, ui);
const factory = setFooterCalls.at(-1)?.args[0] as
| ((
tui: { requestRender(): void },
theme: { fg(style: string, text: string): string },
data: {
onBranchChange(callback: () => void): () => void;
getExtensionStatuses(): ReadonlyMap<string, string>;
},
) => {
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<string, typeof tps | typeof quota>([
[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 () => {
const bridge: ProducerBridge = {};
setBridge(bridge);
session = await createPiSession({
extensionPaths: [footerPath, producerPath],
});
const emitSegment = bridge.emit;
const { ui, setFooterCalls } = createRecordingUi();
await mountTui(session, ui);
const runner = session.session.extensionRunner;
await session.extensions[0]?.commands
.get("footer")
?.handler("", runner.createCommandContext());
const factory = setFooterCalls.at(-1)?.args[0] as
| ((
tui: { requestRender(force?: boolean): void },
theme: { fg(style: string, text: string): string },
data: {
getGitBranch(): string | null;
onBranchChange(callback: () => void): () => void;
getExtensionStatuses(): ReadonlyMap<string, string>;
},
) => {
render(width: number): string[];
})
| undefined;
expect(
factory?.(
{ requestRender() {} },
{ fg: (_style, text) => text },
{
getGitBranch: () => null,
onBranchChange: () => () => {},
getExtensionStatuses: () => new Map(),
},
).render(80),
).toEqual([]);
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
emitSegment?.({ id: "tps", text: "50/s", zone: "llm" });
expect(setTimeoutSpy).not.toHaveBeenCalled();
setTimeoutSpy.mockRestore();
});
it("re-publishes Pi branch changes as footer:branch-change", async () => {
const bridge: ProducerBridge = {};
setBridge(bridge);
session = await createPiSession({
extensionPaths: [footerPath, producerPath],
});
const received: unknown[] = [];
bridge.listen?.("footer:branch-change", (data) => received.push(data));
const { ui, setFooterCalls } = createRecordingUi();
await mountTui(session, ui);
type Factory = (
tui: { requestRender(force?: boolean): void },
theme: { fg(style: string, text: string): string },
data: {
getGitBranch(): string | null;
onBranchChange(callback: () => void): () => void;
getExtensionStatuses(): ReadonlyMap<string, string>;
},
) => { dispose(): void };
let branch: string | null = "main";
let unsubscribed = 0;
const mountWithBranchData = () => {
let notify: (() => void) | undefined;
const factory = setFooterCalls.at(-1)?.args[0] as Factory | undefined;
const component = factory?.(
{ requestRender() {} },
{ fg: (_style, text) => text },
{
getGitBranch: () => branch,
onBranchChange: (callback) => {
notify = callback;
return () => unsubscribed++;
},
getExtensionStatuses: () => new Map(),
},
);
return { component, notify: () => notify?.() };
};
const visible = mountWithBranchData();
visible.notify();
expect(received).toEqual([{ branch: "main" }]);
branch = null;
visible.notify();
expect(received).toEqual([{ branch: "main" }, { branch: null }]);
visible.component?.dispose();
expect(unsubscribed).toBe(1);
// Hidden footer still forwards branch changes.
const runner = session.session.extensionRunner;
await session.extensions[0]?.commands
.get("footer")
?.handler("", runner.createCommandContext());
branch = "feature";
const hiddenFooter = mountWithBranchData();
hiddenFooter.notify();
expect(received.at(-1)).toEqual({ branch: "feature" });
hiddenFooter.component?.dispose();
expect(unsubscribed).toBe(2);
});
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]]);
});
});