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.
67 lines
3.1 KiB
TypeScript
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."]);
|
|
});
|