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.
This commit is contained in:
commit
4e854ba8c2
1 changed files with 2416 additions and 0 deletions
67
test/command.test.ts
Normal file
67
test/command.test.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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."]);
|
||||
});
|
||||
Loading…
Reference in a new issue