pi-unigpt/test/command.test.ts
Benjamin Hackl 4e854ba8c2 UniGPT provider for pi
Native /login with a verified API key, automatic model discovery from
UniGPT's /models endpoint, per-deployment deployment-id headers, and
model metadata from pi's bundled catalog.
2026-11-03 20:01:28 +01:00

67 lines
3.1 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import { refreshCatalog } from "../src/index.ts";
interface Fake {
configured?: boolean;
hasUI?: boolean;
result?: { aborted: boolean; errors: Map<string, Error> };
count?: number;
}
function fakeContext(fake: Fake) {
const notices: { message: string; level: string }[] = [];
const refreshes: unknown[] = [];
const ctx = {
hasUI: fake.hasUI ?? true,
ui: { notify: (message: string, level: string) => notices.push({ message, level }) },
modelRegistry: {
getProviderAuthStatus: () => ({ configured: fake.configured ?? true }),
getProviderAuth: () => { throw new Error("must not resolve credentials"); },
refresh: async (options: unknown) => {
refreshes.push(options);
return fake.result ?? { aborted: false, errors: new Map() };
},
getProvider: () => ({ getModels: () => Array.from({ length: fake.count ?? 0 }) }),
},
} as unknown as ExtensionCommandContext;
return { ctx, notices, refreshes };
}
test("refresh asks to log in without resolving credentials or calling the network", async () => {
const { ctx, notices, refreshes } = fakeContext({ configured: false });
await refreshCatalog(ctx);
assert.equal(refreshes.length, 0);
assert.deepEqual(notices, [{ message: "UniGPT is not configured. Run /login unigpt first.", level: "warning" }]);
});
test("refresh targets only UniGPT, bounded by a deadline, and reports the model count", async () => {
const { ctx, notices, refreshes } = fakeContext({ count: 3 });
await refreshCatalog(ctx);
const [options] = refreshes as { providers: string[]; signal: AbortSignal }[];
assert.deepEqual(options.providers, ["unigpt"]);
assert.ok(options.signal instanceof AbortSignal);
assert.deepEqual(notices, [{ message: "UniGPT: 3 models available. Select one with /model.", level: "info" }]);
});
test("refresh reports errors, timeouts, and empty catalogs as warnings", async () => {
const failed = fakeContext({ result: { aborted: false, errors: new Map([["unigpt", new Error("HTTP 503")]]) } });
await refreshCatalog(failed.ctx);
assert.match(failed.notices[0].message, /refresh failed \(HTTP 503\); keeping the cached/);
const timedOut = fakeContext({ result: { aborted: true, errors: new Map() } });
await refreshCatalog(timedOut.ctx);
assert.match(timedOut.notices[0].message, /no response within 20s/);
const empty = fakeContext({ count: 0 });
await refreshCatalog(empty.ctx);
assert.deepEqual(empty.notices, [{ message: "UniGPT lists no models for this API key.", level: "warning" }]);
});
test("without a UI, refresh reports on stderr instead of staying silent", async (t) => {
const lines: string[] = [];
t.mock.method(console, "error", (line: string) => lines.push(line));
const { ctx, notices } = fakeContext({ hasUI: false, count: 2 });
await refreshCatalog(ctx);
assert.equal(notices.length, 0);
assert.deepEqual(lines, ["[unigpt] UniGPT: 2 models available. Select one with /model."]);
});