fix(web_search): skip redundant provider re-resolution for external Brave plugin

Guards the secondary resolveProviders call with `!allProviders.some(p => p.id === rawProvider)` so it only fires when the first pass genuinely missed the configured provider. Eliminates the spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and incorrect `providerSource: "none"` for external Brave plugin installs. Fixes #77676.
This commit is contained in:
Chunyue Wang
2026-05-06 17:45:20 +08:00
committed by GitHub
parent 8cb58813f2
commit 34b67c3f25
3 changed files with 96 additions and 1 deletions

View File

@@ -433,6 +433,7 @@ Docs: https://docs.openclaw.ai
- Exec approvals: enforce allowlist `argPattern` argument restrictions on Linux and macOS as well as Windows, so an entry like `{ pattern: "python3", argPattern: "^safe\.py$" }` no longer silently relaxes to a path-only match on non-Windows hosts. (#75143) Thanks @eleqtrizit.
- Agents/compaction: disable Pi auto-compaction whenever OpenClaw effectively owns safeguard compaction, including provider-backed safeguard mode, so Pi and OpenClaw no longer fight over long-session compaction. Fixes #73003. (#73839) Thanks @bradhallett.
- Telegram/streaming: finalize text replies by stopping the edited stream message instead of sending a second answer bubble, so Telegram turns cannot duplicate the streamed final response. (#77947) Thanks @obviyus.
- web_search/Brave: fix provider selection when Brave is installed as an external plugin and `tools.web.search.provider: "brave"` is explicitly configured — a redundant provider re-resolution at startup could race and return an empty list, causing a spurious `WEB_SEARCH_PROVIDER_INVALID_AUTODETECT` warning and treating the explicitly configured provider as absent. Fixes #77676. Thanks @openperf.
## 2026.5.3-1

View File

@@ -234,7 +234,11 @@ export async function resolveRuntimeWebProviderSurface<
) {
configuredBundledPluginId = undefined;
}
if (params.rawProvider && !configuredBundledPluginId) {
if (
params.rawProvider &&
!configuredBundledPluginId &&
!allProviders.some((provider) => provider.id === params.rawProvider)
) {
const resolveManifestContractOwnerPluginId = await loadResolveManifestContractOwnerPluginId();
configuredBundledPluginId = resolveManifestContractOwnerPluginId({
contract: params.contract,

View File

@@ -1564,4 +1564,94 @@ describe("runtime web tools resolution", () => {
expect(resolveBundledWebFetchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled();
expect(resolvePluginWebFetchProvidersMock).not.toHaveBeenCalled();
});
describe("when brave is installed as an external plugin and explicitly configured", () => {
const externalBraveImpl = ({
value,
origin,
}: {
value: string;
origin?: string;
}): string | undefined => {
if (origin === "bundled" && value === "brave") {
return undefined;
}
return (
{
brave: "brave",
firecrawl: "firecrawl",
gemini: "google",
grok: "xai",
kimi: "moonshot",
perplexity: "perplexity",
} as Record<string, string | undefined>
)[value];
};
const defaultImpl = ({ value }: { value: string }): string | undefined =>
(
({
brave: "brave",
firecrawl: "firecrawl",
gemini: "google",
grok: "xai",
kimi: "moonshot",
perplexity: "perplexity",
}) as Record<string, string | undefined>
)[value];
beforeEach(() => {
loadInstalledPluginIndexInstallRecordsSyncMock.mockReturnValue({
brave: { source: "npm", spec: "@openclaw/brave-search" },
});
resolveManifestContractOwnerPluginIdMock.mockImplementation(externalBraveImpl);
});
afterEach(() => {
resolveManifestContractOwnerPluginIdMock.mockImplementation(defaultImpl);
});
it("selects the configured provider without re-invoking provider discovery when found in the first pass", async () => {
resolvePluginWebSearchProvidersMock
.mockReturnValueOnce(buildTestWebSearchProviders())
.mockReturnValueOnce([]);
const { metadata, context } = await runRuntimeWebTools({
config: asConfig({
tools: {
web: {
search: {
provider: "brave",
},
},
},
plugins: {
entries: {
brave: {
config: {
webSearch: {
apiKey: "brave-api-key", // pragma: allowlist secret
},
},
},
},
},
}),
});
expect(metadata.search.selectedProvider).toBe("brave");
expect(metadata.search.providerSource).toBe("configured");
expect(metadata.search.selectedProviderKeySource).toBe("config");
expect(context.warnings).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "WEB_SEARCH_PROVIDER_INVALID_AUTODETECT" }),
]),
);
expect(resolvePluginWebSearchProvidersMock).toHaveBeenCalledTimes(1);
expect(
resolveBundledExplicitWebSearchProvidersFromPublicArtifactsMock,
).not.toHaveBeenCalled();
expect(resolveBundledWebSearchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled();
});
});
});