fix(cron): detect configured Brave search during preflight (#108636)

* fix(cron): use secrets config for web search preflight

* fix(cron): reuse prepared web search provider

Co-authored-by: Hosan <200961079+hosanxiv@users.noreply.github.com>

---------

Co-authored-by: Hosan <200961079+hosanxiv@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Hosan
2026-07-16 15:03:41 +08:00
committed by GitHub
parent 66b09405d0
commit 7bf6bf3150
5 changed files with 127 additions and 37 deletions
+7 -8
View File
@@ -95,8 +95,7 @@ export const getChannelPluginMock = createMock();
export const retireSessionMcpRuntimeMock = createMock();
export const callGatewayMock = createMock();
export const ensureRuntimePluginsLoadedMock = createMock();
export const listWebSearchProvidersMock = createMock();
export const resolveWebSearchProviderIdMock = createMock();
export const hasUsableWebSearchProviderMock = createMock();
export const classifyEmbeddedAgentRunResultForModelFallbackMock = createMock();
export const mergeEmbeddedAgentRunResultForModelFallbackExhaustionMock = createMock();
@@ -187,8 +186,7 @@ vi.mock("../../plugins/runtime-plugins.runtime.js", () => ({
}));
vi.mock("../../web-search/runtime.js", () => ({
listWebSearchProviders: listWebSearchProvidersMock,
resolveWebSearchProviderId: resolveWebSearchProviderIdMock,
hasUsableWebSearchProvider: hasUsableWebSearchProviderMock,
}));
vi.mock("../../skills/runtime/cron-snapshot.runtime.js", () => ({
@@ -814,10 +812,11 @@ export function resetRunCronIsolatedAgentTurnHarness(): void {
setSessionRuntimeModelMock.mockReturnValue(undefined);
logWarnMock.mockReset();
ensureRuntimePluginsLoadedMock.mockReset();
listWebSearchProvidersMock.mockReset();
listWebSearchProvidersMock.mockReturnValue([{ id: "duckduckgo" }]);
resolveWebSearchProviderIdMock.mockReset();
resolveWebSearchProviderIdMock.mockReturnValue("duckduckgo");
hasUsableWebSearchProviderMock.mockReset();
hasUsableWebSearchProviderMock.mockImplementation(
(params?: { runtimeWebSearch?: { selectedProvider?: string } }) =>
Boolean(params?.runtimeWebSearch?.selectedProvider),
);
}
export function clearFastTestEnv(): string | undefined {
+51 -10
View File
@@ -2,13 +2,16 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "../../agents/test-helpers/fast-coding-tools.js";
import {
listWebSearchProvidersMock,
clearActiveRuntimeWebToolsMetadata,
setActiveRuntimeWebToolsMetadata,
} from "../../secrets/runtime-web-tools-state.js";
import {
hasUsableWebSearchProviderMock,
loadModelCatalogMock,
loadRunCronIsolatedAgentTurn,
resolveConfiguredModelRefMock,
resetRunCronIsolatedAgentTurnHarness,
resolveDeliveryTargetMock,
resolveWebSearchProviderIdMock,
runEmbeddedAgentMock,
runWithModelFallbackMock,
} from "./run.test-harness.js";
@@ -93,6 +96,7 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => {
previousFastTestEnv = process.env.OPENCLAW_TEST_FAST;
vi.stubEnv("OPENCLAW_TEST_FAST", "1");
resetRunCronIsolatedAgentTurnHarness();
clearActiveRuntimeWebToolsMetadata();
resolveDeliveryTargetMock.mockResolvedValue({
channel: "forum",
to: "123",
@@ -106,6 +110,7 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => {
});
afterEach(() => {
clearActiveRuntimeWebToolsMetadata();
if (previousFastTestEnv == null) {
vi.unstubAllEnvs();
delete process.env.OPENCLAW_TEST_FAST;
@@ -156,9 +161,6 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => {
"adds cron diagnostics when web_search is allowed without a selected provider",
{ timeout: RUN_TOOLS_ALLOW_TIMEOUT_MS },
async () => {
listWebSearchProvidersMock.mockReturnValue([{ id: "duckduckgo" }]);
resolveWebSearchProviderIdMock.mockReturnValue("");
const result = await runCronIsolatedAgentTurn(makeParamsWithToolsAllow(["web_search"]));
expect(result.status).toBe("ok");
@@ -178,12 +180,54 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => {
},
);
it(
"uses the prepared provider selected from a plugin-scoped web search key",
{ timeout: RUN_TOOLS_ALLOW_TIMEOUT_MS },
async () => {
setActiveRuntimeWebToolsMetadata({
search: {
providerSource: "auto-detect",
selectedProvider: "brave",
selectedProviderKeySource: "config",
diagnostics: [],
},
fetch: { providerSource: "none", diagnostics: [] },
diagnostics: [],
});
const cfg = {
plugins: {
entries: {
brave: {
enabled: true,
config: {
webSearch: { apiKey: "token-oversized" },
},
},
},
},
};
const result = await runCronIsolatedAgentTurn({
...makeParamsWithToolsAllow(["web_search"]),
cfg,
});
expect(result.status).toBe("ok");
expect(result.diagnostics).toBeUndefined();
expect(hasUsableWebSearchProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
agentDir: "/tmp/agent-dir",
preferRuntimeProviders: true,
runtimeWebSearch: expect.objectContaining({ selectedProvider: "brave" }),
}),
);
},
);
it(
"does not warn for default-derived toolsAllow that includes web_search",
{ timeout: RUN_TOOLS_ALLOW_TIMEOUT_MS },
async () => {
listWebSearchProvidersMock.mockReturnValue([]);
const result = await runCronIsolatedAgentTurn(
makeParamsWithDefaultToolsAllow(["web_search"]),
);
@@ -197,7 +241,6 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => {
"does not warn when native web_search suppresses the managed provider tool",
{ timeout: RUN_TOOLS_ALLOW_TIMEOUT_MS },
async () => {
listWebSearchProvidersMock.mockReturnValue([]);
resolveConfiguredModelRefMock.mockReturnValue({
provider: "gateway",
model: "gpt-5.5",
@@ -237,8 +280,6 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => {
"keeps web_search provider diagnostics when the run aborts",
{ timeout: RUN_TOOLS_ALLOW_TIMEOUT_MS },
async () => {
listWebSearchProvidersMock.mockReturnValue([]);
resolveWebSearchProviderIdMock.mockReturnValue("");
runWithModelFallbackMock.mockResolvedValueOnce({
result: {
payloads: [],
+23 -10
View File
@@ -155,6 +155,9 @@ const runtimePluginsLoader = createLazyImportLoader(
const codexNativeWebSearchLoader = createLazyImportLoader(
() => import("../../agents/codex-native-web-search.js"),
);
const webToolRuntimeContextLoader = createLazyImportLoader(
() => import("../../agents/tools/web-tool-runtime-context.js"),
);
const webSearchRuntimeLoader = createLazyImportLoader(() => import("../../web-search/runtime.js"));
async function loadSessionAccessorRuntime() {
@@ -197,6 +200,10 @@ async function loadCodexNativeWebSearch() {
return await codexNativeWebSearchLoader.load();
}
async function loadWebToolRuntimeContext() {
return await webToolRuntimeContextLoader.load();
}
async function loadWebSearchRuntime() {
return await webSearchRuntimeLoader.load();
}
@@ -389,21 +396,27 @@ async function createCronToolsAllowPreflightDiagnostics(params: {
) {
return undefined;
}
const { listWebSearchProviders, resolveWebSearchProviderId } = await loadWebSearchRuntime();
const webSearchProviders = listWebSearchProviders({ config: params.cfg });
const { resolveWebSearchToolRuntimeContext } = await loadWebToolRuntimeContext();
const { config, preferRuntimeProviders, runtimeWebSearch } = resolveWebSearchToolRuntimeContext(
{
config: params.cfg,
lateBindRuntimeConfig: true,
},
);
const { hasUsableWebSearchProvider } = await loadWebSearchRuntime();
const hasWebSearchProvider = hasUsableWebSearchProvider({
config,
agentDir: params.agentDir,
runtimeWebSearch,
preferRuntimeProviders,
});
return createCronRunDiagnosticsFromMissingWebSearchProvider({
toolsAllow,
hasWebSearchProvider: Boolean(
resolveWebSearchProviderId({
config: params.cfg,
agentDir: params.agentDir,
providers: webSearchProviders,
}),
),
hasWebSearchProvider,
});
} catch (error) {
logWarn(
`[cron:${params.jobId}] Failed to inspect web_search providers for toolsAllow diagnostics: ${String(error)}`,
`[cron:${params.jobId}] Failed to inspect web_search provider state for toolsAllow diagnostics: ${String(error)}`,
);
return undefined;
}
+36 -9
View File
@@ -165,6 +165,7 @@ function createDuckDuckGoSearchProvider(
}
describe("web search runtime", () => {
let hasUsableWebSearchProvider: typeof import("./runtime.js").hasUsableWebSearchProvider;
let runWebSearch: typeof import("./runtime.js").runWebSearch;
let activateSecretsRuntimeSnapshot: typeof import("../secrets/runtime.js").activateSecretsRuntimeSnapshot;
let clearSecretsRuntimeSnapshot: typeof import("../secrets/runtime.js").clearSecretsRuntimeSnapshot;
@@ -172,7 +173,7 @@ describe("web search runtime", () => {
const tempDirs: string[] = [];
beforeAll(async () => {
({ runWebSearch } = await import("./runtime.js"));
({ hasUsableWebSearchProvider, runWebSearch } = await import("./runtime.js"));
({ activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } =
await import("../secrets/runtime.js"));
({ setRuntimeConfigSnapshot } = await import("../config/config.js"));
@@ -222,6 +223,23 @@ describe("web search runtime", () => {
});
});
it("accepts the prepared provider selection without rediscovering providers", () => {
expect(
hasUsableWebSearchProvider({
config: {},
runtimeWebSearch: {
providerSource: "auto-detect",
selectedProvider: "brave",
selectedProviderKeySource: "config",
diagnostics: [],
},
preferRuntimeProviders: true,
}),
).toBe(true);
expect(resolveRuntimeWebSearchProvidersMock).not.toHaveBeenCalled();
expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled();
});
it("passes the run abort signal to provider execution", async () => {
const controller = new AbortController();
const execute = vi.fn(
@@ -474,18 +492,27 @@ describe("web search runtime", () => {
provider,
createDuckDuckGoSearchProvider(),
]);
const config = {
agents: {
list: [
{ id: "main", default: true, agentDir: defaultAgentDir },
{ id: "side", agentDir: activeAgentDir },
],
},
} satisfies OpenClawConfig;
expect(
hasUsableWebSearchProvider({
agentDir: activeAgentDir,
config,
preferRuntimeProviders: true,
}),
).toBe(true);
await expect(
runWebSearch({
agentDir: activeAgentDir,
config: {
agents: {
list: [
{ id: "main", default: true, agentDir: defaultAgentDir },
{ id: "side", agentDir: activeAgentDir },
],
},
},
config,
args: { query: "active-agent oauth-backed web search" },
}),
).resolves.toEqual({
+10
View File
@@ -406,6 +406,16 @@ function resolveWebSearchCandidates(
return orderedProviders;
}
/** Reports whether web_search can use the prepared selection or resolve an agent-scoped provider. */
export function hasUsableWebSearchProvider(options?: ResolveWebSearchDefinitionParams): boolean {
// Prepared metadata owns config/secret selection. Candidate resolution remains necessary for
// credentials scoped to the active agent, such as provider auth profiles.
if (normalizeOptionalLowercaseString(options?.runtimeWebSearch?.selectedProvider)) {
return true;
}
return resolveWebSearchCandidates(options).length > 0;
}
function hasExplicitWebSearchSelection(params: {
search?: WebSearchConfig;
runtimeWebSearch?: RuntimeWebSearchMetadata;