mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
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:
@@ -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 {
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user