feat(coding-agent): add model catalog refresh flag

This commit is contained in:
Armin Ronacher
2026-07-16 12:46:49 +02:00
parent 2be9efa19c
commit 97f9978fa6
13 changed files with 148 additions and 19 deletions
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ENV_AGENT_DIR, PACKAGE_NAME, VERSION } from "../src/config.ts";
import { ModelRuntime } from "../src/core/model-runtime.ts";
import type { ResolvedPaths } from "../src/core/package-manager.ts";
import { InMemorySettingsStorage, SettingsManager } from "../src/core/settings-manager.ts";
import { ProjectTrustStore } from "../src/core/trust-manager.ts";
@@ -371,6 +372,42 @@ describe("package commands", () => {
}
});
it("refreshes only model catalogs with update --models", async () => {
const refresh = vi.fn(async () => ({ aborted: false, errors: new Map<string, Error>() }));
const create = vi.spyOn(ModelRuntime, "create").mockResolvedValue({ refresh } as unknown as ModelRuntime);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runPackageCommandDirectly(["update", "--models"])).resolves.toBeUndefined();
expect(create).toHaveBeenCalledWith({
authPath: join(agentDir, "auth.json"),
modelsPath: join(agentDir, "models.json"),
allowModelNetwork: false,
});
expect(refresh).toHaveBeenCalledWith({
allowNetwork: true,
force: true,
signal: expect.any(AbortSignal),
});
expect(logSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain("Model catalogs refreshed");
expect(errorSpy).not.toHaveBeenCalled();
expect(process.exitCode).toBeUndefined();
});
it("rejects update --models combined with another update target", async () => {
const create = vi.spyOn(ModelRuntime, "create");
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runPackageCommandDirectly(["update", "--models", "--self"])).resolves.toBeUndefined();
expect(create).not.toHaveBeenCalled();
expect(errorSpy.mock.calls.map(([message]) => String(message)).join("\n")).toContain(
"--models cannot be combined with --self",
);
expect(process.exitCode).toBe(1);
});
it("cycles project package overrides in config local mode", async () => {
const storage = new InMemorySettingsStorage();
storage.withLock("global", () => JSON.stringify({ packages: ["npm:pi-tools"] }));
@@ -21,12 +21,13 @@ function model(id: string): Model<"openai-completions"> {
afterEach(() => vi.restoreAllMocks());
describe("remote catalog provider", () => {
it("parses keyed catalogs, sends version headers, and observes the refresh TTL", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
new Response(JSON.stringify({ dynamic: model("dynamic") }), {
status: 200,
headers: { "content-type": "application/json" },
}),
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 = withRemoteCatalog(
createProvider({
@@ -62,10 +63,20 @@ describe("remote catalog provider", () => {
},
allowNetwork: true,
});
await provider.refreshModels?.({
credential: { type: "api_key" },
store: {
read: () => store.read(provider.id),
write: (entry) => store.write(provider.id, entry),
delete: () => store.delete(provider.id),
},
allowNetwork: true,
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(1);
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(fetchSpy.mock.calls[0]?.[1]?.headers).toMatchObject({
"User-Agent": expect.stringContaining(`pi/${VERSION}`),
});