@openclaw/brave-plugin 2026.5.1-beta.1 → 2026.5.2-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +7 -0
- package/package.json +3 -3
- package/src/brave-web-search-provider.runtime.ts +232 -36
- package/src/brave-web-search-provider.shared.ts +1 -0
- package/src/brave-web-search-provider.test.ts +453 -0
- package/src/brave-web-search-provider.ts +6 -2
- package/web-search-contract-api.ts +1 -1
- package/dist/.boundary-tsc.stamp +0 -1
- package/dist/.boundary-tsc.tsbuildinfo +0 -1
|
@@ -2,22 +2,105 @@ import fs from "node:fs";
|
|
|
2
2
|
import { validateJsonSchemaValue } from "openclaw/plugin-sdk/config-schema";
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { __testing } from "../test-api.js";
|
|
5
|
+
import { createBraveWebSearchProvider as createBraveWebSearchContractProvider } from "../web-search-contract-api.js";
|
|
5
6
|
import { createBraveWebSearchProvider } from "./brave-web-search-provider.js";
|
|
6
7
|
|
|
8
|
+
const loggerInfoMock = vi.hoisted(() => vi.fn());
|
|
9
|
+
|
|
10
|
+
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
|
11
|
+
createSubsystemLogger: () => ({
|
|
12
|
+
info: loggerInfoMock,
|
|
13
|
+
debug: vi.fn(),
|
|
14
|
+
warn: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
fatal: vi.fn(),
|
|
17
|
+
trace: vi.fn(),
|
|
18
|
+
raw: vi.fn(),
|
|
19
|
+
isEnabled: () => true,
|
|
20
|
+
child: () => ({
|
|
21
|
+
info: loggerInfoMock,
|
|
22
|
+
debug: vi.fn(),
|
|
23
|
+
warn: vi.fn(),
|
|
24
|
+
error: vi.fn(),
|
|
25
|
+
fatal: vi.fn(),
|
|
26
|
+
trace: vi.fn(),
|
|
27
|
+
raw: vi.fn(),
|
|
28
|
+
isEnabled: () => true,
|
|
29
|
+
child: vi.fn(),
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
7
34
|
const braveManifest = JSON.parse(
|
|
8
35
|
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
|
|
9
36
|
) as {
|
|
10
37
|
configSchema?: Record<string, unknown>;
|
|
11
38
|
};
|
|
12
39
|
|
|
40
|
+
function installBraveLlmContextFetch() {
|
|
41
|
+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
json: async () => ({
|
|
45
|
+
grounding: {
|
|
46
|
+
generic: [
|
|
47
|
+
{
|
|
48
|
+
url: "https://example.com/context",
|
|
49
|
+
title: "Context",
|
|
50
|
+
snippets: ["snippet"],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
sources: [],
|
|
55
|
+
}),
|
|
56
|
+
} as Response;
|
|
57
|
+
});
|
|
58
|
+
global.fetch = mockFetch as typeof global.fetch;
|
|
59
|
+
return mockFetch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readHeader(init: unknown, name: string): string | null {
|
|
63
|
+
const headers = (init as { headers?: HeadersInit } | undefined)?.headers;
|
|
64
|
+
if (!headers) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return new Headers(headers).get(name);
|
|
68
|
+
}
|
|
69
|
+
|
|
13
70
|
describe("brave web search provider", () => {
|
|
14
71
|
const priorFetch = global.fetch;
|
|
15
72
|
|
|
16
73
|
afterEach(() => {
|
|
17
74
|
vi.unstubAllEnvs();
|
|
75
|
+
loggerInfoMock.mockClear();
|
|
18
76
|
global.fetch = priorFetch;
|
|
19
77
|
});
|
|
20
78
|
|
|
79
|
+
it("points provider metadata at the canonical Brave docs page", () => {
|
|
80
|
+
expect(createBraveWebSearchProvider().docsUrl).toBe(
|
|
81
|
+
"https://docs.openclaw.ai/tools/brave-search",
|
|
82
|
+
);
|
|
83
|
+
expect(createBraveWebSearchContractProvider().docsUrl).toBe(
|
|
84
|
+
"https://docs.openclaw.ai/tools/brave-search",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("points missing-key users to fetch/browser alternatives", async () => {
|
|
89
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
90
|
+
const provider = createBraveWebSearchProvider();
|
|
91
|
+
const tool = provider.createTool({ config: {}, searchConfig: {} });
|
|
92
|
+
if (!tool) {
|
|
93
|
+
throw new Error("Expected tool definition");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await tool.execute({ query: "OpenClaw docs" });
|
|
97
|
+
|
|
98
|
+
expect(result).toMatchObject({
|
|
99
|
+
error: "missing_brave_api_key",
|
|
100
|
+
message: expect.stringContaining("use web_fetch for a specific URL or the browser tool"),
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
21
104
|
it("normalizes brave language parameters and swaps reversed ui/search inputs", () => {
|
|
22
105
|
expect(
|
|
23
106
|
__testing.normalizeBraveLanguageParams({
|
|
@@ -85,6 +168,127 @@ describe("brave web search provider", () => {
|
|
|
85
168
|
expect(result.ok).toBe(true);
|
|
86
169
|
});
|
|
87
170
|
|
|
171
|
+
it("accepts baseUrl in the Brave plugin config schema", () => {
|
|
172
|
+
if (!braveManifest.configSchema) {
|
|
173
|
+
throw new Error("Expected Brave manifest config schema");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result = validateJsonSchemaValue({
|
|
177
|
+
schema: braveManifest.configSchema,
|
|
178
|
+
cacheKey: "test:brave-config-schema-base-url",
|
|
179
|
+
value: {
|
|
180
|
+
webSearch: {
|
|
181
|
+
baseUrl: "https://api.search.brave.com/proxy",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(result.ok).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("uses configured Brave baseUrl for web search requests", async () => {
|
|
190
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
191
|
+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
json: async () => ({ web: { results: [] } }),
|
|
195
|
+
} as Response;
|
|
196
|
+
});
|
|
197
|
+
global.fetch = mockFetch as typeof global.fetch;
|
|
198
|
+
|
|
199
|
+
const provider = createBraveWebSearchProvider();
|
|
200
|
+
const tool = provider.createTool({
|
|
201
|
+
config: {},
|
|
202
|
+
searchConfig: {
|
|
203
|
+
apiKey: "brave-test-key",
|
|
204
|
+
brave: {
|
|
205
|
+
baseUrl: "https://api.search.brave.com/proxy/",
|
|
206
|
+
mode: "web",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
if (!tool) {
|
|
211
|
+
throw new Error("Expected tool definition");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await tool.execute({ query: "latest ai news" });
|
|
215
|
+
|
|
216
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
217
|
+
expect(requestUrl.origin).toBe("https://api.search.brave.com");
|
|
218
|
+
expect(requestUrl.pathname).toBe("/proxy/res/v1/web/search");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("uses configured Brave baseUrl for llm-context requests", async () => {
|
|
222
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
223
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
224
|
+
const provider = createBraveWebSearchProvider();
|
|
225
|
+
const tool = provider.createTool({
|
|
226
|
+
config: {},
|
|
227
|
+
searchConfig: {
|
|
228
|
+
apiKey: "brave-test-key",
|
|
229
|
+
brave: {
|
|
230
|
+
baseUrl: "https://api.search.brave.com/proxy",
|
|
231
|
+
mode: "llm-context",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
if (!tool) {
|
|
236
|
+
throw new Error("Expected tool definition");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await tool.execute({ query: "latest ai news" });
|
|
240
|
+
|
|
241
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
242
|
+
expect(requestUrl.pathname).toBe("/proxy/res/v1/llm/context");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("keeps Brave cache entries isolated by baseUrl", async () => {
|
|
246
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
247
|
+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
|
248
|
+
return {
|
|
249
|
+
ok: true,
|
|
250
|
+
json: async () => ({ web: { results: [] } }),
|
|
251
|
+
} as Response;
|
|
252
|
+
});
|
|
253
|
+
global.fetch = mockFetch as typeof global.fetch;
|
|
254
|
+
|
|
255
|
+
const provider = createBraveWebSearchProvider();
|
|
256
|
+
const firstTool = provider.createTool({
|
|
257
|
+
config: {},
|
|
258
|
+
searchConfig: {
|
|
259
|
+
apiKey: "brave-test-key",
|
|
260
|
+
brave: {
|
|
261
|
+
baseUrl: "https://api.search.brave.com/proxy-one",
|
|
262
|
+
mode: "web",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
const secondTool = provider.createTool({
|
|
267
|
+
config: {},
|
|
268
|
+
searchConfig: {
|
|
269
|
+
apiKey: "brave-test-key",
|
|
270
|
+
brave: {
|
|
271
|
+
baseUrl: "https://api.search.brave.com/proxy-two",
|
|
272
|
+
mode: "web",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
if (!firstTool || !secondTool) {
|
|
277
|
+
throw new Error("Expected tool definitions");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await firstTool.execute({ query: "base url cache identity" });
|
|
281
|
+
await secondTool.execute({ query: "base url cache identity" });
|
|
282
|
+
|
|
283
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
284
|
+
expect(new URL(String(mockFetch.mock.calls[0]?.[0])).pathname).toBe(
|
|
285
|
+
"/proxy-one/res/v1/web/search",
|
|
286
|
+
);
|
|
287
|
+
expect(new URL(String(mockFetch.mock.calls[1]?.[0])).pathname).toBe(
|
|
288
|
+
"/proxy-two/res/v1/web/search",
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
88
292
|
it("rejects invalid Brave mode values in the plugin config schema", () => {
|
|
89
293
|
if (!braveManifest.configSchema) {
|
|
90
294
|
throw new Error("Expected Brave manifest config schema");
|
|
@@ -160,6 +364,182 @@ describe("brave web search provider", () => {
|
|
|
160
364
|
});
|
|
161
365
|
});
|
|
162
366
|
|
|
367
|
+
it("passes freshness to Brave llm-context endpoint", async () => {
|
|
368
|
+
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
|
369
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
370
|
+
const provider = createBraveWebSearchProvider();
|
|
371
|
+
const tool = provider.createTool({
|
|
372
|
+
config: {},
|
|
373
|
+
searchConfig: {
|
|
374
|
+
apiKey: "BSA...",
|
|
375
|
+
brave: { mode: "llm-context" },
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
if (!tool) {
|
|
379
|
+
throw new Error("Expected tool definition");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await tool.execute({ query: "latest ai news", freshness: "week" });
|
|
383
|
+
|
|
384
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
385
|
+
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
|
386
|
+
expect(requestUrl.searchParams.get("freshness")).toBe("pw");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("sends Brave web auth in the X-Subscription-Token header", async () => {
|
|
390
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
391
|
+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
|
392
|
+
return {
|
|
393
|
+
ok: true,
|
|
394
|
+
json: async () => ({ web: { results: [] } }),
|
|
395
|
+
} as Response;
|
|
396
|
+
});
|
|
397
|
+
global.fetch = mockFetch as typeof global.fetch;
|
|
398
|
+
|
|
399
|
+
const provider = createBraveWebSearchProvider();
|
|
400
|
+
const tool = provider.createTool({
|
|
401
|
+
config: {},
|
|
402
|
+
searchConfig: {
|
|
403
|
+
apiKey: "brave-test-key",
|
|
404
|
+
brave: { mode: "web" },
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
if (!tool) {
|
|
408
|
+
throw new Error("Expected tool definition");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await tool.execute({ query: "latest ai news" });
|
|
412
|
+
|
|
413
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
414
|
+
expect(requestUrl.searchParams.get("apikey")).toBeNull();
|
|
415
|
+
expect(requestUrl.searchParams.get("key")).toBeNull();
|
|
416
|
+
expect(readHeader(mockFetch.mock.calls[0]?.[1], "X-Subscription-Token")).toBe("brave-test-key");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("sends Brave llm-context auth in the X-Subscription-Token header", async () => {
|
|
420
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
421
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
422
|
+
const provider = createBraveWebSearchProvider();
|
|
423
|
+
const tool = provider.createTool({
|
|
424
|
+
config: {},
|
|
425
|
+
searchConfig: {
|
|
426
|
+
apiKey: "brave-test-key",
|
|
427
|
+
brave: { mode: "llm-context" },
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
if (!tool) {
|
|
431
|
+
throw new Error("Expected tool definition");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
await tool.execute({ query: "latest ai news" });
|
|
435
|
+
|
|
436
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
437
|
+
expect(requestUrl.searchParams.get("apikey")).toBeNull();
|
|
438
|
+
expect(requestUrl.searchParams.get("key")).toBeNull();
|
|
439
|
+
expect(readHeader(mockFetch.mock.calls[0]?.[1], "X-Subscription-Token")).toBe("brave-test-key");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("passes bounded date ranges to Brave llm-context endpoint", async () => {
|
|
443
|
+
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
|
444
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
445
|
+
const provider = createBraveWebSearchProvider();
|
|
446
|
+
const tool = provider.createTool({
|
|
447
|
+
config: {},
|
|
448
|
+
searchConfig: {
|
|
449
|
+
apiKey: "BSA...",
|
|
450
|
+
brave: { mode: "llm-context" },
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
if (!tool) {
|
|
454
|
+
throw new Error("Expected tool definition");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await tool.execute({
|
|
458
|
+
query: "latest ai news",
|
|
459
|
+
date_after: "2025-01-01",
|
|
460
|
+
date_before: "2025-01-31",
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
464
|
+
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
|
465
|
+
expect(requestUrl.searchParams.get("freshness")).toBe("2025-01-01to2025-01-31");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("uses today as the end date for Brave llm-context date_after-only ranges", async () => {
|
|
469
|
+
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
|
470
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
471
|
+
const provider = createBraveWebSearchProvider();
|
|
472
|
+
const tool = provider.createTool({
|
|
473
|
+
config: {},
|
|
474
|
+
searchConfig: {
|
|
475
|
+
apiKey: "BSA...",
|
|
476
|
+
brave: { mode: "llm-context" },
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
if (!tool) {
|
|
480
|
+
throw new Error("Expected tool definition");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
await tool.execute({ query: "latest ai news", date_after: "2025-01-01" });
|
|
484
|
+
|
|
485
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
486
|
+
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
487
|
+
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
|
488
|
+
expect(requestUrl.searchParams.get("freshness")).toBe(`2025-01-01to${today}`);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("rejects future Brave llm-context date_after-only ranges before fetch", async () => {
|
|
492
|
+
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
|
493
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
494
|
+
const provider = createBraveWebSearchProvider();
|
|
495
|
+
const tool = provider.createTool({
|
|
496
|
+
config: {},
|
|
497
|
+
searchConfig: {
|
|
498
|
+
apiKey: "BSA...",
|
|
499
|
+
brave: { mode: "llm-context" },
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
if (!tool) {
|
|
503
|
+
throw new Error("Expected tool definition");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const result = await tool.execute({
|
|
507
|
+
query: "latest ai news",
|
|
508
|
+
date_after: "2999-01-01",
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
expect(result).toMatchObject({
|
|
512
|
+
error: "invalid_date_range",
|
|
513
|
+
});
|
|
514
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("rejects Brave llm-context date_before-only ranges before fetch", async () => {
|
|
518
|
+
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
|
519
|
+
const mockFetch = installBraveLlmContextFetch();
|
|
520
|
+
const provider = createBraveWebSearchProvider();
|
|
521
|
+
const tool = provider.createTool({
|
|
522
|
+
config: {},
|
|
523
|
+
searchConfig: {
|
|
524
|
+
apiKey: "BSA...",
|
|
525
|
+
brave: { mode: "llm-context" },
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
if (!tool) {
|
|
529
|
+
throw new Error("Expected tool definition");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const result = await tool.execute({
|
|
533
|
+
query: "latest ai news",
|
|
534
|
+
date_before: "2025-01-31",
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
expect(result).toMatchObject({
|
|
538
|
+
error: "unsupported_date_filter",
|
|
539
|
+
});
|
|
540
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
541
|
+
});
|
|
542
|
+
|
|
163
543
|
it("falls back unsupported country values before calling Brave", async () => {
|
|
164
544
|
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
|
165
545
|
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
|
@@ -190,4 +570,77 @@ describe("brave web search provider", () => {
|
|
|
190
570
|
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
|
191
571
|
expect(requestUrl.searchParams.get("country")).toBe("ALL");
|
|
192
572
|
});
|
|
573
|
+
|
|
574
|
+
it("emits brave.http diagnostics for requests, responses, and cache events", async () => {
|
|
575
|
+
vi.stubEnv("BRAVE_API_KEY", "");
|
|
576
|
+
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
status: 200,
|
|
580
|
+
json: async () => ({
|
|
581
|
+
web: {
|
|
582
|
+
results: [
|
|
583
|
+
{
|
|
584
|
+
title: "Diagnostics",
|
|
585
|
+
url: "https://example.com/diagnostics",
|
|
586
|
+
description: "debug details",
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
}),
|
|
591
|
+
} as Response;
|
|
592
|
+
});
|
|
593
|
+
global.fetch = mockFetch as typeof global.fetch;
|
|
594
|
+
|
|
595
|
+
const provider = createBraveWebSearchProvider();
|
|
596
|
+
const tool = provider.createTool({
|
|
597
|
+
config: { diagnostics: { flags: ["brave.http"] } },
|
|
598
|
+
searchConfig: {
|
|
599
|
+
apiKey: "brave-test-key",
|
|
600
|
+
brave: { mode: "web" },
|
|
601
|
+
},
|
|
602
|
+
});
|
|
603
|
+
if (!tool) {
|
|
604
|
+
throw new Error("Expected tool definition");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await tool.execute({ query: "unique brave diagnostics query", count: 1 });
|
|
608
|
+
await tool.execute({ query: "unique brave diagnostics query", count: 1 });
|
|
609
|
+
|
|
610
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
611
|
+
const messages = loggerInfoMock.mock.calls.map((call) => call[0]);
|
|
612
|
+
expect(messages).toEqual(
|
|
613
|
+
expect.arrayContaining([
|
|
614
|
+
"brave http cache miss",
|
|
615
|
+
"brave http request",
|
|
616
|
+
"brave http response",
|
|
617
|
+
"brave http cache write",
|
|
618
|
+
"brave http cache hit",
|
|
619
|
+
]),
|
|
620
|
+
);
|
|
621
|
+
expect(loggerInfoMock.mock.calls).toEqual(
|
|
622
|
+
expect.arrayContaining([
|
|
623
|
+
[
|
|
624
|
+
"brave http request",
|
|
625
|
+
expect.objectContaining({
|
|
626
|
+
mode: "web",
|
|
627
|
+
query: "unique brave diagnostics query",
|
|
628
|
+
params: expect.objectContaining({ q: "unique brave diagnostics query", count: "1" }),
|
|
629
|
+
url: expect.stringContaining("api.search.brave.com/res/v1/web/search"),
|
|
630
|
+
}),
|
|
631
|
+
],
|
|
632
|
+
[
|
|
633
|
+
"brave http response",
|
|
634
|
+
expect.objectContaining({
|
|
635
|
+
mode: "web",
|
|
636
|
+
status: 200,
|
|
637
|
+
ok: true,
|
|
638
|
+
durationMs: expect.any(Number),
|
|
639
|
+
}),
|
|
640
|
+
],
|
|
641
|
+
]),
|
|
642
|
+
);
|
|
643
|
+
expect(JSON.stringify(loggerInfoMock.mock.calls)).not.toContain("brave-test-key");
|
|
644
|
+
expect(JSON.stringify(loggerInfoMock.mock.calls)).not.toContain("X-Subscription-Token");
|
|
645
|
+
});
|
|
193
646
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
|
|
1
2
|
import type {
|
|
2
3
|
SearchConfigRecord,
|
|
3
4
|
WebSearchProviderPlugin,
|
|
@@ -111,8 +112,10 @@ function resolveBraveMode(searchConfig?: Record<string, unknown>): "web" | "llm-
|
|
|
111
112
|
|
|
112
113
|
function createBraveToolDefinition(
|
|
113
114
|
searchConfig?: SearchConfigRecord,
|
|
115
|
+
config?: Parameters<typeof isDiagnosticFlagEnabled>[1],
|
|
114
116
|
): WebSearchProviderToolDefinition {
|
|
115
117
|
const braveMode = resolveBraveMode(searchConfig);
|
|
118
|
+
const diagnosticsEnabled = isDiagnosticFlagEnabled("brave.http", config);
|
|
116
119
|
|
|
117
120
|
return {
|
|
118
121
|
description:
|
|
@@ -122,7 +125,7 @@ function createBraveToolDefinition(
|
|
|
122
125
|
parameters: BraveSearchSchema,
|
|
123
126
|
execute: async (args) => {
|
|
124
127
|
const { executeBraveSearch } = await loadBraveWebSearchRuntime();
|
|
125
|
-
return await executeBraveSearch(args, searchConfig);
|
|
128
|
+
return await executeBraveSearch(args, searchConfig, { diagnosticsEnabled });
|
|
126
129
|
},
|
|
127
130
|
};
|
|
128
131
|
}
|
|
@@ -137,7 +140,7 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
|
|
137
140
|
envVars: ["BRAVE_API_KEY"],
|
|
138
141
|
placeholder: "BSA...",
|
|
139
142
|
signupUrl: "https://brave.com/search/api/",
|
|
140
|
-
docsUrl: "https://docs.openclaw.ai/brave-search",
|
|
143
|
+
docsUrl: "https://docs.openclaw.ai/tools/brave-search",
|
|
141
144
|
autoDetectOrder: 10,
|
|
142
145
|
credentialPath: BRAVE_CREDENTIAL_PATH,
|
|
143
146
|
...createWebSearchProviderContractFields({
|
|
@@ -153,6 +156,7 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
|
|
153
156
|
resolveProviderWebSearchPluginConfig(ctx.config, "brave"),
|
|
154
157
|
{ mirrorApiKeyToTopLevel: true },
|
|
155
158
|
),
|
|
159
|
+
ctx.config,
|
|
156
160
|
),
|
|
157
161
|
};
|
|
158
162
|
}
|
|
@@ -15,7 +15,7 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
|
|
15
15
|
envVars: ["BRAVE_API_KEY"],
|
|
16
16
|
placeholder: "BSA...",
|
|
17
17
|
signupUrl: "https://brave.com/search/api/",
|
|
18
|
-
docsUrl: "https://docs.openclaw.ai/brave-search",
|
|
18
|
+
docsUrl: "https://docs.openclaw.ai/tools/brave-search",
|
|
19
19
|
autoDetectOrder: 10,
|
|
20
20
|
credentialPath,
|
|
21
21
|
...createWebSearchProviderContractFields({
|
package/dist/.boundary-tsc.stamp
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2026-04-29T11:28:47.129Z
|