Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/model-info/__tests__/harness.test.ts

Raw
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createTestSession, type TestSession } from "../../../test/harness";
import modelInfoExtension, { __test } from "../index.ts";
import { shortModelLabel } from "../model-label.ts";

describe("model-info pi-test-harness", () => {
	let t: TestSession | undefined;

	afterEach(() => {
		t?.dispose();
		t = undefined;
	});

	it("loads in real Pi extension runtime", async () => {
		t = await createTestSession({ extensionFactories: [modelInfoExtension] });

		expect(t.session.extensionRunner.extensions).toHaveLength(1);
	});

	it("writes enabled lifecycle debug records in an isolated sandbox", async () => {
		const root = mkdtempSync(join(tmpdir(), "pi-ext-model-info-debug-"));
		const original = {
			debug: process.env.PI_MODEL_INFO_DEBUG,
			state: process.env.XDG_STATE_HOME,
		};
		try {
			process.env.PI_MODEL_INFO_DEBUG = "1";
			process.env.XDG_STATE_HOME = join(root, "state");
			vi.resetModules();
			const { default: extension } = await import("../index.ts");
			t = await createTestSession({
				cwd: root,
				env: {
					PI_MODEL_INFO_DEBUG: "1",
					XDG_STATE_HOME: join(root, "state"),
				},
				extensionFactories: [extension],
			});
			t.session.extensionRunner.setUIContext(
				t.session.extensionRunner.getUIContext(),
				"tui",
			);
			await t.session.extensionRunner.emit({
				type: "session_start",
				reason: "startup",
			});
			await t.session.extensionRunner.emit({
				type: "session_shutdown",
				reason: "quit",
			});
			const events = readdirSync(
				join(root, "state", "pi-ext", "debug", "model-info"),
			).flatMap((file) =>
				readFileSync(
					join(root, "state", "pi-ext", "debug", "model-info", file),
					"utf8",
				)
					.trim()
					.split("\n")
					.map((line) => JSON.parse(line)),
			);
			expect(events).toEqual(
				expect.arrayContaining([
					expect.objectContaining({
						extension: "model-info",
						event: "session.start",
					}),
					expect.objectContaining({
						extension: "model-info",
						event: "session.shutdown",
					}),
					expect.objectContaining({
						event: "route.refresh",
						outcome: "fallback",
					}),
				]),
			);
		} finally {
			t?.dispose();
			t = undefined;
			if (original.debug === undefined) delete process.env.PI_MODEL_INFO_DEBUG;
			else process.env.PI_MODEL_INFO_DEBUG = original.debug;
			if (original.state === undefined) delete process.env.XDG_STATE_HOME;
			else process.env.XDG_STATE_HOME = original.state;
			rmSync(root, { recursive: true, force: true });
		}
	});

	it("uses acknowledged footer segments without transient fallback status", async () => {
		let accepted = 0;
		const footer = (pi: ExtensionAPI) => {
			pi.events.on("footer:segment", (data: unknown) => {
				const accept = (data as { accept?: unknown })?.accept;
				if (typeof accept !== "function") return;
				accept();
				accepted++;
			});
		};
		t = await createTestSession({
			extensionFactories: [footer, modelInfoExtension],
		});
		const runner = t.session.extensionRunner;
		runner.setUIContext(runner.getUIContext(), "tui");
		await runner.emit({ type: "session_start", reason: "startup" });

		const statuses = t.events
			.uiCallsFor("setStatus")
			.filter((call) => call.args[0] === "model-info");
		expect(accepted).toBeGreaterThan(0);
		expect(statuses.at(-1)?.args[1]).toBeUndefined();
	});

	it("keeps fallback status when only removals are acknowledged", async () => {
		let removals = 0;
		const partialFooter = (pi: ExtensionAPI) => {
			pi.events.on("footer:segment", (data: unknown) => {
				const segment = data as { text?: unknown; accept?: unknown };
				if (segment.text !== undefined || typeof segment.accept !== "function")
					return;
				segment.accept();
				removals++;
			});
		};
		t = await createTestSession({
			extensionFactories: [partialFooter, modelInfoExtension],
		});
		const runner = t.session.extensionRunner;
		runner.setUIContext(runner.getUIContext(), "tui");
		await runner.emit({ type: "session_start", reason: "startup" });

		const statuses = t.events
			.uiCallsFor("setStatus")
			.filter((call) => call.args[0] === "model-info");
		expect(removals).toBeGreaterThan(0);
		expect(statuses.at(-1)?.args[1]).toMatch(/:/);
	});

	it("replaces producer fallbacks with the combined route", async () => {
		const producer = (pi: ExtensionAPI) => {
			pi.on("session_start", (_event, ctx) => {
				ctx.ui.setStatus("fast", "fast fallback");
				pi.events.emit("model-route:fast", { active: true });
				ctx.ui.setStatus("angel", "angel fallback");
				pi.events.emit("model-route:angel", {
					advisor: { modelId: "gpt-6-astra", thinkingLevel: "max" },
				});
			});
		};
		t = await createTestSession({
			extensionFactories: [producer, modelInfoExtension],
		});

		const statuses = t.events.uiCallsFor("setStatus");
		const latest = (id: string) =>
			statuses.filter((call) => call.args[0] === id).at(-1)?.args[1];
		expect(latest("fast")).toBeUndefined();
		expect(latest("angel")).toBeUndefined();
		expect(latest("model-info")).toMatch(/→ 󰧑 astra:max$/u);
	});

	it("shows the model a router resolved to after a reply, until the model changes", async () => {
		t = await createTestSession({ extensionFactories: [modelInfoExtension] });
		const runner = t.session.extensionRunner;
		const model = runner.createContext().model;
		const latest = () =>
			t?.events
				.uiCallsFor("setStatus")
				.filter((call) => call.args[0] === "model-info")
				.at(-1)?.args[1];

		await runner.emit({
			type: "message_end",
			message: {
				role: "assistant",
				provider: model.provider,
				model: model.id,
				responseModel: "kilo/dots-studio/dots-3-note-preview:free",
			},
		});
		expect(latest()).toMatch(/ → dots-3-note-preview:free$/);

		await runner.emit({
			type: "model_select",
			model,
			previousModel: model,
			source: "set",
		});
		expect(latest()).not.toMatch(/→/);
	});

	it("reports a served model only when it differs from the selected one", () => {
		const ctx = { model: { provider: "grts", id: "auto" } } as never;
		const reply = (extra: object) => ({
			role: "assistant",
			provider: "grts",
			model: "auto",
			...extra,
		});

		expect(
			__test.servedModel(reply({ responseModel: "kilo/x:free" }), ctx),
		).toBe("kilo/x:free");
		expect(__test.servedModel(reply({}), ctx)).toBeUndefined();
		expect(
			__test.servedModel(reply({ responseModel: "auto" }), ctx),
		).toBeUndefined();
		expect(
			__test.servedModel(
				reply({ provider: "openrouter", responseModel: "a/b" }),
				ctx,
			),
		).toBeUndefined();
		expect(
			__test.servedModel({ role: "user", responseModel: "a/b" }, ctx),
		).toBeUndefined();
	});

	it("restores the served model from the last assistant reply in the branch", () => {
		const ctx = (branch: object[]) =>
			({
				model: { provider: "grts", id: "auto" },
				sessionManager: { getBranch: () => branch },
			}) as never;
		const assistant = (responseModel?: string) => ({
			type: "message",
			message: {
				role: "assistant",
				provider: "grts",
				model: "auto",
				responseModel,
			},
		});

		expect(
			__test.servedFromBranch(
				ctx([
					assistant("kilo/old:free"),
					assistant("google/gemini-3.8-flash"),
					{ type: "message", message: { role: "user" } },
				]),
			),
		).toBe("google/gemini-3.8-flash");
		expect(__test.servedFromBranch(ctx([assistant()]))).toBeUndefined();
	});

	it("formats model and thinking", () => {
		const ctx = {
			cwd: "/tmp",
			model: { provider: "test", id: "openai-codex/gpt-5.6-sol" },
			sessionManager: {
				getBranch: () => [
					{ type: "thinking_level_change", thinkingLevel: "high" },
				],
			},
		} as any;

		expect(__test.modelInfoLabel(ctx)).toBe("sol:high");
		expect(__test.providerLabel(ctx)).toBe("test");
		expect(__test.providerIcon(ctx)).toBeUndefined();
	});

	it.each([
		["openai-codex", ""],
		["anthropic", ""],
		["klaus", ""],
		["github-copilot", ""],
		["google-gemini-cli", ""],
		["hetzner", "󰰀"],
		["grts", "󰙺"],
	])("uses the verified brand icon for %s", (provider, icon) => {
		expect(__test.providerIcon({ model: { provider } } as any)).toBe(icon);
	});

	it("detects declared OAuth subscriptions and Kimi Coding", () => {
		const modelRegistry = {
			isUsingOAuth: () => true,
			getProvider: () => ({ auth: { oauth: { isSubscription: true } } }),
		};
		expect(
			__test.usesSubscription({
				model: { provider: "anthropic" },
				modelRegistry,
			} as any),
		).toBe(true);
		expect(
			__test.usesSubscription({
				model: { provider: "kimi-coding" },
				modelRegistry: {},
			} as any),
		).toBe(true);
	});

	it("combines executor, transport, and advisor into one route", () => {
		expect(
			__test.statusLabel("sol:medium", true, {
				modelId: "gpt-6-astra",
				thinkingLevel: "max",
			}),
		).toBe("sol:medium 󱐋 → 󰧑 astra:max");
	});

	it.each([
		["gpt-6-astra", "astra"],
		["gpt-5.5", "gpt-5.5"],
		["claude-fable-5-1", "fable"],
		["claude-opus-5", "opus"],
		["gemini-3.1-pro-preview", "3.1-pro"],
		["grok-4.6", "grok-4.6"],
		["deepseek-v4-flash-vision-exp", "v4-flash"],
		["MiniMax-M3", "M3"],
		["kimi-k2.7-code-highspeed", "k2.7-code"],
		["glm-5.3-highspeed", "glm-5.3"],
		["mistral-medium-2604", "mistral-medium"],
		["new-family-7-alpha", "new-family-7-alpha"],
	])("shortens %s by model-family grammar", (modelId, expected) => {
		expect(shortModelLabel(modelId)).toBe(expected);
	});
});