Tests: centralize contract coverage follow-ups (#48751)

* Plugins: harden global contract coverage

* Channels: tighten global contract coverage

* Channels: centralize inbound contract coverage

* Channels: move inbound contract helpers into core

* Tests: rename local inbound context checks

* Tests: stabilize contract runner profile

* Tests: split scoped contract lanes

* Channels: move inbound dispatch testkit into contracts

* Plugins: share provider contract registry helpers

* Plugins: reuse provider contract registry helpers
This commit is contained in:
Vincent Koc
2026-03-16 22:26:55 -07:00
committed by GitHub
parent 0bf11c1d69
commit 6c866b8543
27 changed files with 855 additions and 793 deletions

View File

@@ -10,8 +10,9 @@ import {
setupAuthTestEnv,
} from "../../commands/test-wizard-helpers.js";
import { createCapturedPluginRegistration } from "../../test-utils/plugin-registration.js";
import { buildProviderPluginMethodChoice } from "../provider-wizard.js";
import type { OpenClawPluginApi, ProviderPlugin } from "../types.js";
import { providerContractRegistry } from "./registry.js";
import { requireProviderContractProvider, uniqueProviderContractProviders } from "./registry.js";
type ResolvePluginProviders =
typeof import("../../commands/auth-choice.apply.plugin-provider.runtime.js").resolvePluginProviders;
@@ -101,11 +102,7 @@ describe("provider auth-choice contract", () => {
beforeEach(() => {
resolvePreferredProviderPluginProvidersMock.mockReset();
resolvePreferredProviderPluginProvidersMock.mockReturnValue([
...new Map(
providerContractRegistry.map((entry) => [entry.provider.id, entry.provider]),
).values(),
]);
resolvePreferredProviderPluginProvidersMock.mockReturnValue(uniqueProviderContractProviders);
});
afterEach(async () => {
@@ -121,21 +118,34 @@ describe("provider auth-choice contract", () => {
activeStateDir = null;
});
it("maps plugin-backed auth choices through the shared preferred-provider resolver", async () => {
const scenarios = [
{ authChoice: "github-copilot" as const, expectedProvider: "github-copilot" },
{ authChoice: "qwen-portal" as const, expectedProvider: "qwen-portal" },
{ authChoice: "minimax-global-oauth" as const, expectedProvider: "minimax-portal" },
{ authChoice: "modelstudio-api-key" as const, expectedProvider: "modelstudio" },
{ authChoice: "ollama" as const, expectedProvider: "ollama" },
{ authChoice: "unknown", expectedProvider: undefined },
] as const;
it("maps provider-plugin choices through the shared preferred-provider fallback resolver", async () => {
const pluginFallbackScenarios = [
"github-copilot",
"qwen-portal",
"minimax-portal",
"modelstudio",
"ollama",
].map((providerId) => {
const provider = requireProviderContractProvider(providerId);
return {
authChoice: buildProviderPluginMethodChoice(provider.id, provider.auth[0]?.id ?? "default"),
expectedProvider: provider.id,
};
});
for (const scenario of scenarios) {
for (const scenario of pluginFallbackScenarios) {
resolvePreferredProviderPluginProvidersMock.mockClear();
await expect(
resolvePreferredProviderForAuthChoice({ choice: scenario.authChoice }),
resolvePreferredProviderForAuthChoice({ choice: scenario.authChoice as AuthChoice }),
).resolves.toBe(scenario.expectedProvider);
expect(resolvePreferredProviderPluginProvidersMock).toHaveBeenCalled();
}
resolvePreferredProviderPluginProvidersMock.mockClear();
await expect(
resolvePreferredProviderForAuthChoice({ choice: "unknown" as AuthChoice }),
).resolves.toBe(undefined);
expect(resolvePreferredProviderPluginProvidersMock).toHaveBeenCalled();
});
it("applies qwen portal auth choices through the shared plugin-provider path", async () => {

View File

@@ -1,13 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { providerContractRegistry } from "./registry.js";
function uniqueProviders() {
return [
...new Map(
providerContractRegistry.map((entry) => [entry.provider.id, entry.provider]),
).values(),
];
}
import {
providerContractPluginIds,
resolveProviderContractProvidersForPluginIds,
uniqueProviderContractProviders,
} from "./registry.js";
const resolvePluginProvidersMock = vi.fn();
const resolveOwningPluginIdsForProviderMock = vi.fn();
@@ -30,12 +26,10 @@ const {
describe("provider catalog contract", () => {
beforeEach(() => {
const providers = uniqueProviders();
const providerIds = [...new Set(providerContractRegistry.map((entry) => entry.pluginId))];
resetProviderRuntimeHookCacheForTest();
resolveOwningPluginIdsForProviderMock.mockReset();
resolveOwningPluginIdsForProviderMock.mockReturnValue(providerIds);
resolveOwningPluginIdsForProviderMock.mockReturnValue(providerContractPluginIds);
resolveNonBundledProviderPluginIdsMock.mockReset();
resolveNonBundledProviderPluginIdsMock.mockReturnValue([]);
@@ -44,12 +38,9 @@ describe("provider catalog contract", () => {
resolvePluginProvidersMock.mockImplementation((params?: { onlyPluginIds?: string[] }) => {
const onlyPluginIds = params?.onlyPluginIds;
if (!onlyPluginIds || onlyPluginIds.length === 0) {
return providers;
return uniqueProviderContractProviders;
}
const allowed = new Set(onlyPluginIds);
return providerContractRegistry
.filter((entry) => allowed.has(entry.pluginId))
.map((entry) => entry.provider);
return resolveProviderContractProvidersForPluginIds(onlyPluginIds);
});
});

View File

@@ -2,7 +2,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { withBundledPluginAllowlistCompat } from "../bundled-compat.js";
import { __testing as providerTesting } from "../providers.js";
import { resolvePluginWebSearchProviders } from "../web-search-providers.js";
import { providerContractRegistry, webSearchProviderContractRegistry } from "./registry.js";
import {
providerContractPluginIds,
webSearchProviderContractRegistry,
} from "./registry.js";
function uniqueSortedPluginIds(values: string[]) {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
@@ -19,7 +22,7 @@ describe("plugin loader contract", () => {
it("keeps bundled provider compatibility wired to the provider registry", () => {
const providerPluginIds = uniqueSortedPluginIds(
providerContractRegistry.map((entry) => normalizeProviderContractPluginId(entry.pluginId)),
providerContractPluginIds.map(normalizeProviderContractPluginId),
);
const compatPluginIds = providerTesting.resolveBundledProviderCompatPluginIds({
config: {
@@ -46,7 +49,7 @@ describe("plugin loader contract", () => {
it("keeps vitest bundled provider enablement wired to the provider registry", () => {
const providerPluginIds = uniqueSortedPluginIds(
providerContractRegistry.map((entry) => normalizeProviderContractPluginId(entry.pluginId)),
providerContractPluginIds.map(normalizeProviderContractPluginId),
);
const compatConfig = providerTesting.withBundledProviderVitestCompat({
config: undefined,

View File

@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import { loadPluginManifestRegistry } from "../manifest-registry.js";
import { resolvePluginWebSearchProviders } from "../web-search-providers.js";
import {
mediaUnderstandingProviderContractRegistry,
pluginRegistrationContractRegistry,
providerContractPluginIds,
providerContractRegistry,
speechProviderContractRegistry,
webSearchProviderContractRegistry,
@@ -84,6 +87,27 @@ describe("plugin contract registry", () => {
expect(ids).toEqual([...new Set(ids)]);
});
it("covers every bundled provider plugin discovered from manifests", () => {
const bundledProviderPluginIds = loadPluginManifestRegistry({})
.plugins.filter((plugin) => plugin.origin === "bundled" && plugin.providers.length > 0)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
expect(providerContractPluginIds).toEqual(bundledProviderPluginIds);
});
it("covers every bundled web search plugin from the shared resolver", () => {
const bundledWebSearchPluginIds = resolvePluginWebSearchProviders({})
.map((provider) => provider.pluginId)
.toSorted((left, right) => left.localeCompare(right));
expect(
[...new Set(webSearchProviderContractRegistry.map((entry) => entry.pluginId))].toSorted(
(left, right) => left.localeCompare(right),
),
).toEqual(bundledWebSearchPluginIds);
});
it("keeps multi-provider plugin ownership explicit", () => {
expect(findProviderIdsForPlugin("google")).toEqual(["google", "google-gemini-cli"]);
expect(findProviderIdsForPlugin("minimax")).toEqual(["minimax", "minimax-portal"]);
@@ -146,6 +170,23 @@ describe("plugin contract registry", () => {
});
});
it("tracks every provider, speech, media, or web search plugin in the registration registry", () => {
const expectedPluginIds = [
...new Set([
...providerContractRegistry.map((entry) => entry.pluginId),
...speechProviderContractRegistry.map((entry) => entry.pluginId),
...mediaUnderstandingProviderContractRegistry.map((entry) => entry.pluginId),
...webSearchProviderContractRegistry.map((entry) => entry.pluginId),
]),
].toSorted((left, right) => left.localeCompare(right));
expect(
pluginRegistrationContractRegistry
.map((entry) => entry.pluginId)
.toSorted((left, right) => left.localeCompare(right)),
).toEqual(expectedPluginIds);
});
it("keeps bundled speech voice-list support explicit", () => {
expect(findSpeechProviderForPlugin("openai").listVoices).toEqual(expect.any(Function));
expect(findSpeechProviderForPlugin("elevenlabs").listVoices).toEqual(expect.any(Function));

View File

@@ -1,3 +1,4 @@
import amazonBedrockPlugin from "../../../extensions/amazon-bedrock/index.js";
import anthropicPlugin from "../../../extensions/anthropic/index.js";
import bravePlugin from "../../../extensions/brave/index.js";
import byteplusPlugin from "../../../extensions/byteplus/index.js";
@@ -72,6 +73,7 @@ type PluginRegistrationContractEntry = {
};
const bundledProviderPlugins: RegistrablePlugin[] = [
amazonBedrockPlugin,
anthropicPlugin,
byteplusPlugin,
cloudflareAiGatewayPlugin,
@@ -150,6 +152,35 @@ export const providerContractRegistry: ProviderContractEntry[] = buildCapability
select: (captured) => captured.providers,
});
export const uniqueProviderContractProviders: ProviderPlugin[] = [
...new Map(providerContractRegistry.map((entry) => [entry.provider.id, entry.provider])).values(),
];
export const providerContractPluginIds = [
...new Set(providerContractRegistry.map((entry) => entry.pluginId)),
].toSorted((left, right) => left.localeCompare(right));
export function requireProviderContractProvider(providerId: string): ProviderPlugin {
const provider = uniqueProviderContractProviders.find((entry) => entry.id === providerId);
if (!provider) {
throw new Error(`provider contract entry missing for ${providerId}`);
}
return provider;
}
export function resolveProviderContractProvidersForPluginIds(
pluginIds: readonly string[],
): ProviderPlugin[] {
const allowed = new Set(pluginIds);
return [
...new Map(
providerContractRegistry
.filter((entry) => allowed.has(entry.pluginId))
.map((entry) => [entry.provider.id, entry.provider]),
).values(),
];
}
export const webSearchProviderContractRegistry: WebSearchProviderContractEntry[] =
bundledWebSearchPlugins.flatMap((plugin) => {
const captured = captureRegistrations(plugin);

View File

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { createProviderUsageFetch, makeResponse } from "../../test-utils/provider-usage-fetch.js";
import type { ProviderRuntimeModel } from "../types.js";
import { requireProviderContractProvider } from "./registry.js";
const getOAuthApiKeyMock = vi.hoisted(() => vi.fn());
const refreshQwenPortalCredentialsMock = vi.hoisted(() => vi.fn());
@@ -17,16 +18,6 @@ vi.mock("../../providers/qwen-portal-oauth.js", () => ({
refreshQwenPortalCredentials: refreshQwenPortalCredentialsMock,
}));
const { providerContractRegistry } = await import("./registry.js");
function requireProvider(providerId: string) {
const entry = providerContractRegistry.find((candidate) => candidate.provider.id === providerId);
if (!entry) {
throw new Error(`provider contract entry missing for ${providerId}`);
}
return entry.provider;
}
function createModel(overrides: Partial<ProviderRuntimeModel> & Pick<ProviderRuntimeModel, "id">) {
return {
id: overrides.id,
@@ -45,7 +36,7 @@ function createModel(overrides: Partial<ProviderRuntimeModel> & Pick<ProviderRun
describe("provider runtime contract", () => {
describe("anthropic", () => {
it("owns anthropic 4.6 forward-compat resolution", () => {
const provider = requireProvider("anthropic");
const provider = requireProviderContractProvider("anthropic");
const model = provider.resolveDynamicModel?.({
provider: "anthropic",
modelId: "claude-sonnet-4.6-20260219",
@@ -71,7 +62,7 @@ describe("provider runtime contract", () => {
});
it("owns usage auth resolution", async () => {
const provider = requireProvider("anthropic");
const provider = requireProviderContractProvider("anthropic");
await expect(
provider.resolveUsageAuth?.({
config: {} as never,
@@ -88,7 +79,7 @@ describe("provider runtime contract", () => {
});
it("owns auth doctor hint generation", () => {
const provider = requireProvider("anthropic");
const provider = requireProviderContractProvider("anthropic");
const hint = provider.buildAuthDoctorHint?.({
provider: "anthropic",
profileId: "anthropic:default",
@@ -121,7 +112,7 @@ describe("provider runtime contract", () => {
});
it("owns usage snapshot fetching", async () => {
const provider = requireProvider("anthropic");
const provider = requireProviderContractProvider("anthropic");
const mockFetch = createProviderUsageFetch(async (url) => {
if (url.includes("api.anthropic.com/api/oauth/usage")) {
return makeResponse(200, {
@@ -154,7 +145,7 @@ describe("provider runtime contract", () => {
describe("github-copilot", () => {
it("owns Copilot-specific forward-compat fallbacks", () => {
const provider = requireProvider("github-copilot");
const provider = requireProviderContractProvider("github-copilot");
const model = provider.resolveDynamicModel?.({
provider: "github-copilot",
modelId: "gpt-5.3-codex",
@@ -181,7 +172,7 @@ describe("provider runtime contract", () => {
describe("google", () => {
it("owns google direct gemini 3.1 forward-compat resolution", () => {
const provider = requireProvider("google");
const provider = requireProviderContractProvider("google");
const model = provider.resolveDynamicModel?.({
provider: "google",
modelId: "gemini-3.1-pro-preview",
@@ -213,7 +204,7 @@ describe("provider runtime contract", () => {
describe("google-gemini-cli", () => {
it("owns gemini cli 3.1 forward-compat resolution", () => {
const provider = requireProvider("google-gemini-cli");
const provider = requireProviderContractProvider("google-gemini-cli");
const model = provider.resolveDynamicModel?.({
provider: "google-gemini-cli",
modelId: "gemini-3.1-pro-preview",
@@ -241,7 +232,7 @@ describe("provider runtime contract", () => {
});
it("owns usage-token parsing", async () => {
const provider = requireProvider("google-gemini-cli");
const provider = requireProviderContractProvider("google-gemini-cli");
await expect(
provider.resolveUsageAuth?.({
config: {} as never,
@@ -260,7 +251,7 @@ describe("provider runtime contract", () => {
});
it("owns OAuth auth-profile formatting", () => {
const provider = requireProvider("google-gemini-cli");
const provider = requireProviderContractProvider("google-gemini-cli");
expect(
provider.formatApiKey?.({
@@ -275,7 +266,7 @@ describe("provider runtime contract", () => {
});
it("owns usage snapshot fetching", async () => {
const provider = requireProvider("google-gemini-cli");
const provider = requireProviderContractProvider("google-gemini-cli");
const mockFetch = createProviderUsageFetch(async (url) => {
if (url.includes("cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota")) {
return makeResponse(200, {
@@ -309,7 +300,7 @@ describe("provider runtime contract", () => {
describe("openai", () => {
it("owns openai gpt-5.4 forward-compat resolution", () => {
const provider = requireProvider("openai");
const provider = requireProviderContractProvider("openai");
const model = provider.resolveDynamicModel?.({
provider: "openai",
modelId: "gpt-5.4-pro",
@@ -337,7 +328,7 @@ describe("provider runtime contract", () => {
});
it("owns direct openai transport normalization", () => {
const provider = requireProvider("openai");
const provider = requireProviderContractProvider("openai");
expect(
provider.normalizeResolvedModel?.({
provider: "openai",
@@ -360,7 +351,7 @@ describe("provider runtime contract", () => {
describe("openai-codex", () => {
it("owns refresh fallback for accountId extraction failures", async () => {
const provider = requireProvider("openai-codex");
const provider = requireProviderContractProvider("openai-codex");
const credential = {
type: "oauth" as const,
provider: "openai-codex",
@@ -376,7 +367,7 @@ describe("provider runtime contract", () => {
});
it("owns forward-compat codex models", () => {
const provider = requireProvider("openai-codex");
const provider = requireProviderContractProvider("openai-codex");
const model = provider.resolveDynamicModel?.({
provider: "openai-codex",
modelId: "gpt-5.4",
@@ -403,7 +394,7 @@ describe("provider runtime contract", () => {
});
it("owns codex transport defaults", () => {
const provider = requireProvider("openai-codex");
const provider = requireProviderContractProvider("openai-codex");
expect(
provider.prepareExtraParams?.({
provider: "openai-codex",
@@ -417,7 +408,7 @@ describe("provider runtime contract", () => {
});
it("owns usage snapshot fetching", async () => {
const provider = requireProvider("openai-codex");
const provider = requireProviderContractProvider("openai-codex");
const mockFetch = createProviderUsageFetch(async (url) => {
if (url.includes("chatgpt.com/backend-api/wham/usage")) {
return makeResponse(200, {
@@ -455,7 +446,7 @@ describe("provider runtime contract", () => {
describe("qwen-portal", () => {
it("owns OAuth refresh", async () => {
const provider = requireProvider("qwen-portal");
const provider = requireProviderContractProvider("qwen-portal");
const credential = {
type: "oauth" as const,
provider: "qwen-portal",
@@ -478,7 +469,7 @@ describe("provider runtime contract", () => {
describe("zai", () => {
it("owns glm-5 forward-compat resolution", () => {
const provider = requireProvider("zai");
const provider = requireProviderContractProvider("zai");
const model = provider.resolveDynamicModel?.({
provider: "zai",
modelId: "glm-5",
@@ -507,7 +498,7 @@ describe("provider runtime contract", () => {
});
it("owns usage auth resolution", async () => {
const provider = requireProvider("zai");
const provider = requireProviderContractProvider("zai");
await expect(
provider.resolveUsageAuth?.({
config: {} as never,
@@ -524,7 +515,7 @@ describe("provider runtime contract", () => {
});
it("owns usage snapshot fetching", async () => {
const provider = requireProvider("zai");
const provider = requireProviderContractProvider("zai");
const mockFetch = createProviderUsageFetch(async (url) => {
if (url.includes("api.z.ai/api/monitor/usage/quota/limit")) {
return makeResponse(200, {

View File

@@ -1,14 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ProviderPlugin } from "../types.js";
import { providerContractRegistry } from "./registry.js";
function uniqueProviders(): ProviderPlugin[] {
return [
...new Map(
providerContractRegistry.map((entry) => [entry.provider.id, entry.provider]),
).values(),
];
}
import { providerContractPluginIds, uniqueProviderContractProviders } from "./registry.js";
const resolvePluginProvidersMock = vi.fn();
@@ -81,18 +73,16 @@ function resolveExpectedModelPickerValues(providers: ProviderPlugin[]) {
describe("provider wizard contract", () => {
beforeEach(() => {
const providers = uniqueProviders();
resolvePluginProvidersMock.mockReset();
resolvePluginProvidersMock.mockReturnValue(providers);
resolvePluginProvidersMock.mockReturnValue(uniqueProviderContractProviders);
});
it("exposes every registered provider setup choice through the shared wizard layer", () => {
const providers = uniqueProviders();
const options = resolveProviderWizardOptions({
config: {
plugins: {
enabled: true,
allow: [...new Set(providerContractRegistry.map((entry) => entry.pluginId))],
allow: providerContractPluginIds,
slots: {
memory: "none",
},
@@ -103,18 +93,16 @@ describe("provider wizard contract", () => {
expect(
options.map((option) => option.value).toSorted((left, right) => left.localeCompare(right)),
).toEqual(resolveExpectedWizardChoiceValues(providers));
).toEqual(resolveExpectedWizardChoiceValues(uniqueProviderContractProviders));
expect(options.map((option) => option.value)).toEqual([
...new Set(options.map((option) => option.value)),
]);
});
it("round-trips every shared wizard choice back to its provider and auth method", () => {
const providers = uniqueProviders();
for (const option of resolveProviderWizardOptions({ config: {}, env: process.env })) {
const resolved = resolveProviderPluginChoice({
providers,
providers: uniqueProviderContractProviders,
choice: option.value,
});
expect(resolved).not.toBeNull();
@@ -124,15 +112,14 @@ describe("provider wizard contract", () => {
});
it("exposes every registered model-picker entry through the shared wizard layer", () => {
const providers = uniqueProviders();
const entries = resolveProviderModelPickerEntries({ config: {}, env: process.env });
expect(
entries.map((entry) => entry.value).toSorted((left, right) => left.localeCompare(right)),
).toEqual(resolveExpectedModelPickerValues(providers));
).toEqual(resolveExpectedModelPickerValues(uniqueProviderContractProviders));
for (const entry of entries) {
const resolved = resolveProviderPluginChoice({
providers,
providers: uniqueProviderContractProviders,
choice: entry.value,
});
expect(resolved).not.toBeNull();