repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/gratis/__tests__/contract.test.ts
Rawimport {
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
type AssistantMessage,
createFauxCore,
fauxAssistantMessage,
} from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it } from "vitest";
import {
createPiSession,
type PiSession,
} from "../../../test/fixture/create-pi-session.ts";
const extensionPath = fileURLToPath(new URL("../index.ts", import.meta.url));
const free = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const paid = { input: 1, output: 1, cacheRead: 0, cacheWrite: 0 };
function models(provider: string) {
return [
{
id:
provider === "openrouter"
? "qwen/test:free"
: provider === "google"
? "gemma-3-27b-it"
: "test-free",
name: `${provider} free`,
reasoning: false,
input: ["text" as const],
cost: free,
contextWindow: 32_000,
maxTokens: 1_024,
},
];
}
function nativeProvider(
provider: string,
cost = free,
id?: string,
additionalIds: string[] = [],
) {
const core = createFauxCore({ api: "openai", provider });
const model = {
...models(provider)[0]!,
id: id ?? models(provider)[0]!.id,
cost,
};
return {
core,
factory: (pi: ExtensionAPI) =>
pi.registerProvider(provider, {
api: "openai-completions",
apiKey: "test-key",
baseUrl: "https://faux.invalid",
models: [model, ...additionalIds.map((id) => ({ ...model, id }))],
streamSimple: (requestModel, context, options) =>
core.streamSimple(requestModel, context, options),
}),
};
}
let session: PiSession | undefined;
let debugRoot: string | undefined;
afterEach(async () => {
await session?.dispose();
session = undefined;
if (debugRoot) rmSync(debugRoot, { recursive: true, force: true });
debugRoot = undefined;
});
async function setup(providers: ReturnType<typeof nativeProvider>[]) {
session = await createPiSession({
env: {
GEMINI_API_KEY: undefined,
GOOGLE_API_KEY: undefined,
MISTRAL_API_KEY: undefined,
NVIDIA_API_KEY: undefined,
OPENROUTER_API_KEY: undefined,
},
extensionPaths: [extensionPath],
extensionFactories: providers.map((provider) => provider.factory),
});
expect(session.errors).toEqual([]);
const virtual = session.session.extensionRunner
.createContext()
.modelRegistry.find("grts", "auto");
if (!virtual) throw new Error("missing grts/auto");
return virtual;
}
function physicalMessages() {
return session!.session.agent.state.messages.filter(
(message) => message.role === "assistant",
) as AssistantMessage[];
}
describe("gratis extension contract", () => {
it("loads path-loaded, registers grts/auto, and dispatches a native physical model", async () => {
const openrouter = nativeProvider("openrouter");
const virtual = await setup([openrouter]);
openrouter.core.setResponses([fauxAssistantMessage("ok")]);
const message = await session!.session.extensionRunner
.createContext()
.modelRegistry.streamSimple(virtual, { messages: [] })
.result();
expect(message).toMatchObject({
provider: "openrouter",
model: "qwen/test:free",
});
expect(openrouter.core.state.callCount).toBe(1);
});
it.each([
[free, "auto"],
[paid, "qwen/paid:free"],
[{ ...free, cacheWrite: 1 }, "qwen/cache-priced:free"],
])("excludes paid or non-free OpenRouter entry %j/%s", async (cost, id) => {
const virtual = await setup([nativeProvider("openrouter", cost, id)]);
await expect(
session!.session.extensionRunner
.createContext()
.modelRegistry.streamSimple(virtual, { messages: [] })
.result(),
).resolves.toMatchObject({
stopReason: "error",
errorMessage: "Gratis: no eligible free native models are available.",
});
});
it.each([
["openrouter", "openrouter/free"],
["google", "gemma-4-31b-it"],
["mistral", "labs-devstral-small-2512"],
["mistral", "labs-leanstral-1-5-1"],
["nvidia", "openai/gpt-oss-20b"],
])("dispatches eligible %s/%s through native auth", async (provider, id) => {
const physical = nativeProvider(provider, free, id);
const virtual = await setup([physical]);
physical.core.setResponses([fauxAssistantMessage("ok")]);
const message = await session!.session.extensionRunner
.createContext()
.modelRegistry.streamSimple(virtual, { messages: [] })
.result();
expect(message).toMatchObject({ provider, model: id, stopReason: "stop" });
});
it.each([
["faux", "model"],
["zai", "glm-5.3-highspeed"],
["mistral", "unknown-zero-priced"],
["openrouter", "unknown-zero-priced"],
["google", "gemini-paid-tier"],
])("rejects unsupported free inference for %s/%s", async (provider, id) => {
const virtual = await setup([nativeProvider(provider, free, id)]);
const message = await session!.session.extensionRunner
.createContext()
.modelRegistry.streamSimple(virtual, { messages: [] })
.result();
expect(message).toMatchObject({ stopReason: "error" });
});
it("keeps tool follow-ups on the previous eligible physical model", async () => {
const openrouter = nativeProvider("openrouter");
const google = nativeProvider("google");
const virtual = await setup([openrouter, google]);
const file = path.join(session!.cwd, "note.txt");
writeFileSync(file, "test evidence");
openrouter.core.setResponses([
fauxAssistantMessage(
[
{
type: "toolCall",
id: "read-note",
name: "read",
arguments: { path: file },
},
],
{ stopReason: "toolUse" },
),
fauxAssistantMessage("finished"),
]);
await session!.session.setModel(virtual);
await session!.session.prompt("read the note");
expect(openrouter.core.state.callCount).toBe(2);
expect(google.core.state.callCount).toBe(0);
expect(session!.session.model).toMatchObject({
provider: "grts",
id: "auto",
});
expect(physicalMessages().at(-1)).toMatchObject({ provider: "openrouter" });
});
it("does not fail over when Pi retries are disabled", async () => {
const openrouter = nativeProvider("openrouter");
const google = nativeProvider("google");
const virtual = await setup([openrouter, google]);
openrouter.core.setResponses([
fauxAssistantMessage("down", {
stopReason: "error",
errorMessage: "429 rate limit",
}),
]);
session!.session.setAutoRetryEnabled(false);
await session!.session.setModel(virtual);
await session!.session.prompt("one request");
expect(openrouter.core.state.callCount).toBe(1);
expect(google.core.state.callCount).toBe(0);
});
it("advances only when Pi retries", async () => {
const openrouter = nativeProvider("openrouter");
const google = nativeProvider("google");
const virtual = await setup([openrouter, google]);
openrouter.core.setResponses([
fauxAssistantMessage("down", {
stopReason: "error",
errorMessage: "429 rate limit",
}),
]);
google.core.setResponses([fauxAssistantMessage("recovered")]);
session!.session.setAutoRetryEnabled(true);
await session!.session.setModel(virtual);
await session!.session.sendUserMessage("retry once");
await session!.session.waitForIdle();
expect(openrouter.core.state.callCount).toBe(1);
expect(google.core.state.callCount).toBe(1);
expect(physicalMessages().at(-1)).toMatchObject({
provider: "google",
model: "gemma-3-27b-it",
});
const before = session!.session.sessionManager.getBranch();
openrouter.core.setResponses([fauxAssistantMessage("direct")]);
await session!.session.extensionRunner
.createContext()
.modelRegistry.streamSimple(virtual, { messages: [] })
.result();
expect(session!.session.sessionManager.getBranch()).toEqual(before);
openrouter.core.setResponses([fauxAssistantMessage("new turn")]);
await session!.session.prompt("fresh user turn");
expect(physicalMessages().at(-1)).toMatchObject({ provider: "openrouter" });
});
it("alternates providers and exhausts a pool of at most eight candidates", async () => {
const openrouter = nativeProvider(
"openrouter",
free,
"openrouter/free",
Array.from({ length: 12 }, (_, index) => `test/${index}:free`),
);
const google = nativeProvider("google");
const virtual = await setup([openrouter, google]);
const failure = () =>
fauxAssistantMessage("down", {
stopReason: "error",
errorMessage: "429 rate limit",
});
openrouter.core.setResponses(Array.from({ length: 12 }, failure));
google.core.setResponses([failure()]);
session!.session.settingsManager.applyOverrides({
retry: {
enabled: true,
maxRetries: 10,
baseDelayMs: 1,
maxAgentDelayMs: 1,
},
});
await session!.session.setModel(virtual);
await session!.session.prompt("bounded failure");
expect(openrouter.core.state.callCount).toBe(7);
expect(google.core.state.callCount).toBe(1);
expect(physicalMessages().at(-1)).toMatchObject({
errorMessage:
"Gratis: all eligible free native models were exhausted by Pi retries.",
});
});
it("reports exhausted retries and does not leak tried state into another session", async () => {
const openrouter = nativeProvider("openrouter");
const virtual = await setup([openrouter]);
openrouter.core.setResponses([
fauxAssistantMessage("down", {
stopReason: "error",
errorMessage: "429 rate limit",
}),
]);
session!.session.setAutoRetryEnabled(true);
await session!.session.setModel(virtual);
await session!.session.sendUserMessage("exhaust");
await session!.session.waitForIdle();
expect(physicalMessages().at(-1)).toMatchObject({
errorMessage:
"Gratis: all eligible free native models were exhausted by Pi retries.",
});
await session!.dispose();
session = undefined;
const next = nativeProvider("openrouter");
await setup([next]);
next.core.setResponses([fauxAssistantMessage("fresh")]);
const nextVirtual = session!.session.extensionRunner
.createContext()
.modelRegistry.find("grts", "auto");
if (!nextVirtual) throw new Error("missing grts/auto");
await session!.session.setModel(nextVirtual);
await session!.session.sendUserMessage("fresh branch");
await session!.session.waitForIdle();
expect(next.core.state.callCount).toBe(1);
});
it("writes lifecycle debug records under sandboxed LOCALAPPDATA", async () => {
const openrouter = nativeProvider("openrouter");
debugRoot = mkdtempSync(path.join(tmpdir(), "gratis-local-app-data-"));
session = await createPiSession({
env: { PI_GRATIS_DEBUG: "1", LOCALAPPDATA: debugRoot },
extensionPaths: [extensionPath],
extensionFactories: [openrouter.factory],
});
const localAppData = debugRoot;
await session.session.extensionRunner.emit({
type: "session_start",
reason: "startup",
});
await session.session.extensionRunner.emit({
type: "session_shutdown",
reason: "quit",
});
const directory =
process.platform === "win32"
? path.join(localAppData, "pi-ext", "debug", "gratis")
: process.platform === "darwin"
? path.join(
session.cwd,
".home",
"Library",
"Logs",
"pi-ext",
"gratis",
)
: path.join(
session.env["XDG_STATE_HOME"]!,
"pi-ext",
"debug",
"gratis",
);
const records = readFileSync(
path.join(directory, readdirSync(directory)[0]!),
"utf8",
);
expect(records).toContain('"event":"session.start"');
expect(records).toContain('"event":"session.shutdown"');
});
});