Plugins: add provider discovery contracts

This commit is contained in:
Vincent Koc 2026-03-16 00:29:03 -07:00
parent 209f1a08d7
commit a6eda07316

View File

@ -0,0 +1,193 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
clearRuntimeAuthProfileStoreSnapshots,
replaceRuntimeAuthProfileStoreSnapshots,
} from "../../agents/auth-profiles/store.js";
import { QWEN_OAUTH_MARKER } from "../../agents/model-auth-markers.js";
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import { runProviderCatalog } from "../provider-discovery.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn());
vi.mock("../../../extensions/github-copilot/token.js", async () => {
const actual = await vi.importActual<object>("../../../extensions/github-copilot/token.js");
return {
...actual,
resolveCopilotApiToken: resolveCopilotApiTokenMock,
};
});
const qwenPortalPlugin = (await import("../../../extensions/qwen-portal-auth/index.js")).default;
const githubCopilotPlugin = (await import("../../../extensions/github-copilot/index.js")).default;
function registerProviders(...plugins: Array<{ register(api: OpenClawPluginApi): void }>) {
const captured = createCapturedPluginRegistration();
for (const plugin of plugins) {
plugin.register(captured.api);
}
return captured.providers;
}
function requireProvider(providers: ProviderPlugin[], providerId: string) {
const provider = providers.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider ${providerId} missing`);
}
return provider;
}
describe("provider discovery contract", () => {
afterEach(() => {
resolveCopilotApiTokenMock.mockReset();
clearRuntimeAuthProfileStoreSnapshots();
});
it("keeps qwen portal oauth marker fallback provider-owned", async () => {
const provider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal");
replaceRuntimeAuthProfileStoreSnapshots([
{
store: {
version: 1,
profiles: {
"qwen-portal:default": {
type: "oauth",
provider: "qwen-portal",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
},
},
},
]);
await expect(
runProviderCatalog({
provider,
config: {},
env: {} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
}),
).resolves.toEqual({
provider: {
baseUrl: "https://portal.qwen.ai/v1",
apiKey: QWEN_OAUTH_MARKER,
api: "openai-completions",
models: [
expect.objectContaining({ id: "coder-model", name: "Qwen Coder" }),
expect.objectContaining({ id: "vision-model", name: "Qwen Vision" }),
],
},
});
});
it("keeps qwen portal env api keys higher priority than oauth markers", async () => {
const provider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal");
replaceRuntimeAuthProfileStoreSnapshots([
{
store: {
version: 1,
profiles: {
"qwen-portal:default": {
type: "oauth",
provider: "qwen-portal",
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
},
},
},
]);
await expect(
runProviderCatalog({
provider,
config: {},
env: { QWEN_PORTAL_API_KEY: "env-key" } as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: "env-key" }),
}),
).resolves.toMatchObject({
provider: {
apiKey: "env-key",
},
});
});
it("keeps GitHub Copilot catalog disabled without env tokens or profiles", async () => {
const provider = requireProvider(registerProviders(githubCopilotPlugin), "github-copilot");
await expect(
runProviderCatalog({
provider,
config: {},
env: {} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
}),
).resolves.toBeNull();
});
it("keeps GitHub Copilot profile-only catalog fallback provider-owned", async () => {
const provider = requireProvider(registerProviders(githubCopilotPlugin), "github-copilot");
replaceRuntimeAuthProfileStoreSnapshots([
{
store: {
version: 1,
profiles: {
"github-copilot:github": {
type: "token",
provider: "github-copilot",
token: "profile-token",
},
},
},
},
]);
await expect(
runProviderCatalog({
provider,
config: {},
env: {} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
}),
).resolves.toEqual({
provider: {
baseUrl: "https://api.individual.githubcopilot.com",
models: [],
},
});
});
it("keeps GitHub Copilot env-token base URL resolution provider-owned", async () => {
const provider = requireProvider(registerProviders(githubCopilotPlugin), "github-copilot");
resolveCopilotApiTokenMock.mockResolvedValueOnce({
token: "copilot-api-token",
baseUrl: "https://copilot-proxy.example.com",
expiresAt: Date.now() + 60_000,
});
await expect(
runProviderCatalog({
provider,
config: {},
env: {
GITHUB_TOKEN: "github-env-token",
} as NodeJS.ProcessEnv,
resolveProviderApiKey: () => ({ apiKey: undefined }),
}),
).resolves.toEqual({
provider: {
baseUrl: "https://copilot-proxy.example.com",
models: [],
},
});
expect(resolveCopilotApiTokenMock).toHaveBeenCalledWith({
githubToken: "github-env-token",
env: expect.objectContaining({
GITHUB_TOKEN: "github-env-token",
}),
});
});
});