Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

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

Raw
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { MessageEndEvent } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, type MockInstance, vi } from "vitest";
import {
	createPiSession,
	type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
import { __test } from "../index.ts";
import { shortModelLabel } from "../model-label.ts";
import type { ProbeHooks } from "./probe.ts";

function extensionPath(): string {
	return fileURLToPath(new URL("../index.ts", import.meta.url));
}

function probePath(): string {
	return fileURLToPath(new URL("./probe.ts", import.meta.url));
}

function setProbe(hooks: ProbeHooks): void {
	(globalThis as { __modelInfoProbe?: ProbeHooks }).__modelInfoProbe = hooks;
}

function clearProbe(): void {
	delete (globalThis as { __modelInfoProbe?: ProbeHooks }).__modelInfoProbe;
}

function captureStatus(
	session: PiSession,
	mode: "tui" | "print",
): MockInstance {
	const runner = session.session.extensionRunner;
	const ui = { ...runner.getUIContext() };
	const spy = vi.spyOn(ui, "setStatus");
	runner.setUIContext(ui, mode);
	return spy;
}

function latestStatus(spy: MockInstance, id: string): unknown {
	const calls = spy.mock.calls as Array<[string, unknown?]>;
	return calls.filter((call) => call[0] === id).at(-1)?.[1];
}

describe("model-info contract", () => {
	let session: PiSession | undefined;

	afterEach(async () => {
		await session?.dispose();
		session = undefined;
		clearProbe();
		vi.restoreAllMocks();
	});

	it("loads in real Pi extension runtime", async () => {
		session = await createPiSession({ extensionPaths: [extensionPath()] });

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

	it("writes enabled lifecycle debug records in an isolated sandbox", async () => {
		const root = mkdtempSync(join(tmpdir(), "pi-ext-model-info-debug-"));
		try {
			session = await createPiSession({
				cwd: root,
				extensionPaths: [extensionPath()],
				env: {
					PI_MODEL_INFO_DEBUG: "1",
					XDG_STATE_HOME: join(root, "state"),
					LOCALAPPDATA: join(root, "state"),
				},
			});
			const runner = session.session.extensionRunner;
			runner.setUIContext({ ...runner.getUIContext() }, "tui");
			await runner.emit({ type: "session_start", reason: "startup" });
			await runner.emit({ type: "session_shutdown", reason: "quit" });
			const debugDir =
				process.platform === "darwin"
					? join(root, ".home", "Library", "Logs", "pi-ext", "model-info")
					: join(root, "state", "pi-ext", "debug", "model-info");
			const events = readdirSync(debugDir).flatMap((file) =>
				readFileSync(join(debugDir, 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 {
			await session?.dispose();
			session = undefined;
			rmSync(root, { recursive: true, force: true });
		}
	});

	it("uses acknowledged footer segments without transient fallback status", async () => {
		let accepted = 0;
		setProbe({
			onFooterSegment: (data: unknown) => {
				const accept = (data as { accept?: unknown })?.accept;
				if (typeof accept !== "function") return;
				(accept as () => void)();
				accepted++;
			},
		});
		session = await createPiSession({
			extensionPaths: [probePath(), extensionPath()],
		});
		const spy = captureStatus(session, "tui");
		await session.session.extensionRunner.emit({
			type: "session_start",
			reason: "startup",
		});

		expect(accepted).toBeGreaterThan(0);
		expect(latestStatus(spy, "model-info")).toBeUndefined();
	});

	it("keeps fallback status when only removals are acknowledged", async () => {
		let removals = 0;
		setProbe({
			onFooterSegment: (data: unknown) => {
				const segment = data as { text?: unknown; accept?: unknown };
				if (segment.text !== undefined || typeof segment.accept !== "function")
					return;
				(segment.accept as () => void)();
				removals++;
			},
		});
		session = await createPiSession({
			extensionPaths: [probePath(), extensionPath()],
		});
		const spy = captureStatus(session, "tui");
		await session.session.extensionRunner.emit({
			type: "session_start",
			reason: "startup",
		});

		expect(removals).toBeGreaterThan(0);
		expect(latestStatus(spy, "model-info")).toMatch(/:/);
	});

	it("replaces producer fallbacks with the combined route", async () => {
		setProbe({
			onSessionStart: (ctx, pi) => {
				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" },
				});
			},
		});
		session = await createPiSession({
			extensionPaths: [probePath(), extensionPath()],
		});
		const spy = captureStatus(session, "print");
		await session.session.extensionRunner.emit({
			type: "session_start",
			reason: "startup",
		});

		expect(latestStatus(spy, "fast")).toBeUndefined();
		expect(latestStatus(spy, "angel")).toBeUndefined();
		expect(latestStatus(spy, "model-info")).toMatch(/→ 󰧑 astra-6:max$/u);
	});

	it("shows the model a router resolved to after a reply, until the model changes", async () => {
		session = await createPiSession({ extensionPaths: [extensionPath()] });
		const runner = session.session.extensionRunner;
		const spy = captureStatus(session, "print");
		const model = runner.createContext().model;
		if (!model) throw new Error("fixture model missing");

		await runner.emitMessageEnd({
			type: "message_end",
			message: {
				role: "assistant",
				provider: model.provider,
				model: model.id,
				responseModel: "kilo/dots-studio/dots-3-note-preview:free",
			} as unknown as MessageEndEvent["message"],
		});
		expect(latestStatus(spy, "model-info")).toMatch(
			/ → dots-3-note-preview:free$/,
		);

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

	it("reports native virtual dispatch without synthetic responseModel metadata", () => {
		const ctx = {
			model: { api: "pi-virtual", provider: "grts", id: "auto" },
		} as never;
		const reply = {
			role: "assistant",
			provider: "google",
			model: "gemma-4-31b-it",
			stopReason: "stop",
		};
		expect(__test.servedModel(reply, ctx)).toBe("google/gemma-4-31b-it");
		expect(
			__test.servedModel({ ...reply, stopReason: "error" }, ctx),
		).toBeUndefined();
		expect(
			__test.servedModel({ ...reply, stopReason: "aborted" }, ctx),
		).toBeUndefined();
		expect(
			__test.servedModel({ ...reply, provider: "grts", model: "auto" }, ctx),
		).toBeUndefined();
		expect(
			__test.servedModel(
				{
					...reply,
					provider: "openrouter",
					model: "openrouter/free",
					responseModel: "qwen/test:free",
				},
				ctx,
			),
		).toBe("openrouter/qwen/test:free");
	});

	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" },
		} as any;

		expect(__test.modelInfoLabel(ctx, "high")).toBe("sol-5.6: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-5.6:medium", true, {
				modelId: "gpt-6-astra",
				thinkingLevel: "max",
			}),
		).toBe("sol-5.6:medium 󱐋 → 󰧑 astra-6:max");
	});

	it.each([
		["gpt-6-astra", "astra-6"],
		["gpt-5.6-sol", "sol-5.6"],
		["gpt-5.5", "gpt-5.5"],
		["claude-fable-5-1", "fable-5.1"],
		["claude-opus-5", "opus-5"],
		["claude-opus-5-5", "opus-5.5"],
		["claude-opus-4-5-20251101", "opus-4.5"],
		["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);
	});
});