@kodelyth/brave-plugin 2026.5.39 → 2026.5.42

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.
@@ -0,0 +1,756 @@
1
+ import fs from "node:fs";
2
+ import { validateJsonSchemaValue } from "klaw/plugin-sdk/config-schema";
3
+ import { afterAll, 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("klaw/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("../klaw.plugin.json", import.meta.url), "utf-8"),
36
+ ) as {
37
+ configSchema?: Record<string, unknown>;
38
+ };
39
+
40
+ afterAll(() => {
41
+ vi.doUnmock("klaw/plugin-sdk/runtime-env");
42
+ vi.resetModules();
43
+ });
44
+
45
+ function installBraveLlmContextFetch() {
46
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
47
+ return {
48
+ ok: true,
49
+ json: async () => ({
50
+ grounding: {
51
+ generic: [
52
+ {
53
+ url: "https://example.com/context",
54
+ title: "Context",
55
+ snippets: ["snippet"],
56
+ },
57
+ ],
58
+ },
59
+ sources: [],
60
+ }),
61
+ } as unknown as Response;
62
+ });
63
+ global.fetch = mockFetch as typeof global.fetch;
64
+ return mockFetch;
65
+ }
66
+
67
+ function readHeader(init: unknown, name: string): string | null {
68
+ const headers = (init as { headers?: HeadersInit } | undefined)?.headers;
69
+ if (!headers) {
70
+ return null;
71
+ }
72
+ return new Headers(headers).get(name);
73
+ }
74
+
75
+ function fetchCall(mockFetch: { mock: { calls: Array<Array<unknown>> } }, index = 0) {
76
+ const call = mockFetch.mock.calls[index];
77
+ if (!call) {
78
+ throw new Error(`Expected fetch call ${index + 1}`);
79
+ }
80
+ return call;
81
+ }
82
+
83
+ function fetchRequestUrl(mockFetch: { mock: { calls: Array<Array<unknown>> } }, index = 0) {
84
+ return new URL(String(fetchCall(mockFetch, index)[0]));
85
+ }
86
+
87
+ function fetchRequestInit(mockFetch: { mock: { calls: Array<Array<unknown>> } }, index = 0) {
88
+ return fetchCall(mockFetch, index)[1];
89
+ }
90
+
91
+ describe("brave web search provider", () => {
92
+ const priorFetch = global.fetch;
93
+
94
+ afterEach(() => {
95
+ vi.unstubAllEnvs();
96
+ loggerInfoMock.mockClear();
97
+ global.fetch = priorFetch;
98
+ });
99
+
100
+ it("points provider metadata at the canonical Brave docs page", () => {
101
+ expect(createBraveWebSearchProvider().docsUrl).toBe(
102
+ "https://klaw.kodelyth.com/tools/brave-search",
103
+ );
104
+ expect(createBraveWebSearchContractProvider().docsUrl).toBe(
105
+ "https://klaw.kodelyth.com/tools/brave-search",
106
+ );
107
+ });
108
+
109
+ it("exposes legacy top-level apiKey as a Brave-owned compatibility fallback", () => {
110
+ const apiKey = { source: "env", provider: "default", id: "BRAVE_API_KEY" } as const;
111
+ const config = {
112
+ tools: {
113
+ web: {
114
+ search: {
115
+ apiKey,
116
+ },
117
+ },
118
+ },
119
+ };
120
+
121
+ expect(createBraveWebSearchProvider().getConfiguredCredentialValue?.(config)).toEqual(apiKey);
122
+ expect(createBraveWebSearchContractProvider().getConfiguredCredentialValue?.(config)).toEqual(
123
+ apiKey,
124
+ );
125
+ expect(createBraveWebSearchProvider().getConfiguredCredentialFallback?.(config)).toEqual({
126
+ path: "tools.web.search.apiKey",
127
+ value: apiKey,
128
+ });
129
+ expect(
130
+ createBraveWebSearchContractProvider().getConfiguredCredentialFallback?.(config),
131
+ ).toEqual({
132
+ path: "tools.web.search.apiKey",
133
+ value: apiKey,
134
+ });
135
+ });
136
+
137
+ it("points missing-key users to fetch/browser alternatives", async () => {
138
+ vi.stubEnv("BRAVE_API_KEY", "");
139
+ const provider = createBraveWebSearchProvider();
140
+ const tool = provider.createTool({ config: {}, searchConfig: {} });
141
+ if (!tool) {
142
+ throw new Error("Expected tool definition");
143
+ }
144
+
145
+ const result = await tool.execute({ query: "Klaw docs" });
146
+
147
+ expect(result).toEqual({
148
+ error: "missing_brave_api_key",
149
+ message:
150
+ "web_search (brave) needs a Brave Search API key. Run `klaw configure --section web` to store it, or set BRAVE_API_KEY in the Gateway environment. If you do not want to configure a search API key, use web_fetch for a specific URL or the browser tool for interactive pages.",
151
+ docs: "https://klaw.kodelyth.com/tools/web",
152
+ });
153
+ });
154
+
155
+ it("normalizes brave language parameters and swaps reversed ui/search inputs", () => {
156
+ expect(
157
+ testing.normalizeBraveLanguageParams({
158
+ search_lang: "en-US",
159
+ ui_lang: "ja",
160
+ }),
161
+ ).toEqual({
162
+ search_lang: "jp",
163
+ ui_lang: "en-US",
164
+ });
165
+ expect(testing.normalizeBraveLanguageParams({ search_lang: "tr-TR", ui_lang: "tr" })).toEqual({
166
+ search_lang: "tr",
167
+ ui_lang: "tr-TR",
168
+ });
169
+ expect(testing.normalizeBraveLanguageParams({ search_lang: "EN", ui_lang: "en-us" })).toEqual({
170
+ search_lang: "en",
171
+ ui_lang: "en-US",
172
+ });
173
+ });
174
+
175
+ it("flags invalid brave language fields", () => {
176
+ expect(
177
+ testing.normalizeBraveLanguageParams({
178
+ search_lang: "xx",
179
+ }),
180
+ ).toEqual({ invalidField: "search_lang" });
181
+ expect(testing.normalizeBraveLanguageParams({ search_lang: "en-US" })).toEqual({
182
+ invalidField: "search_lang",
183
+ });
184
+ expect(testing.normalizeBraveLanguageParams({ ui_lang: "en" })).toEqual({
185
+ invalidField: "ui_lang",
186
+ });
187
+ });
188
+
189
+ it("normalizes Brave country codes and falls back unsupported values to ALL", () => {
190
+ expect(testing.normalizeBraveCountry("de")).toBe("DE");
191
+ expect(testing.normalizeBraveCountry(" VN ")).toBe("ALL");
192
+ expect(testing.normalizeBraveCountry("")).toBeUndefined();
193
+ });
194
+
195
+ it("defaults brave mode to web unless llm-context is explicitly selected", () => {
196
+ expect(testing.resolveBraveMode()).toBe("web");
197
+ expect(testing.resolveBraveMode({ mode: "llm-context" })).toBe("llm-context");
198
+ });
199
+
200
+ it("accepts llm-context in the Brave plugin config schema", () => {
201
+ if (!braveManifest.configSchema) {
202
+ throw new Error("Expected Brave manifest config schema");
203
+ }
204
+
205
+ const result = validateJsonSchemaValue({
206
+ schema: braveManifest.configSchema,
207
+ cacheKey: "test:brave-config-schema",
208
+ value: {
209
+ webSearch: {
210
+ mode: "llm-context",
211
+ },
212
+ },
213
+ });
214
+
215
+ expect(result.ok).toBe(true);
216
+ });
217
+
218
+ it("accepts baseUrl in the Brave plugin config schema", () => {
219
+ if (!braveManifest.configSchema) {
220
+ throw new Error("Expected Brave manifest config schema");
221
+ }
222
+
223
+ const result = validateJsonSchemaValue({
224
+ schema: braveManifest.configSchema,
225
+ cacheKey: "test:brave-config-schema-base-url",
226
+ value: {
227
+ webSearch: {
228
+ baseUrl: "https://api.search.brave.com/proxy",
229
+ },
230
+ },
231
+ });
232
+
233
+ expect(result.ok).toBe(true);
234
+ });
235
+
236
+ it("uses configured Brave baseUrl for web search requests", async () => {
237
+ vi.stubEnv("BRAVE_API_KEY", "");
238
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
239
+ return {
240
+ ok: true,
241
+ json: async () => ({ web: { results: [] } }),
242
+ } as unknown as Response;
243
+ });
244
+ global.fetch = mockFetch as typeof global.fetch;
245
+
246
+ const provider = createBraveWebSearchProvider();
247
+ const tool = provider.createTool({
248
+ config: {},
249
+ searchConfig: {
250
+ apiKey: "brave-test-key",
251
+ brave: {
252
+ baseUrl: "https://api.search.brave.com/proxy/",
253
+ mode: "web",
254
+ },
255
+ },
256
+ });
257
+ if (!tool) {
258
+ throw new Error("Expected tool definition");
259
+ }
260
+
261
+ await tool.execute({ query: "latest ai news" });
262
+
263
+ const requestUrl = fetchRequestUrl(mockFetch);
264
+ expect(requestUrl.origin).toBe("https://api.search.brave.com");
265
+ expect(requestUrl.pathname).toBe("/proxy/res/v1/web/search");
266
+ });
267
+
268
+ it("uses configured Brave baseUrl for llm-context requests", async () => {
269
+ vi.stubEnv("BRAVE_API_KEY", "");
270
+ const mockFetch = installBraveLlmContextFetch();
271
+ const provider = createBraveWebSearchProvider();
272
+ const tool = provider.createTool({
273
+ config: {},
274
+ searchConfig: {
275
+ apiKey: "brave-test-key",
276
+ brave: {
277
+ baseUrl: "https://api.search.brave.com/proxy",
278
+ mode: "llm-context",
279
+ },
280
+ },
281
+ });
282
+ if (!tool) {
283
+ throw new Error("Expected tool definition");
284
+ }
285
+
286
+ await tool.execute({ query: "latest ai news" });
287
+
288
+ const requestUrl = fetchRequestUrl(mockFetch);
289
+ expect(requestUrl.pathname).toBe("/proxy/res/v1/llm/context");
290
+ });
291
+
292
+ it("reports malformed Brave web search JSON as a provider error", async () => {
293
+ vi.stubEnv("BRAVE_API_KEY", "");
294
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
295
+ return {
296
+ ok: true,
297
+ json: async () => {
298
+ throw new SyntaxError("Unexpected token");
299
+ },
300
+ } as unknown as Response;
301
+ });
302
+ global.fetch = mockFetch as typeof global.fetch;
303
+
304
+ const provider = createBraveWebSearchProvider();
305
+ const tool = provider.createTool({
306
+ config: {},
307
+ searchConfig: {
308
+ apiKey: "brave-test-key",
309
+ brave: { mode: "web" },
310
+ },
311
+ });
312
+ if (!tool) {
313
+ throw new Error("Expected tool definition");
314
+ }
315
+
316
+ await expect(tool.execute({ query: "latest ai news" })).rejects.toThrow(
317
+ "Brave Search API error: malformed JSON response",
318
+ );
319
+ });
320
+
321
+ it("reports malformed Brave llm-context JSON as a provider error", async () => {
322
+ vi.stubEnv("BRAVE_API_KEY", "");
323
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
324
+ return {
325
+ ok: true,
326
+ json: async () => {
327
+ throw new SyntaxError("Unexpected token");
328
+ },
329
+ } as unknown as Response;
330
+ });
331
+ global.fetch = mockFetch as typeof global.fetch;
332
+
333
+ const provider = createBraveWebSearchProvider();
334
+ const tool = provider.createTool({
335
+ config: {},
336
+ searchConfig: {
337
+ apiKey: "brave-test-key",
338
+ brave: { mode: "llm-context" },
339
+ },
340
+ });
341
+ if (!tool) {
342
+ throw new Error("Expected tool definition");
343
+ }
344
+
345
+ await expect(tool.execute({ query: "latest ai news" })).rejects.toThrow(
346
+ "Brave LLM Context API error: malformed JSON response",
347
+ );
348
+ });
349
+
350
+ it("keeps Brave cache entries isolated by baseUrl", async () => {
351
+ vi.stubEnv("BRAVE_API_KEY", "");
352
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
353
+ return {
354
+ ok: true,
355
+ json: async () => ({ web: { results: [] } }),
356
+ } as unknown as Response;
357
+ });
358
+ global.fetch = mockFetch as typeof global.fetch;
359
+
360
+ const provider = createBraveWebSearchProvider();
361
+ const firstTool = provider.createTool({
362
+ config: {},
363
+ searchConfig: {
364
+ apiKey: "brave-test-key",
365
+ brave: {
366
+ baseUrl: "https://api.search.brave.com/proxy-one",
367
+ mode: "web",
368
+ },
369
+ },
370
+ });
371
+ const secondTool = provider.createTool({
372
+ config: {},
373
+ searchConfig: {
374
+ apiKey: "brave-test-key",
375
+ brave: {
376
+ baseUrl: "https://api.search.brave.com/proxy-two",
377
+ mode: "web",
378
+ },
379
+ },
380
+ });
381
+ if (!firstTool || !secondTool) {
382
+ throw new Error("Expected tool definitions");
383
+ }
384
+
385
+ await firstTool.execute({ query: "base url cache identity" });
386
+ await secondTool.execute({ query: "base url cache identity" });
387
+
388
+ expect(mockFetch).toHaveBeenCalledTimes(2);
389
+ expect(fetchRequestUrl(mockFetch).pathname).toBe("/proxy-one/res/v1/web/search");
390
+ expect(fetchRequestUrl(mockFetch, 1).pathname).toBe("/proxy-two/res/v1/web/search");
391
+ });
392
+
393
+ it("rejects invalid Brave mode values in the plugin config schema", () => {
394
+ if (!braveManifest.configSchema) {
395
+ throw new Error("Expected Brave manifest config schema");
396
+ }
397
+
398
+ const result = validateJsonSchemaValue({
399
+ schema: braveManifest.configSchema,
400
+ cacheKey: "test:brave-config-schema",
401
+ value: {
402
+ webSearch: {
403
+ mode: "invalid-mode",
404
+ },
405
+ },
406
+ });
407
+
408
+ expect(result.ok).toBe(false);
409
+ if (result.ok) {
410
+ return;
411
+ }
412
+ expect(result.errors).toEqual([
413
+ {
414
+ path: "webSearch.mode",
415
+ message: 'must be equal to one of the allowed values (allowed: "web", "llm-context")',
416
+ text: 'webSearch.mode: must be equal to one of the allowed values (allowed: "web", "llm-context")',
417
+ allowedValues: ["web", "llm-context"],
418
+ allowedValuesHiddenCount: 0,
419
+ },
420
+ ]);
421
+ });
422
+
423
+ it("maps llm-context results into wrapped source entries", () => {
424
+ expect(
425
+ testing.mapBraveLlmContextResults({
426
+ grounding: {
427
+ generic: [
428
+ {
429
+ url: "https://example.com/post",
430
+ title: "Example",
431
+ snippets: ["a", "", "b"],
432
+ },
433
+ ],
434
+ },
435
+ }),
436
+ ).toEqual([
437
+ {
438
+ url: "https://example.com/post",
439
+ title: "Example",
440
+ snippets: ["a", "b"],
441
+ siteName: "example.com",
442
+ },
443
+ ]);
444
+ });
445
+
446
+ it("returns validation errors for invalid date ranges", async () => {
447
+ vi.stubEnv("BRAVE_API_KEY", "");
448
+ const provider = createBraveWebSearchProvider();
449
+ const tool = provider.createTool({
450
+ config: {},
451
+ searchConfig: {
452
+ apiKey: "BSA...",
453
+ brave: { apiKey: "BSA..." },
454
+ },
455
+ });
456
+ if (!tool) {
457
+ throw new Error("Expected tool definition");
458
+ }
459
+
460
+ const result = await tool.execute({
461
+ query: "latest gpu news",
462
+ date_after: "2026-03-20",
463
+ date_before: "2026-03-01",
464
+ });
465
+
466
+ expect(result).toEqual({
467
+ error: "invalid_date_range",
468
+ message: "date_after must be before date_before.",
469
+ docs: "https://klaw.kodelyth.com/tools/web",
470
+ });
471
+ });
472
+
473
+ it("passes freshness to Brave llm-context endpoint", async () => {
474
+ vi.stubEnv("BRAVE_API_KEY", "test-key");
475
+ const mockFetch = installBraveLlmContextFetch();
476
+ const provider = createBraveWebSearchProvider();
477
+ const tool = provider.createTool({
478
+ config: {},
479
+ searchConfig: {
480
+ apiKey: "BSA...",
481
+ brave: { mode: "llm-context" },
482
+ },
483
+ });
484
+ if (!tool) {
485
+ throw new Error("Expected tool definition");
486
+ }
487
+
488
+ await tool.execute({ query: "latest ai news", freshness: "week" });
489
+
490
+ const requestUrl = fetchRequestUrl(mockFetch);
491
+ expect(requestUrl.pathname).toBe("/res/v1/llm/context");
492
+ expect(requestUrl.searchParams.get("freshness")).toBe("pw");
493
+ });
494
+
495
+ it("sends Brave web auth in the X-Subscription-Token header", async () => {
496
+ vi.stubEnv("BRAVE_API_KEY", "");
497
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
498
+ return {
499
+ ok: true,
500
+ json: async () => ({ web: { results: [] } }),
501
+ } as unknown as Response;
502
+ });
503
+ global.fetch = mockFetch as typeof global.fetch;
504
+
505
+ const provider = createBraveWebSearchProvider();
506
+ const tool = provider.createTool({
507
+ config: {},
508
+ searchConfig: {
509
+ apiKey: "brave-test-key",
510
+ brave: { mode: "web" },
511
+ },
512
+ });
513
+ if (!tool) {
514
+ throw new Error("Expected tool definition");
515
+ }
516
+
517
+ await tool.execute({ query: "latest ai news" });
518
+
519
+ const requestUrl = fetchRequestUrl(mockFetch);
520
+ expect(requestUrl.searchParams.get("apikey")).toBeNull();
521
+ expect(requestUrl.searchParams.get("key")).toBeNull();
522
+ expect(readHeader(fetchRequestInit(mockFetch), "X-Subscription-Token")).toBe("brave-test-key");
523
+ });
524
+
525
+ it("sends Brave llm-context auth in the X-Subscription-Token header", async () => {
526
+ vi.stubEnv("BRAVE_API_KEY", "");
527
+ const mockFetch = installBraveLlmContextFetch();
528
+ const provider = createBraveWebSearchProvider();
529
+ const tool = provider.createTool({
530
+ config: {},
531
+ searchConfig: {
532
+ apiKey: "brave-test-key",
533
+ brave: { mode: "llm-context" },
534
+ },
535
+ });
536
+ if (!tool) {
537
+ throw new Error("Expected tool definition");
538
+ }
539
+
540
+ await tool.execute({ query: "latest ai news" });
541
+
542
+ const requestUrl = fetchRequestUrl(mockFetch);
543
+ expect(requestUrl.searchParams.get("apikey")).toBeNull();
544
+ expect(requestUrl.searchParams.get("key")).toBeNull();
545
+ expect(readHeader(fetchRequestInit(mockFetch), "X-Subscription-Token")).toBe("brave-test-key");
546
+ });
547
+
548
+ it("passes bounded date ranges to Brave llm-context endpoint", async () => {
549
+ vi.stubEnv("BRAVE_API_KEY", "test-key");
550
+ const mockFetch = installBraveLlmContextFetch();
551
+ const provider = createBraveWebSearchProvider();
552
+ const tool = provider.createTool({
553
+ config: {},
554
+ searchConfig: {
555
+ apiKey: "BSA...",
556
+ brave: { mode: "llm-context" },
557
+ },
558
+ });
559
+ if (!tool) {
560
+ throw new Error("Expected tool definition");
561
+ }
562
+
563
+ await tool.execute({
564
+ query: "latest ai news",
565
+ date_after: "2025-01-01",
566
+ date_before: "2025-01-31",
567
+ });
568
+
569
+ const requestUrl = fetchRequestUrl(mockFetch);
570
+ expect(requestUrl.pathname).toBe("/res/v1/llm/context");
571
+ expect(requestUrl.searchParams.get("freshness")).toBe("2025-01-01to2025-01-31");
572
+ });
573
+
574
+ it("uses today as the end date for Brave llm-context date_after-only ranges", async () => {
575
+ vi.stubEnv("BRAVE_API_KEY", "test-key");
576
+ const mockFetch = installBraveLlmContextFetch();
577
+ const provider = createBraveWebSearchProvider();
578
+ const tool = provider.createTool({
579
+ config: {},
580
+ searchConfig: {
581
+ apiKey: "BSA...",
582
+ brave: { mode: "llm-context" },
583
+ },
584
+ });
585
+ if (!tool) {
586
+ throw new Error("Expected tool definition");
587
+ }
588
+
589
+ await tool.execute({ query: "latest ai news", date_after: "2025-01-01" });
590
+
591
+ const today = new Date().toISOString().slice(0, 10);
592
+ const requestUrl = fetchRequestUrl(mockFetch);
593
+ expect(requestUrl.pathname).toBe("/res/v1/llm/context");
594
+ expect(requestUrl.searchParams.get("freshness")).toBe(`2025-01-01to${today}`);
595
+ });
596
+
597
+ it("rejects future Brave llm-context date_after-only ranges before fetch", async () => {
598
+ vi.stubEnv("BRAVE_API_KEY", "test-key");
599
+ const mockFetch = installBraveLlmContextFetch();
600
+ const provider = createBraveWebSearchProvider();
601
+ const tool = provider.createTool({
602
+ config: {},
603
+ searchConfig: {
604
+ apiKey: "BSA...",
605
+ brave: { mode: "llm-context" },
606
+ },
607
+ });
608
+ if (!tool) {
609
+ throw new Error("Expected tool definition");
610
+ }
611
+
612
+ const result = await tool.execute({
613
+ query: "latest ai news",
614
+ date_after: "2999-01-01",
615
+ });
616
+
617
+ expect(result).toEqual({
618
+ error: "invalid_date_range",
619
+ message: "date_after cannot be in the future for Brave llm-context mode.",
620
+ docs: "https://klaw.kodelyth.com/tools/web",
621
+ });
622
+ expect(mockFetch).not.toHaveBeenCalled();
623
+ });
624
+
625
+ it("rejects Brave llm-context date_before-only ranges before fetch", async () => {
626
+ vi.stubEnv("BRAVE_API_KEY", "test-key");
627
+ const mockFetch = installBraveLlmContextFetch();
628
+ const provider = createBraveWebSearchProvider();
629
+ const tool = provider.createTool({
630
+ config: {},
631
+ searchConfig: {
632
+ apiKey: "BSA...",
633
+ brave: { mode: "llm-context" },
634
+ },
635
+ });
636
+ if (!tool) {
637
+ throw new Error("Expected tool definition");
638
+ }
639
+
640
+ const result = await tool.execute({
641
+ query: "latest ai news",
642
+ date_before: "2025-01-31",
643
+ });
644
+
645
+ expect(result).toEqual({
646
+ error: "unsupported_date_filter",
647
+ message:
648
+ "Brave llm-context mode requires date_after when date_before is set. Use a bounded date range or freshness.",
649
+ docs: "https://klaw.kodelyth.com/tools/web",
650
+ });
651
+ expect(mockFetch).not.toHaveBeenCalled();
652
+ });
653
+
654
+ it("falls back unsupported country values before calling Brave", async () => {
655
+ vi.stubEnv("BRAVE_API_KEY", "test-key");
656
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
657
+ return {
658
+ ok: true,
659
+ json: async () => ({ web: { results: [] } }),
660
+ } as unknown as Response;
661
+ });
662
+ global.fetch = mockFetch as typeof global.fetch;
663
+
664
+ const provider = createBraveWebSearchProvider();
665
+ const tool = provider.createTool({
666
+ config: {},
667
+ searchConfig: {
668
+ apiKey: "BSA...",
669
+ brave: { apiKey: "BSA..." },
670
+ },
671
+ });
672
+ if (!tool) {
673
+ throw new Error("Expected tool definition");
674
+ }
675
+
676
+ await tool.execute({
677
+ query: "latest Vietnam news",
678
+ country: "VN",
679
+ });
680
+
681
+ const requestUrl = fetchRequestUrl(mockFetch);
682
+ expect(requestUrl.searchParams.get("country")).toBe("ALL");
683
+ });
684
+
685
+ it("emits brave.http diagnostics for requests, responses, and cache events", async () => {
686
+ vi.stubEnv("BRAVE_API_KEY", "");
687
+ const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
688
+ return {
689
+ ok: true,
690
+ status: 200,
691
+ json: async () => ({
692
+ web: {
693
+ results: [
694
+ {
695
+ title: "Diagnostics",
696
+ url: "https://example.com/diagnostics",
697
+ description: "debug details",
698
+ },
699
+ ],
700
+ },
701
+ }),
702
+ } as unknown as Response;
703
+ });
704
+ global.fetch = mockFetch as typeof global.fetch;
705
+
706
+ const provider = createBraveWebSearchProvider();
707
+ const tool = provider.createTool({
708
+ config: { diagnostics: { flags: ["brave.http"] } },
709
+ searchConfig: {
710
+ apiKey: "brave-test-key",
711
+ brave: { mode: "web" },
712
+ },
713
+ });
714
+ if (!tool) {
715
+ throw new Error("Expected tool definition");
716
+ }
717
+
718
+ await tool.execute({ query: "unique brave diagnostics query", count: 1 });
719
+ await tool.execute({ query: "unique brave diagnostics query", count: 1 });
720
+
721
+ expect(mockFetch).toHaveBeenCalledTimes(1);
722
+ const messages = loggerInfoMock.mock.calls.map((call) => call[0]);
723
+ expect(messages).toEqual([
724
+ "brave http cache miss",
725
+ "brave http request",
726
+ "brave http response",
727
+ "brave http cache write",
728
+ "brave http cache hit",
729
+ ]);
730
+ const requestLog = loggerInfoMock.mock.calls.find(
731
+ ([message]) => message === "brave http request",
732
+ );
733
+ expect(requestLog?.[1]).toEqual({
734
+ mode: "web",
735
+ query: "unique brave diagnostics query",
736
+ params: {
737
+ count: "1",
738
+ q: "unique brave diagnostics query",
739
+ },
740
+ url: "https://api.search.brave.com/res/v1/web/search?q=unique+brave+diagnostics+query&count=1",
741
+ });
742
+ const responseLog = loggerInfoMock.mock.calls.find(
743
+ ([message]) => message === "brave http response",
744
+ );
745
+ const responsePayload = responseLog?.[1] as
746
+ | { durationMs?: unknown; mode?: unknown; ok?: unknown; status?: unknown }
747
+ | undefined;
748
+ expect(responsePayload?.mode).toBe("web");
749
+ expect(responsePayload?.status).toBe(200);
750
+ expect(responsePayload?.ok).toBe(true);
751
+ expect(typeof responsePayload?.durationMs).toBe("number");
752
+ expect(responsePayload?.durationMs).toBeGreaterThanOrEqual(0);
753
+ expect(JSON.stringify(loggerInfoMock.mock.calls)).not.toContain("brave-test-key");
754
+ expect(JSON.stringify(loggerInfoMock.mock.calls)).not.toContain("X-Subscription-Token");
755
+ });
756
+ });