@openclaw/brave-plugin 2026.5.2-beta.2 → 2026.5.3-beta.1

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.
@@ -1,646 +0,0 @@
1
- import fs from "node:fs";
2
- import { validateJsonSchemaValue } from "openclaw/plugin-sdk/config-schema";
3
- import { afterEach, describe, expect, it, vi } from "vitest";
4
- import { __testing } from "../test-api.js";
5
- import { createBraveWebSearchProvider as createBraveWebSearchContractProvider } from "../web-search-contract-api.js";
6
- import { createBraveWebSearchProvider } from "./brave-web-search-provider.js";
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
-
34
- const braveManifest = JSON.parse(
35
- fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
36
- ) as {
37
- configSchema?: Record<string, unknown>;
38
- };
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
-
70
- describe("brave web search provider", () => {
71
- const priorFetch = global.fetch;
72
-
73
- afterEach(() => {
74
- vi.unstubAllEnvs();
75
- loggerInfoMock.mockClear();
76
- global.fetch = priorFetch;
77
- });
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
-
104
- it("normalizes brave language parameters and swaps reversed ui/search inputs", () => {
105
- expect(
106
- __testing.normalizeBraveLanguageParams({
107
- search_lang: "en-US",
108
- ui_lang: "ja",
109
- }),
110
- ).toEqual({
111
- search_lang: "jp",
112
- ui_lang: "en-US",
113
- });
114
- expect(__testing.normalizeBraveLanguageParams({ search_lang: "tr-TR", ui_lang: "tr" })).toEqual(
115
- {
116
- search_lang: "tr",
117
- ui_lang: "tr-TR",
118
- },
119
- );
120
- expect(__testing.normalizeBraveLanguageParams({ search_lang: "EN", ui_lang: "en-us" })).toEqual(
121
- {
122
- search_lang: "en",
123
- ui_lang: "en-US",
124
- },
125
- );
126
- });
127
-
128
- it("flags invalid brave language fields", () => {
129
- expect(
130
- __testing.normalizeBraveLanguageParams({
131
- search_lang: "xx",
132
- }),
133
- ).toEqual({ invalidField: "search_lang" });
134
- expect(__testing.normalizeBraveLanguageParams({ search_lang: "en-US" })).toEqual({
135
- invalidField: "search_lang",
136
- });
137
- expect(__testing.normalizeBraveLanguageParams({ ui_lang: "en" })).toEqual({
138
- invalidField: "ui_lang",
139
- });
140
- });
141
-
142
- it("normalizes Brave country codes and falls back unsupported values to ALL", () => {
143
- expect(__testing.normalizeBraveCountry("de")).toBe("DE");
144
- expect(__testing.normalizeBraveCountry(" VN ")).toBe("ALL");
145
- expect(__testing.normalizeBraveCountry("")).toBeUndefined();
146
- });
147
-
148
- it("defaults brave mode to web unless llm-context is explicitly selected", () => {
149
- expect(__testing.resolveBraveMode()).toBe("web");
150
- expect(__testing.resolveBraveMode({ mode: "llm-context" })).toBe("llm-context");
151
- });
152
-
153
- it("accepts llm-context in the Brave plugin config schema", () => {
154
- if (!braveManifest.configSchema) {
155
- throw new Error("Expected Brave manifest config schema");
156
- }
157
-
158
- const result = validateJsonSchemaValue({
159
- schema: braveManifest.configSchema,
160
- cacheKey: "test:brave-config-schema",
161
- value: {
162
- webSearch: {
163
- mode: "llm-context",
164
- },
165
- },
166
- });
167
-
168
- expect(result.ok).toBe(true);
169
- });
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
-
292
- it("rejects invalid Brave mode values in the plugin config schema", () => {
293
- if (!braveManifest.configSchema) {
294
- throw new Error("Expected Brave manifest config schema");
295
- }
296
-
297
- const result = validateJsonSchemaValue({
298
- schema: braveManifest.configSchema,
299
- cacheKey: "test:brave-config-schema",
300
- value: {
301
- webSearch: {
302
- mode: "invalid-mode",
303
- },
304
- },
305
- });
306
-
307
- expect(result.ok).toBe(false);
308
- if (result.ok) {
309
- return;
310
- }
311
- expect(result.errors).toContainEqual(
312
- expect.objectContaining({
313
- path: "webSearch.mode",
314
- allowedValues: ["web", "llm-context"],
315
- }),
316
- );
317
- });
318
-
319
- it("maps llm-context results into wrapped source entries", () => {
320
- expect(
321
- __testing.mapBraveLlmContextResults({
322
- grounding: {
323
- generic: [
324
- {
325
- url: "https://example.com/post",
326
- title: "Example",
327
- snippets: ["a", "", "b"],
328
- },
329
- ],
330
- },
331
- }),
332
- ).toEqual([
333
- {
334
- url: "https://example.com/post",
335
- title: "Example",
336
- snippets: ["a", "b"],
337
- siteName: "example.com",
338
- },
339
- ]);
340
- });
341
-
342
- it("returns validation errors for invalid date ranges", async () => {
343
- vi.stubEnv("BRAVE_API_KEY", "");
344
- const provider = createBraveWebSearchProvider();
345
- const tool = provider.createTool({
346
- config: {},
347
- searchConfig: {
348
- apiKey: "BSA...",
349
- brave: { apiKey: "BSA..." },
350
- },
351
- });
352
- if (!tool) {
353
- throw new Error("Expected tool definition");
354
- }
355
-
356
- const result = await tool.execute({
357
- query: "latest gpu news",
358
- date_after: "2026-03-20",
359
- date_before: "2026-03-01",
360
- });
361
-
362
- expect(result).toMatchObject({
363
- error: "invalid_date_range",
364
- });
365
- });
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
-
543
- it("falls back unsupported country values before calling Brave", async () => {
544
- vi.stubEnv("BRAVE_API_KEY", "test-key");
545
- const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
546
- return {
547
- ok: true,
548
- json: async () => ({ web: { results: [] } }),
549
- } as Response;
550
- });
551
- global.fetch = mockFetch as typeof global.fetch;
552
-
553
- const provider = createBraveWebSearchProvider();
554
- const tool = provider.createTool({
555
- config: {},
556
- searchConfig: {
557
- apiKey: "BSA...",
558
- brave: { apiKey: "BSA..." },
559
- },
560
- });
561
- if (!tool) {
562
- throw new Error("Expected tool definition");
563
- }
564
-
565
- await tool.execute({
566
- query: "latest Vietnam news",
567
- country: "VN",
568
- });
569
-
570
- const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
571
- expect(requestUrl.searchParams.get("country")).toBe("ALL");
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
- });
646
- });