Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
241
packages/coding-agent/test/remote-catalog-provider.test.ts
Normal file
241
packages/coding-agent/test/remote-catalog-provider.test.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import {
|
||||
createModels,
|
||||
createProvider,
|
||||
InMemoryModelsStore,
|
||||
type Model,
|
||||
type ModelsPublication,
|
||||
type Provider,
|
||||
type RefreshModelsContext,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { VERSION } from "../src/config.ts";
|
||||
import { withRemoteCatalog } from "../src/core/remote-catalog-provider.ts";
|
||||
|
||||
const neverAbortedSignal = new AbortController().signal;
|
||||
|
||||
function model(id: string): Model<"openai-completions"> {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
api: "openai-completions",
|
||||
provider: "test-provider",
|
||||
baseUrl: "https://example.test/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
};
|
||||
}
|
||||
|
||||
function testProvider(localGeneratedAt?: number) {
|
||||
return withRemoteCatalog(
|
||||
createProvider({
|
||||
id: "test-provider",
|
||||
auth: { apiKey: { name: "Test", resolve: async () => ({ auth: {} }) } },
|
||||
models: [model("static")],
|
||||
api: {
|
||||
stream: () => {
|
||||
throw new Error("not used");
|
||||
},
|
||||
streamSimple: () => {
|
||||
throw new Error("not used");
|
||||
},
|
||||
},
|
||||
}),
|
||||
"https://pi.dev",
|
||||
localGeneratedAt,
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshProvider(
|
||||
provider: Provider,
|
||||
store: InMemoryModelsStore,
|
||||
overrides: Partial<Pick<RefreshModelsContext, "allowNetwork" | "force" | "signal">> = {},
|
||||
): Promise<void> {
|
||||
const publish = async (publication: ModelsPublication): Promise<boolean> => {
|
||||
if (publication.persist === null) await store.delete(provider.id);
|
||||
else if (publication.persist !== undefined) await store.write(provider.id, publication.persist);
|
||||
publication.update?.();
|
||||
return true;
|
||||
};
|
||||
await provider.refreshModels?.({
|
||||
credential: { type: "api_key" },
|
||||
stored: await store.read(provider.id),
|
||||
publish,
|
||||
allowNetwork: overrides.allowNetwork ?? true,
|
||||
force: overrides.force,
|
||||
signal: overrides.signal ?? neverAbortedSignal,
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("remote catalog provider", () => {
|
||||
it("parses keyed catalogs, sends version headers, observes the refresh TTL, and supports forced refreshes", async () => {
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
const provider = testProvider();
|
||||
const store = new InMemoryModelsStore();
|
||||
await refreshProvider(provider, store);
|
||||
await refreshProvider(provider, store);
|
||||
await refreshProvider(provider, store, { force: true });
|
||||
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "dynamic"]);
|
||||
expect((await store.read(provider.id))?.models.map((entry) => entry.id)).toEqual(["dynamic"]);
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(fetchSpy.mock.calls[0]?.[1]?.headers).toMatchObject({
|
||||
"User-Agent": expect.stringContaining(`pi/${VERSION}`),
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the newer of the generated and remote catalogs", async () => {
|
||||
const localGeneratedAt = Date.parse("2026-07-23T10:00:00.000Z");
|
||||
const newerHeader = new Date(localGeneratedAt + 60_000).toUTCString();
|
||||
const responses = [
|
||||
new Response(JSON.stringify({ old: model("old") }), {
|
||||
headers: { "last-modified": new Date(localGeneratedAt - 60_000).toUTCString() },
|
||||
}),
|
||||
new Response(JSON.stringify({ newer: model("newer") }), {
|
||||
headers: { "last-modified": newerHeader },
|
||||
}),
|
||||
];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async () => responses.shift() as Response);
|
||||
const provider = testProvider(localGeneratedAt);
|
||||
const store = new InMemoryModelsStore();
|
||||
|
||||
await refreshProvider(provider, store);
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static"]);
|
||||
|
||||
await refreshProvider(provider, store, { force: true });
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "newer"]);
|
||||
expect(await store.read(provider.id)).toMatchObject({ lastModified: Date.parse(newerHeader) });
|
||||
});
|
||||
|
||||
it("revalidates a stored catalog with its etag and keeps the overlay on 304", async () => {
|
||||
const responses = [
|
||||
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
||||
headers: { "content-type": "application/json", etag: '"catalog-1"' },
|
||||
}),
|
||||
new Response(null, { status: 304, headers: { etag: '"catalog-1"' } }),
|
||||
];
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => responses.shift() as Response);
|
||||
const provider = testProvider();
|
||||
const store = new InMemoryModelsStore();
|
||||
|
||||
await refreshProvider(provider, store);
|
||||
expect(fetchSpy.mock.calls[0]?.[1]?.headers).not.toHaveProperty("if-none-match");
|
||||
expect(await store.read(provider.id)).toMatchObject({ etag: '"catalog-1"' });
|
||||
|
||||
const checkedAt = (await store.read(provider.id))?.checkedAt;
|
||||
await refreshProvider(provider, store, { force: true });
|
||||
|
||||
expect(fetchSpy.mock.calls[1]?.[1]?.headers).toMatchObject({ "if-none-match": '"catalog-1"' });
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "dynamic"]);
|
||||
const stored = await store.read(provider.id);
|
||||
expect(stored?.models.map((entry) => entry.id)).toEqual(["dynamic"]);
|
||||
expect(stored?.etag).toBe('"catalog-1"');
|
||||
expect(stored?.checkedAt).toBeGreaterThanOrEqual(checkedAt ?? 0);
|
||||
});
|
||||
|
||||
it("drops a stale etag when the overlay becomes unavailable", async () => {
|
||||
const responses = [
|
||||
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
||||
headers: { "content-type": "application/json", etag: '"catalog-1"' },
|
||||
}),
|
||||
new Response("not implemented", { status: 501 }),
|
||||
];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async () => responses.shift() as Response);
|
||||
const provider = testProvider();
|
||||
const store = new InMemoryModelsStore();
|
||||
|
||||
await refreshProvider(provider, store);
|
||||
await refreshProvider(provider, store, { force: true });
|
||||
|
||||
expect((await store.read(provider.id))?.etag).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps the etag and overlay after a transient failure", async () => {
|
||||
const responses = [
|
||||
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
|
||||
headers: { "content-type": "application/json", etag: '"catalog-1"' },
|
||||
}),
|
||||
new Response("rate limited", { status: 429 }),
|
||||
new Response("rate limited", { status: 429 }),
|
||||
new Response("rate limited", { status: 429 }),
|
||||
new Response(null, { status: 304, headers: { etag: '"catalog-1"' } }),
|
||||
];
|
||||
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async () => responses.shift() as Response);
|
||||
const provider = testProvider();
|
||||
const store = new InMemoryModelsStore();
|
||||
|
||||
await refreshProvider(provider, store);
|
||||
await expect(refreshProvider(provider, store, { force: true })).rejects.toThrow(/429/);
|
||||
|
||||
const stored = await store.read(provider.id);
|
||||
expect(stored?.etag).toBe('"catalog-1"');
|
||||
expect(stored?.models.map((entry) => entry.id)).toEqual(["dynamic"]);
|
||||
|
||||
await refreshProvider(provider, store, { force: true });
|
||||
expect(fetchSpy.mock.calls[4]?.[1]?.headers).toMatchObject({ "if-none-match": '"catalog-1"' });
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "dynamic"]);
|
||||
});
|
||||
|
||||
it("lets a newer catalog request bypass a stalled older request without stale publication", async () => {
|
||||
let calls = 0;
|
||||
let markFirstStarted: (() => void) | undefined;
|
||||
let finishFirst: ((response: Response) => void) | undefined;
|
||||
const firstStarted = new Promise<void>((resolve) => {
|
||||
markFirstStarted = resolve;
|
||||
});
|
||||
const firstResponse = new Promise<Response>((resolve) => {
|
||||
finishFirst = resolve;
|
||||
});
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
|
||||
calls++;
|
||||
if (calls === 1) {
|
||||
markFirstStarted?.();
|
||||
return firstResponse;
|
||||
}
|
||||
return new Response(JSON.stringify({ newer: model("newer") }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
});
|
||||
const provider = testProvider();
|
||||
const store = new InMemoryModelsStore();
|
||||
const models = createModels({ modelsStore: store });
|
||||
models.setProvider(provider);
|
||||
|
||||
const first = models.refresh({ providers: [provider.id], force: true });
|
||||
await firstStarted;
|
||||
const second = models.refresh({ providers: [provider.id], force: true });
|
||||
await second;
|
||||
await first;
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "newer"]);
|
||||
|
||||
finishFirst?.(
|
||||
new Response(JSON.stringify({ older: model("older") }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static", "newer"]);
|
||||
expect((await store.read(provider.id))?.models.map((entry) => entry.id)).toEqual(["newer"]);
|
||||
});
|
||||
|
||||
it("treats unimplemented pi.dev catalog routes as an unavailable overlay", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("not implemented", { status: 501 }));
|
||||
const provider = testProvider();
|
||||
const store = new InMemoryModelsStore();
|
||||
|
||||
await expect(refreshProvider(provider, store)).resolves.toBeUndefined();
|
||||
expect(provider.getModels().map((entry) => entry.id)).toEqual(["static"]);
|
||||
expect(await store.read(provider.id)).toMatchObject({ models: [], checkedAt: expect.any(Number) });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user