test: dedupe plugin provider runtime suites

This commit is contained in:
Peter Steinberger 2026-03-28 03:29:41 +00:00
parent de173f0e3e
commit b4c38c78f3
6 changed files with 105 additions and 64 deletions

View File

@ -70,6 +70,18 @@ function expectBundledCompatLoadPath(params: {
});
}
function createCompatChainConfig() {
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
const allowlistCompat = { plugins: { allow: ["custom-plugin", "openai"] } };
const enablementCompat = {
plugins: {
allow: ["custom-plugin", "openai"],
entries: { openai: { enabled: true } },
},
};
return { cfg, allowlistCompat, enablementCompat };
}
function setBundledCapabilityFixture(contractKey: string) {
mocks.loadPluginManifestRegistry.mockReturnValue({
plugins: [
@ -137,14 +149,7 @@ describe("resolvePluginCapabilityProviders", () => {
["mediaUnderstandingProviders", "mediaUnderstandingProviders"],
["imageGenerationProviders", "imageGenerationProviders"],
] as const)("applies bundled compat before fallback loading for %s", (key, contractKey) => {
const cfg = { plugins: { allow: ["custom-plugin"] } } as OpenClawConfig;
const allowlistCompat = { plugins: { allow: ["custom-plugin", "openai"] } };
const enablementCompat = {
plugins: {
allow: ["custom-plugin", "openai"],
entries: { openai: { enabled: true } },
},
};
const { cfg, allowlistCompat, enablementCompat } = createCompatChainConfig();
setBundledCapabilityFixture(contractKey);
mocks.withBundledPluginAllowlistCompat.mockReturnValue(allowlistCompat);
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);

View File

@ -30,6 +30,10 @@ function expectDiagnosticMessages(
);
}
function expectDiagnosticText(diagnostics: PluginDiagnostic[], messages: readonly string[]) {
expect(diagnostics.map((diag) => diag.message)).toEqual([...messages]);
}
function normalizeProviderFixture(provider: ProviderPlugin) {
const { diagnostics, pushDiagnostic } = collectDiagnostics();
const normalizedProvider = normalizeRegisteredProvider({
@ -155,7 +159,7 @@ describe("normalizeRegisteredProvider", () => {
diagnostics: PluginDiagnostic[],
) => {
expect(provider?.wizard).toBeUndefined();
expect(diagnostics.map((diag) => diag.message)).toEqual([
expectDiagnosticText(diagnostics, [
'provider "demo" setup metadata ignored because it has no auth methods',
'provider "demo" model-picker metadata ignored because it has no auth methods',
]);
@ -195,7 +199,7 @@ describe("normalizeRegisteredProvider", () => {
expect(provider?.catalog).toBeDefined();
expect(provider?.discovery).toBeUndefined();
expect(diagnostics.map((diag) => diag.message)).toEqual([
expectDiagnosticText(diagnostics, [
'provider "demo" registered both catalog and discovery; using catalog',
]);
});

View File

@ -95,6 +95,13 @@ function expectNoCompatibilityWarnings() {
expect(buildPluginCompatibilityWarnings()).toEqual([]);
}
function expectCapabilityKinds(
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
kinds: readonly string[],
) {
expect(inspect.capabilities.map((entry) => entry.kind)).toEqual(kinds);
}
describe("buildPluginStatusReport", () => {
beforeEach(async () => {
vi.resetModules();
@ -230,7 +237,7 @@ describe("buildPluginStatusReport", () => {
expect(inspect).not.toBeNull();
expect(inspect?.shape).toBe("hybrid-capability");
expect(inspect?.capabilityMode).toBe("hybrid");
expect(inspect?.capabilities.map((entry) => entry.kind)).toEqual([
expectCapabilityKinds(inspect!, [
"cli-backend",
"text-inference",
"media-understanding",
@ -279,10 +286,7 @@ describe("buildPluginStatusReport", () => {
expect(inspect.map((entry) => entry.plugin.id)).toEqual(["lca", "microsoft"]);
expect(inspect.map((entry) => entry.shape)).toEqual(["hook-only", "hybrid-capability"]);
expect(inspect[0]?.usesLegacyBeforeAgentStart).toBe(true);
expect(inspect[1]?.capabilities.map((entry) => entry.kind)).toEqual([
"text-inference",
"web-search",
]);
expectCapabilityKinds(inspect[1], ["text-inference", "web-search"]);
});
it("treats a CLI-backend-only plugin as a plain capability", () => {
@ -298,6 +302,7 @@ describe("buildPluginStatusReport", () => {
expect(inspect?.shape).toBe("plain-capability");
expect(inspect?.capabilityMode).toBe("plain");
expectCapabilityKinds(inspect!, ["cli-backend"]);
expect(inspect?.capabilities).toEqual([{ kind: "cli-backend", ids: ["claude-cli"] }]);
});

View File

@ -117,6 +117,14 @@ function expectLoaderCall(overrides: Record<string, unknown>) {
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(expect.objectContaining(overrides));
}
function expectSingleDiagnosticMessage(
diagnostics: Array<{ message: string }>,
messageFragment: string,
) {
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0]?.message).toContain(messageFragment);
}
describe("resolvePluginTools optional tools", () => {
beforeEach(async () => {
vi.resetModules();
@ -170,8 +178,7 @@ describe("resolvePluginTools optional tools", () => {
);
expect(tools).toHaveLength(0);
expect(registry.diagnostics).toHaveLength(1);
expect(registry.diagnostics[0]?.message).toContain("plugin id conflicts with core tool name");
expectSingleDiagnosticMessage(registry.diagnostics, "plugin id conflicts with core tool name");
});
it("skips conflicting tool names but keeps other tools", () => {
@ -179,8 +186,7 @@ describe("resolvePluginTools optional tools", () => {
const tools = resolveWithConflictingCoreName();
expectResolvedToolNames(tools, ["other_tool"]);
expect(registry.diagnostics).toHaveLength(1);
expect(registry.diagnostics[0]?.message).toContain("plugin tool name conflict");
expectSingleDiagnosticMessage(registry.diagnostics, "plugin tool name conflict");
});
it("suppresses conflict diagnostics when requested", () => {

View File

@ -131,6 +131,51 @@ function expectBundledRuntimeProviderKeys(
);
}
function createManifestRegistryFixture() {
return {
plugins: [
{
id: "brave",
origin: "bundled",
rootDir: "/tmp/brave",
source: "/tmp/brave/index.js",
manifestPath: "/tmp/brave/openclaw.plugin.json",
channels: [],
providers: [],
skills: [],
hooks: [],
configUiHints: { "webSearch.apiKey": { label: "key" } },
},
{
id: "noise",
origin: "bundled",
rootDir: "/tmp/noise",
source: "/tmp/noise/index.js",
manifestPath: "/tmp/noise/openclaw.plugin.json",
channels: [],
providers: [],
skills: [],
hooks: [],
configUiHints: { unrelated: { label: "nope" } },
},
],
diagnostics: [],
};
}
function expectLoaderCallCount(count: number) {
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(count);
}
function expectScopedWebSearchCandidates(pluginIds: readonly string[]) {
expect(loadPluginManifestRegistryMock).toHaveBeenCalled();
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: [...pluginIds],
}),
);
}
describe("resolvePluginWebSearchProviders", () => {
beforeAll(async () => {
({ createEmptyPluginRegistry } = await import("./registry.js"));
@ -148,39 +193,13 @@ describe("resolvePluginWebSearchProviders", () => {
resetWebSearchProviderSnapshotCacheForTests();
loadPluginManifestRegistryMock = vi
.spyOn(manifestRegistryModule, "loadPluginManifestRegistry")
.mockReturnValue({
plugins: [
{
id: "brave",
origin: "bundled",
rootDir: "/tmp/brave",
source: "/tmp/brave/index.js",
manifestPath: "/tmp/brave/openclaw.plugin.json",
channels: [],
providers: [],
skills: [],
hooks: [],
configUiHints: { "webSearch.apiKey": { label: "key" } },
},
{
id: "noise",
origin: "bundled",
rootDir: "/tmp/noise",
source: "/tmp/noise/index.js",
manifestPath: "/tmp/noise/openclaw.plugin.json",
channels: [],
providers: [],
skills: [],
hooks: [],
configUiHints: { unrelated: { label: "nope" } },
},
],
diagnostics: [],
} as ManifestRegistryModule["loadPluginManifestRegistry"] extends (
...args: unknown[]
) => infer R
? R
: never);
.mockReturnValue(
createManifestRegistryFixture() as ManifestRegistryModule["loadPluginManifestRegistry"] extends (
...args: unknown[]
) => infer R
? R
: never,
);
loadOpenClawPluginsMock = vi
.spyOn(loaderModule, "loadOpenClawPlugins")
.mockImplementation((params) => {
@ -201,18 +220,13 @@ describe("resolvePluginWebSearchProviders", () => {
const providers = resolvePluginWebSearchProviders({});
expectBundledRuntimeProviderKeys(providers);
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
expectLoaderCallCount(1);
});
it("scopes plugin loading to manifest-declared web-search candidates", () => {
resolvePluginWebSearchProviders({});
expect(loadPluginManifestRegistryMock).toHaveBeenCalled();
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: ["brave"],
}),
);
expectScopedWebSearchCandidates(["brave"]);
});
it("memoizes snapshot provider resolution for the same config and env", () => {
@ -224,7 +238,7 @@ describe("resolvePluginWebSearchProviders", () => {
const second = resolvePluginWebSearchProviders(runtimeParams);
expect(second).toBe(first);
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
expectLoaderCallCount(1);
});
it("invalidates the snapshot cache when config or env contents change in place", () => {
@ -236,7 +250,7 @@ describe("resolvePluginWebSearchProviders", () => {
env.OPENCLAW_HOME = "/tmp/openclaw-home-b";
resolvePluginWebSearchProviders(createSnapshotParams({ config, env }));
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2);
expectLoaderCallCount(2);
});
it.each([
@ -258,7 +272,7 @@ describe("resolvePluginWebSearchProviders", () => {
resolvePluginWebSearchProviders(createSnapshotParams({ config, env: createWebSearchEnv(env) }));
resolvePluginWebSearchProviders(createSnapshotParams({ config, env: createWebSearchEnv(env) }));
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2);
expectLoaderCallCount(2);
});
it("does not leak host Vitest env into an explicit non-Vitest cache key", () => {
@ -313,7 +327,7 @@ describe("resolvePluginWebSearchProviders", () => {
resolvePluginWebSearchProviders(createSnapshotParams({ config, env }));
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2);
expectLoaderCallCount(2);
});
it("prefers the active plugin registry for runtime resolution", () => {

View File

@ -51,6 +51,13 @@ function expectBundledWebSearchProviders(
);
}
function expectResolvedPluginIds(
providers: ReturnType<typeof resolveBundledPluginWebSearchProviders>,
expectedPluginIds: readonly string[],
) {
expect(providers.map((provider) => provider.pluginId)).toEqual(expectedPluginIds);
}
describe("resolveBundledPluginWebSearchProviders", () => {
it(
"returns bundled providers in alphabetical order",
@ -123,7 +130,7 @@ describe("resolveBundledPluginWebSearchProviders", () => {
])("$title", ({ params, expectedPluginIds }) => {
const providers = resolveBundledPluginWebSearchProviders(params);
expect(providers.map((provider) => provider.pluginId)).toEqual(expectedPluginIds);
expectResolvedPluginIds(providers, expectedPluginIds);
});
it("preserves explicit bundled provider entry state", () => {