@kognitivedev/cloud-web-search 0.2.28

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,784 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { HttpTransport } from "@kognitivedev/client-core";
3
+ import {
4
+ CloudWebSearchValidationError,
5
+ KognitiveCloudWebSearchClient,
6
+ toCloudWebSearchRichEvents,
7
+ } from "../index";
8
+
9
+ function toSSEPayload(events: Array<{
10
+ event?: string;
11
+ id?: string;
12
+ retry?: number;
13
+ data: unknown;
14
+ }>) {
15
+ return events.map((entry) => {
16
+ const lines: string[] = [];
17
+ if (entry.id) {
18
+ lines.push(`id: ${entry.id}`);
19
+ }
20
+ if (entry.retry !== undefined) {
21
+ lines.push(`retry: ${entry.retry}`);
22
+ }
23
+ if (entry.event) {
24
+ lines.push(`event: ${entry.event}`);
25
+ }
26
+ lines.push(`data: ${JSON.stringify(entry.data)}`);
27
+ lines.push("");
28
+ return lines.join("\n");
29
+ }).join("\n");
30
+ }
31
+
32
+ function toReadableStream(payload: string) {
33
+ const encoder = new TextEncoder();
34
+ return new ReadableStream<Uint8Array>({
35
+ start(controller) {
36
+ controller.enqueue(encoder.encode(payload));
37
+ controller.close();
38
+ },
39
+ });
40
+ }
41
+
42
+ async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
43
+ const values: T[] = [];
44
+ for await (const value of iterable) {
45
+ values.push(value);
46
+ }
47
+ return values;
48
+ }
49
+
50
+ function createProgress(overrides: Partial<Record<keyof ReturnType<typeof baseProgress>, unknown>> = {}) {
51
+ return {
52
+ ...baseProgress(),
53
+ ...overrides,
54
+ };
55
+ }
56
+
57
+ function baseProgress() {
58
+ return {
59
+ stage: "queued",
60
+ message: "Web-search job created",
61
+ timestamp: "2026-04-22T10:00:00.000Z",
62
+ };
63
+ }
64
+
65
+ function createJob(overrides: Record<string, unknown> = {}) {
66
+ return {
67
+ id: "job_123",
68
+ projectId: "project_123",
69
+ mode: "search",
70
+ searchQuery: "latest Bun release notes",
71
+ searchInstructions: null,
72
+ responseInstructions: null,
73
+ responseSchema: null,
74
+ researchPlan: null,
75
+ parameters: {},
76
+ status: "queued",
77
+ results: null,
78
+ progress: [],
79
+ errorMessage: null,
80
+ cancelRequestedAt: null,
81
+ cancelledAt: null,
82
+ createdAt: "2026-04-22T10:00:00.000Z",
83
+ updatedAt: "2026-04-22T10:00:00.000Z",
84
+ ...overrides,
85
+ };
86
+ }
87
+
88
+ function createJobEvent(overrides: Record<string, unknown> = {}) {
89
+ return {
90
+ id: "evt_1",
91
+ jobId: "job_123",
92
+ projectId: "project_123",
93
+ eventType: "job.started",
94
+ stage: "dispatching",
95
+ status: "in_progress",
96
+ message: "Queued web-search job for execution",
97
+ payload: {},
98
+ createdAt: "2026-04-22T10:00:01.000Z",
99
+ ...overrides,
100
+ };
101
+ }
102
+
103
+ function parseSummary(value: unknown): { summary: string; markdown?: string } {
104
+ if (!value || typeof value !== "object" || Array.isArray(value) || typeof (value as { summary?: unknown }).summary !== "string") {
105
+ throw new Error("Expected { summary: string }");
106
+ }
107
+ return {
108
+ summary: (value as { summary: string }).summary,
109
+ ...((value as { markdown?: unknown }).markdown && typeof (value as { markdown?: unknown }).markdown === "string"
110
+ ? { markdown: (value as { markdown: string }).markdown }
111
+ : {}),
112
+ };
113
+ }
114
+
115
+ describe("@kognitivedev/cloud-web-search client", () => {
116
+ it("validates outgoing create inputs and decodes list/create responses", async () => {
117
+ const json = vi.fn()
118
+ .mockResolvedValueOnce({
119
+ jobs: [createJob()],
120
+ })
121
+ .mockResolvedValueOnce(createJob({
122
+ id: "job_456",
123
+ status: "queued",
124
+ parameters: { maxPages: 4 },
125
+ }));
126
+ const transport = {
127
+ baseUrl: "https://api.kognitive.dev",
128
+ json,
129
+ raw: vi.fn(),
130
+ poll: vi.fn(),
131
+ } as unknown as HttpTransport;
132
+
133
+ const client = new KognitiveCloudWebSearchClient(transport);
134
+
135
+ await expect(client.jobs.list()).resolves.toEqual([createJob()]);
136
+ await expect(client.jobs.createSearch("latest Bun release notes", { maxPages: 4 })).resolves.toMatchObject({
137
+ id: "job_456",
138
+ projectId: "project_123",
139
+ status: "queued",
140
+ parameters: { maxPages: 4 },
141
+ });
142
+
143
+ await expect(client.jobs.create({
144
+ mode: "search",
145
+ query: " ",
146
+ })).rejects.toBeInstanceOf(CloudWebSearchValidationError);
147
+
148
+ expect(json).toHaveBeenNthCalledWith(1, "/api/cloud/web-search/jobs");
149
+ expect(json).toHaveBeenNthCalledWith(2, "/api/cloud/web-search/jobs", {
150
+ method: "POST",
151
+ body: JSON.stringify({
152
+ mode: "search",
153
+ query: "latest Bun release notes",
154
+ parameters: { maxPages: 4 },
155
+ }),
156
+ });
157
+ });
158
+
159
+ it("waits for completion and decodes structured output with parseOutput", async () => {
160
+ const json = vi.fn().mockResolvedValue({
161
+ jobId: "job_123",
162
+ status: "completed",
163
+ result: {
164
+ output: { summary: "done" },
165
+ text: "done",
166
+ sources: [{ title: "Source", url: "https://example.com/source", faviconUrl: "https://example.com/favicon.ico" }],
167
+ },
168
+ });
169
+ const poll = vi.fn().mockResolvedValue(createJob({
170
+ status: "completed",
171
+ mode: "research",
172
+ searchQuery: null,
173
+ searchInstructions: "Research Bun updates",
174
+ responseSchema: {
175
+ type: "object",
176
+ properties: {
177
+ summary: { type: "string" },
178
+ },
179
+ },
180
+ }));
181
+ const transport = {
182
+ baseUrl: "https://api.kognitive.dev",
183
+ json,
184
+ raw: vi.fn(),
185
+ poll,
186
+ } as unknown as HttpTransport;
187
+
188
+ const client = new KognitiveCloudWebSearchClient(transport);
189
+ const result = await client.jobs.waitForCompletion("job_123", {
190
+ intervalMs: 250,
191
+ timeoutMs: 5_000,
192
+ parseOutput: parseSummary,
193
+ });
194
+
195
+ expect(poll).toHaveBeenCalledTimes(1);
196
+ expect(result.job.status).toBe("completed");
197
+ expect(result.result?.output).toEqual({ summary: "done" });
198
+ expect(result.result?.sources[0]?.faviconUrl).toBe("https://example.com/favicon.ico");
199
+ expect(json).toHaveBeenCalledWith("/api/cloud/web-search/jobs/job_123/result");
200
+ });
201
+
202
+ it("normalizes replayed backend event logs, dedupes snapshot progress, and parses typed results", async () => {
203
+ const raw = vi.fn().mockResolvedValue(new Response(toReadableStream(toSSEPayload([
204
+ {
205
+ event: "job.snapshot",
206
+ data: {
207
+ job: createJob({
208
+ mode: "research",
209
+ searchQuery: null,
210
+ searchInstructions: "Research Bun updates",
211
+ responseSchema: {
212
+ type: "object",
213
+ properties: {
214
+ summary: { type: "string" },
215
+ },
216
+ },
217
+ status: "in_progress",
218
+ progress: [
219
+ createProgress(),
220
+ ],
221
+ updatedAt: "2026-04-22T10:00:00.000Z",
222
+ }),
223
+ },
224
+ },
225
+ {
226
+ event: "job.created",
227
+ data: createJobEvent({
228
+ id: "evt_queued",
229
+ eventType: "job.created",
230
+ stage: "queued",
231
+ status: "queued",
232
+ message: "Web-search job created",
233
+ payload: { mode: "research" },
234
+ createdAt: "2026-04-22T10:00:00.000Z",
235
+ }),
236
+ },
237
+ {
238
+ event: "websearch.research.plan.started",
239
+ data: createJobEvent({
240
+ id: "evt_plan",
241
+ eventType: "websearch.research.plan.started",
242
+ stage: "planning",
243
+ status: "in_progress",
244
+ message: "Building research plan",
245
+ payload: { step: "plan" },
246
+ createdAt: "2026-04-22T10:00:01.000Z",
247
+ }),
248
+ },
249
+ {
250
+ event: "websearch.research.reasoning",
251
+ data: createJobEvent({
252
+ id: "evt_reasoning",
253
+ eventType: "websearch.research.reasoning",
254
+ stage: null,
255
+ status: "in_progress",
256
+ message: null,
257
+ payload: { delta: "Thinking..." },
258
+ createdAt: "2026-04-22T10:00:01.500Z",
259
+ }),
260
+ },
261
+ {
262
+ event: "job.completed",
263
+ data: createJobEvent({
264
+ id: "evt_completed",
265
+ eventType: "job.completed",
266
+ stage: "completed",
267
+ status: "completed",
268
+ message: "Web-search job completed",
269
+ payload: {
270
+ runId: "run_123",
271
+ result: {
272
+ output: { summary: "Ready" },
273
+ text: "Ready",
274
+ sources: [],
275
+ },
276
+ },
277
+ createdAt: "2026-04-22T10:00:02.000Z",
278
+ }),
279
+ },
280
+ ]))));
281
+ const transport = {
282
+ baseUrl: "https://api.kognitive.dev",
283
+ json: vi.fn(),
284
+ raw,
285
+ poll: vi.fn(),
286
+ } as unknown as HttpTransport;
287
+
288
+ const client = new KognitiveCloudWebSearchClient(transport);
289
+ const stream = await client.jobs.subscribe("job_123", {
290
+ parseOutput: parseSummary,
291
+ });
292
+ const events = await collect(stream);
293
+
294
+ expect(raw).toHaveBeenCalledWith("/api/cloud/web-search/jobs/job_123/events/stream", undefined);
295
+ expect(events).toHaveLength(5);
296
+ expect(events[0]).toMatchObject({
297
+ type: "snapshot",
298
+ job: {
299
+ id: "job_123",
300
+ status: "in_progress",
301
+ },
302
+ });
303
+ expect(events[1]).toMatchObject({
304
+ type: "progress",
305
+ replay: true,
306
+ progress: {
307
+ stage: "queued",
308
+ message: "Web-search job created",
309
+ },
310
+ });
311
+ expect(events[2]).toMatchObject({
312
+ type: "progress",
313
+ eventId: "evt_plan",
314
+ rawEventType: "websearch.research.plan.started",
315
+ progress: {
316
+ stage: "planning",
317
+ message: "Building research plan",
318
+ },
319
+ });
320
+ expect(events[3]).toMatchObject({
321
+ type: "event",
322
+ event: {
323
+ eventType: "websearch.research.reasoning",
324
+ },
325
+ });
326
+ expect(events[4]).toMatchObject({
327
+ type: "completed",
328
+ eventId: "evt_completed",
329
+ jobId: "job_123",
330
+ result: {
331
+ output: { summary: "Ready" },
332
+ text: "Ready",
333
+ sources: [],
334
+ },
335
+ });
336
+ });
337
+
338
+ it("parses canonical lifecycle frames and surfaces unknown events", async () => {
339
+ const raw = vi.fn().mockResolvedValue(new Response(toReadableStream(toSSEPayload([
340
+ {
341
+ event: "job.snapshot",
342
+ data: {
343
+ job: createJob({
344
+ status: "in_progress",
345
+ }),
346
+ },
347
+ },
348
+ {
349
+ id: "evt_progress",
350
+ event: "job.progress",
351
+ data: {
352
+ jobId: "job_123",
353
+ status: "in_progress",
354
+ progress: {
355
+ stage: "searching",
356
+ message: "Executing hosted search",
357
+ timestamp: "2026-04-22T10:00:01.000Z",
358
+ },
359
+ },
360
+ },
361
+ {
362
+ event: "custom.opaque",
363
+ data: "opaque",
364
+ },
365
+ {
366
+ id: "evt_completed",
367
+ event: "job.completed",
368
+ data: {
369
+ job: createJob({
370
+ status: "completed",
371
+ updatedAt: "2026-04-22T10:00:02.000Z",
372
+ }),
373
+ result: {
374
+ text: "Done",
375
+ sources: [],
376
+ },
377
+ },
378
+ },
379
+ ]))));
380
+ const transport = {
381
+ baseUrl: "https://api.kognitive.dev",
382
+ json: vi.fn(),
383
+ raw,
384
+ poll: vi.fn(),
385
+ } as unknown as HttpTransport;
386
+
387
+ const client = new KognitiveCloudWebSearchClient(transport);
388
+ const stream = await client.jobs.subscribe("job_123");
389
+ const events = await collect(stream);
390
+
391
+ expect(events).toHaveLength(4);
392
+ expect(events[1]).toMatchObject({
393
+ type: "progress",
394
+ eventId: "evt_progress",
395
+ progress: {
396
+ stage: "searching",
397
+ message: "Executing hosted search",
398
+ },
399
+ });
400
+ expect(events[2]).toMatchObject({
401
+ type: "unknown",
402
+ frame: {
403
+ event: "custom.opaque",
404
+ data: "opaque",
405
+ },
406
+ });
407
+ expect(events[3]).toMatchObject({
408
+ type: "completed",
409
+ eventId: "evt_completed",
410
+ result: {
411
+ text: "Done",
412
+ sources: [],
413
+ },
414
+ });
415
+ });
416
+
417
+ it("throws validation errors for malformed server payloads", async () => {
418
+ const transport = {
419
+ baseUrl: "https://api.kognitive.dev",
420
+ json: vi.fn().mockResolvedValue({
421
+ jobs: [{ id: "job_123" }],
422
+ }),
423
+ raw: vi.fn(),
424
+ poll: vi.fn(),
425
+ } as unknown as HttpTransport;
426
+
427
+ const client = new KognitiveCloudWebSearchClient(transport);
428
+ await expect(client.jobs.list()).rejects.toBeInstanceOf(CloudWebSearchValidationError);
429
+ });
430
+
431
+ it("fails fast for malformed canonical lifecycle frames", async () => {
432
+ const raw = vi.fn().mockResolvedValue(new Response(toReadableStream(toSSEPayload([
433
+ {
434
+ event: "job.snapshot",
435
+ data: {
436
+ job: createJob({
437
+ status: "in_progress",
438
+ }),
439
+ },
440
+ },
441
+ {
442
+ event: "job.completed",
443
+ data: {
444
+ bad: true,
445
+ },
446
+ },
447
+ ]))));
448
+ const transport = {
449
+ baseUrl: "https://api.kognitive.dev",
450
+ json: vi.fn(),
451
+ raw,
452
+ poll: vi.fn(),
453
+ } as unknown as HttpTransport;
454
+
455
+ const client = new KognitiveCloudWebSearchClient(transport);
456
+ const stream = await client.jobs.subscribe("job_123");
457
+
458
+ await expect(collect(stream)).rejects.toBeInstanceOf(CloudWebSearchValidationError);
459
+ });
460
+
461
+ it("maps persisted backend events into typed rich product events", async () => {
462
+ const raw = vi.fn().mockResolvedValue(new Response(toReadableStream(toSSEPayload([
463
+ {
464
+ event: "job.snapshot",
465
+ data: {
466
+ job: createJob({
467
+ mode: "research",
468
+ searchQuery: null,
469
+ searchInstructions: "Research browser-use alternatives",
470
+ status: "in_progress",
471
+ }),
472
+ },
473
+ },
474
+ {
475
+ event: "websearch.search.started",
476
+ data: createJobEvent({
477
+ id: "evt_search_started",
478
+ eventType: "websearch.search.started",
479
+ stage: "search",
480
+ status: "in_progress",
481
+ message: "Executing web search",
482
+ payload: { query: "browser-use alternatives" },
483
+ createdAt: "2026-04-22T10:00:01.000Z",
484
+ }),
485
+ },
486
+ {
487
+ event: "websearch.search.completed",
488
+ data: createJobEvent({
489
+ id: "evt_search_completed",
490
+ eventType: "websearch.search.completed",
491
+ stage: "search",
492
+ status: "completed",
493
+ message: "Web search completed",
494
+ payload: {
495
+ query: "browser-use alternatives",
496
+ provider: "mock",
497
+ results: [
498
+ {
499
+ title: "Source",
500
+ url: "https://example.com/source",
501
+ host: "example.com",
502
+ snippet: "snippet",
503
+ relevantContent: "extracted relevant text",
504
+ keyFacts: ["fact"],
505
+ confidence: 0.9,
506
+ faviconUrl: "https://example.com/favicon.ico",
507
+ },
508
+ ],
509
+ },
510
+ createdAt: "2026-04-22T10:00:02.000Z",
511
+ }),
512
+ },
513
+ {
514
+ event: "websearch.source.opening",
515
+ data: createJobEvent({
516
+ id: "evt_source_opening",
517
+ eventType: "websearch.source.opening",
518
+ stage: "fetch",
519
+ status: "in_progress",
520
+ message: "Opening source",
521
+ payload: {
522
+ index: 0,
523
+ source: {
524
+ title: "Source",
525
+ url: "https://example.com/source",
526
+ host: "example.com",
527
+ faviconUrl: "https://example.com/favicon.ico",
528
+ },
529
+ },
530
+ createdAt: "2026-04-22T10:00:02.250Z",
531
+ }),
532
+ },
533
+ {
534
+ event: "websearch.source.extracted",
535
+ data: createJobEvent({
536
+ id: "evt_source_extracted",
537
+ eventType: "websearch.source.extracted",
538
+ stage: "extract",
539
+ status: "completed",
540
+ message: "Source extracted",
541
+ payload: {
542
+ index: 0,
543
+ snippet: "extracted relevant text",
544
+ confidence: 0.9,
545
+ source: {
546
+ title: "Source",
547
+ url: "https://example.com/source",
548
+ host: "example.com",
549
+ relevantContent: "extracted relevant text",
550
+ keyFacts: ["fact"],
551
+ confidence: 0.9,
552
+ faviconUrl: "https://example.com/favicon.ico",
553
+ },
554
+ },
555
+ createdAt: "2026-04-22T10:00:02.500Z",
556
+ }),
557
+ },
558
+ {
559
+ event: "websearch.tool.sources.reviewing",
560
+ data: createJobEvent({
561
+ id: "evt_sources_reviewing",
562
+ eventType: "websearch.tool.sources.reviewing",
563
+ stage: "review",
564
+ status: "in_progress",
565
+ message: "Reviewing extracted sources",
566
+ payload: {
567
+ totalExtractions: 1,
568
+ },
569
+ createdAt: "2026-04-22T10:00:02.600Z",
570
+ }),
571
+ },
572
+ {
573
+ event: "websearch.tool.sources.filtered",
574
+ data: createJobEvent({
575
+ id: "evt_sources_filtered",
576
+ eventType: "websearch.tool.sources.filtered",
577
+ stage: "review",
578
+ status: "in_progress",
579
+ message: "Cross-checking extracted sources",
580
+ payload: {
581
+ totalSources: 1,
582
+ credibleSources: 1,
583
+ filteredOut: 0,
584
+ },
585
+ createdAt: "2026-04-22T10:00:02.700Z",
586
+ }),
587
+ },
588
+ {
589
+ event: "websearch.tool.retrieval.completed",
590
+ data: createJobEvent({
591
+ id: "evt_sources_reviewed",
592
+ eventType: "websearch.tool.retrieval.completed",
593
+ stage: "review",
594
+ status: "completed",
595
+ message: "Source retrieval completed",
596
+ payload: {
597
+ credibleSourceCount: 1,
598
+ sources: [{
599
+ title: "Source",
600
+ url: "https://example.com/source",
601
+ host: "example.com",
602
+ relevantContent: "extracted relevant text",
603
+ keyFacts: ["fact"],
604
+ confidence: 0.9,
605
+ faviconUrl: "https://example.com/favicon.ico",
606
+ }],
607
+ },
608
+ createdAt: "2026-04-22T10:00:02.800Z",
609
+ }),
610
+ },
611
+ {
612
+ event: "websearch.research.synthesis.started",
613
+ data: createJobEvent({
614
+ id: "evt_synthesis_started",
615
+ eventType: "websearch.research.synthesis.started",
616
+ stage: "synthesize",
617
+ status: "in_progress",
618
+ message: "Synthesizing final answer",
619
+ payload: null,
620
+ createdAt: "2026-04-22T10:00:02.900Z",
621
+ }),
622
+ },
623
+ {
624
+ event: "websearch.research.answer.delta",
625
+ data: createJobEvent({
626
+ id: "evt_delta",
627
+ eventType: "websearch.research.answer.delta",
628
+ stage: "research",
629
+ status: "in_progress",
630
+ message: "Streaming answer delta",
631
+ payload: { delta: "partial" },
632
+ createdAt: "2026-04-22T10:00:03.000Z",
633
+ }),
634
+ },
635
+ {
636
+ event: "websearch.research.synthesis.completed",
637
+ data: createJobEvent({
638
+ id: "evt_synthesis_completed",
639
+ eventType: "websearch.research.synthesis.completed",
640
+ stage: "synthesize",
641
+ status: "completed",
642
+ message: "Final research synthesis completed",
643
+ payload: {
644
+ hasStructuredOutput: true,
645
+ toolCalls: 1,
646
+ },
647
+ createdAt: "2026-04-22T10:00:03.500Z",
648
+ }),
649
+ },
650
+ {
651
+ event: "job.completed",
652
+ data: createJobEvent({
653
+ id: "evt_completed",
654
+ eventType: "job.completed",
655
+ stage: "completed",
656
+ status: "completed",
657
+ message: "Web-search job completed",
658
+ payload: {
659
+ result: {
660
+ output: { summary: "Ready", markdown: "# Ready\n\nDone [c1]" },
661
+ text: "Ready",
662
+ sources: [{
663
+ title: "Source",
664
+ url: "https://example.com/source",
665
+ host: "example.com",
666
+ relevantContent: "extracted relevant text",
667
+ keyFacts: ["fact"],
668
+ confidence: 0.9,
669
+ faviconUrl: "https://example.com/favicon.ico",
670
+ }],
671
+ },
672
+ },
673
+ createdAt: "2026-04-22T10:00:04.000Z",
674
+ }),
675
+ },
676
+ ]))));
677
+ const transport = {
678
+ baseUrl: "https://api.kognitive.dev",
679
+ json: vi.fn(),
680
+ raw,
681
+ poll: vi.fn(),
682
+ } as unknown as HttpTransport;
683
+
684
+ const client = new KognitiveCloudWebSearchClient(transport);
685
+ const stream = await client.jobs.subscribeRich("job_123", {
686
+ parseOutput: parseSummary,
687
+ });
688
+ const events = await collect(stream);
689
+ const terminalEventTypes = events.slice(-2).map((event) => event.type);
690
+
691
+ expect(events).toEqual(expect.arrayContaining([
692
+ expect.objectContaining({
693
+ type: "search.started",
694
+ query: "browser-use alternatives",
695
+ }),
696
+ expect.objectContaining({
697
+ type: "search.completed",
698
+ provider: "mock",
699
+ results: [
700
+ expect.objectContaining({
701
+ title: "Source",
702
+ host: "example.com",
703
+ relevantContent: "extracted relevant text",
704
+ keyFacts: ["fact"],
705
+ confidence: 0.9,
706
+ faviconUrl: "https://example.com/favicon.ico",
707
+ }),
708
+ ],
709
+ }),
710
+ expect.objectContaining({
711
+ type: "source.opening",
712
+ index: 0,
713
+ source: expect.objectContaining({
714
+ host: "example.com",
715
+ faviconUrl: "https://example.com/favicon.ico",
716
+ }),
717
+ }),
718
+ expect.objectContaining({
719
+ type: "source.extracted",
720
+ index: 0,
721
+ snippet: "extracted relevant text",
722
+ confidence: 0.9,
723
+ source: expect.objectContaining({
724
+ relevantContent: "extracted relevant text",
725
+ keyFacts: ["fact"],
726
+ }),
727
+ }),
728
+ expect.objectContaining({
729
+ type: "sources.reviewing",
730
+ totalExtractions: 1,
731
+ }),
732
+ expect.objectContaining({
733
+ type: "sources.filtered",
734
+ totalSources: 1,
735
+ credibleSources: 1,
736
+ filteredOut: 0,
737
+ }),
738
+ expect.objectContaining({
739
+ type: "sources.reviewed",
740
+ credibleSourceCount: 1,
741
+ sources: [
742
+ expect.objectContaining({
743
+ host: "example.com",
744
+ relevantContent: "extracted relevant text",
745
+ }),
746
+ ],
747
+ }),
748
+ expect.objectContaining({
749
+ type: "synthesis.started",
750
+ }),
751
+ expect.objectContaining({
752
+ type: "answer.delta",
753
+ delta: "partial",
754
+ }),
755
+ expect.objectContaining({
756
+ type: "synthesis.completed",
757
+ }),
758
+ expect.objectContaining({
759
+ type: "completed",
760
+ result: {
761
+ output: { summary: "Ready", markdown: "# Ready\n\nDone [c1]" },
762
+ text: "Ready",
763
+ sources: [
764
+ expect.objectContaining({
765
+ host: "example.com",
766
+ relevantContent: "extracted relevant text",
767
+ }),
768
+ ],
769
+ },
770
+ }),
771
+ expect.objectContaining({
772
+ type: "answer.completed",
773
+ answer: { summary: "Ready", markdown: "# Ready\n\nDone [c1]" },
774
+ }),
775
+ ]));
776
+ expect(terminalEventTypes).toEqual(["answer.completed", "completed"]);
777
+
778
+ expect(toCloudWebSearchRichEvents({
779
+ type: "unknown",
780
+ job: null,
781
+ frame: { event: "opaque", data: {} },
782
+ })).toEqual([]);
783
+ });
784
+ });