@assistant-ui/react 0.14.9 → 0.14.11

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.
Files changed (37) hide show
  1. package/dist/internal.d.ts +3 -4
  2. package/dist/internal.js +1 -3
  3. package/dist/internal.js.map +1 -1
  4. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +3 -2
  5. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +1 -1
  6. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  7. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +4 -10
  8. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  9. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +1 -1
  10. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
  11. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  12. package/dist/mcp-apps/app-frame.js.map +1 -1
  13. package/dist/primitives/assistantModal/AssistantModalRoot.js +5 -1
  14. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  15. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  16. package/dist/utils/useToolArgsFieldStatus.d.ts.map +1 -1
  17. package/package.json +3 -3
  18. package/src/internal.ts +1 -4
  19. package/src/legacy-runtime/runtime/ThreadListRuntime.ts +1 -4
  20. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +3 -1
  21. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +7 -15
  22. package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +1 -1
  23. package/src/mcp-apps/McpAppRenderer.tsx +1 -1
  24. package/src/mcp-apps/app-frame.tsx +2 -2
  25. package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -1
  26. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +1 -1
  27. package/src/primitives/composer/ComposerInput.test.tsx +1 -1
  28. package/src/primitives/composer/trigger/TriggerPopoverRootContext.test.tsx +1 -1
  29. package/src/primitives/threadListItem/ThreadListItemTitle.test.tsx +1 -1
  30. package/src/tests/BaseComposerRuntimeCore.test.ts +2 -2
  31. package/src/tests/MessageParts.loading.test.tsx +1 -1
  32. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +2 -4
  33. package/src/tests/auiV0Encode.test.ts +1 -1
  34. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.d.ts +0 -2
  35. package/dist/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.js +0 -2
  36. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.test.ts +0 -892
  37. package/src/legacy-runtime/runtime-cores/assistant-transport/useToolInvocations.ts +0 -4
@@ -1,892 +0,0 @@
1
- // @vitest-environment jsdom
2
-
3
- import type { ThreadAssistantMessage } from "@assistant-ui/core";
4
- import type { Tool } from "assistant-stream";
5
- import { act, renderHook, waitFor } from "@testing-library/react";
6
- import { describe, expect, it, vi } from "vitest";
7
- import type { AssistantTransportState } from "./types";
8
- import {
9
- type ToolExecutionStatus,
10
- useToolInvocations,
11
- } from "./useToolInvocations";
12
- import type {
13
- ReadonlyJSONObject,
14
- ReadonlyJSONValue,
15
- } from "assistant-stream/utils";
16
-
17
- const createState = (
18
- messages: ThreadAssistantMessage[],
19
- isRunning: boolean = true,
20
- ): AssistantTransportState => ({
21
- messages,
22
- isRunning,
23
- });
24
-
25
- const createAssistantMessage = (
26
- argsText: string,
27
- args: Record<string, unknown>,
28
- options?: {
29
- result?: ReadonlyJSONValue;
30
- isError?: boolean;
31
- toolCallId?: string;
32
- toolName?: string;
33
- nestedMessages?: ThreadAssistantMessage[];
34
- },
35
- ): ThreadAssistantMessage => ({
36
- id: "m-1",
37
- role: "assistant",
38
- createdAt: new Date(),
39
- status: { type: "requires-action", reason: "tool-calls" },
40
- metadata: {
41
- unstable_state: null,
42
- unstable_annotations: [],
43
- unstable_data: [],
44
- steps: [],
45
- custom: {},
46
- },
47
- content: [
48
- {
49
- type: "tool-call",
50
- toolCallId: options?.toolCallId ?? "tool-1",
51
- toolName: options?.toolName ?? "weatherSearch",
52
- args: args as ReadonlyJSONObject,
53
- argsText,
54
- ...(options?.result !== undefined && { result: options.result }),
55
- ...(options?.isError !== undefined && { isError: options.isError }),
56
- ...(options?.nestedMessages && { messages: options.nestedMessages }),
57
- },
58
- ],
59
- });
60
-
61
- describe("useToolInvocations", () => {
62
- it("does not crash when tool argsText rewrites a previously streamed value", async () => {
63
- const execute = vi.fn(async () => ({ forecast: "ok" }));
64
- const getTools = () => ({
65
- weatherSearch: {
66
- parameters: { type: "object", properties: {} },
67
- execute,
68
- } satisfies Tool,
69
- });
70
- const onResult = vi.fn();
71
- const setToolStatuses = vi.fn();
72
-
73
- const { rerender } = renderHook(
74
- ({ state }: { state: AssistantTransportState }) =>
75
- useToolInvocations({
76
- state,
77
- getTools,
78
- onResult,
79
- setToolStatuses,
80
- }),
81
- {
82
- initialProps: {
83
- state: createState([]),
84
- },
85
- },
86
- );
87
-
88
- expect(() => {
89
- act(() => {
90
- rerender({
91
- state: createState([
92
- createAssistantMessage('{"query":"London","longitude":0', {
93
- query: "London",
94
- longitude: 0,
95
- }),
96
- ]),
97
- });
98
- });
99
- }).not.toThrow();
100
-
101
- expect(() => {
102
- act(() => {
103
- rerender({
104
- state: createState([
105
- createAssistantMessage('{"query":"London","longitude":-0.125', {
106
- query: "London",
107
- longitude: -0.125,
108
- }),
109
- ]),
110
- });
111
- });
112
- }).not.toThrow();
113
-
114
- act(() => {
115
- rerender({
116
- state: createState([
117
- createAssistantMessage(
118
- '{"query":"London","longitude":-0.125,"latitude":51.5072}',
119
- { query: "London", longitude: -0.125, latitude: 51.5072 },
120
- ),
121
- ]),
122
- });
123
- });
124
-
125
- await waitFor(() => {
126
- expect(execute).toHaveBeenCalledTimes(1);
127
- });
128
-
129
- expect(execute).toHaveBeenCalledWith(
130
- {
131
- query: "London",
132
- longitude: -0.125,
133
- latitude: 51.5072,
134
- },
135
- expect.objectContaining({ toolCallId: "tool-1" }),
136
- );
137
-
138
- await waitFor(() => {
139
- expect(onResult).toHaveBeenCalledTimes(1);
140
- });
141
- expect(onResult).toHaveBeenCalledWith(
142
- expect.objectContaining({
143
- type: "add-tool-result",
144
- toolCallId: "tool-1",
145
- toolName: "weatherSearch",
146
- }),
147
- );
148
- });
149
-
150
- it("keeps logical toolCallId mapping through reset while abort settles", async () => {
151
- const execute = vi.fn(
152
- async () =>
153
- await new Promise(() => {
154
- // never resolves: reset() should cancel this call
155
- }),
156
- );
157
- const getTools = () => ({
158
- weatherSearch: {
159
- parameters: { type: "object", properties: {} },
160
- execute,
161
- } satisfies Tool,
162
- });
163
- const onResult = vi.fn();
164
-
165
- let statuses: Record<string, ToolExecutionStatus> = {};
166
- const setToolStatuses = vi.fn(
167
- (
168
- updater:
169
- | Record<string, ToolExecutionStatus>
170
- | ((
171
- prev: Record<string, ToolExecutionStatus>,
172
- ) => Record<string, ToolExecutionStatus>),
173
- ) => {
174
- statuses =
175
- typeof updater === "function" ? updater(statuses) : { ...updater };
176
- },
177
- );
178
-
179
- const { result, rerender } = renderHook(
180
- ({ state }: { state: AssistantTransportState }) =>
181
- useToolInvocations({
182
- state,
183
- getTools,
184
- onResult,
185
- setToolStatuses,
186
- }),
187
- {
188
- initialProps: {
189
- state: createState([]),
190
- },
191
- },
192
- );
193
-
194
- act(() => {
195
- rerender({
196
- state: createState([
197
- createAssistantMessage('{"query":"London","longitude":0', {
198
- query: "London",
199
- longitude: 0,
200
- }),
201
- ]),
202
- });
203
- });
204
-
205
- act(() => {
206
- rerender({
207
- state: createState([
208
- createAssistantMessage('{"query":"London","longitude":-0.125', {
209
- query: "London",
210
- longitude: -0.125,
211
- }),
212
- ]),
213
- });
214
- });
215
-
216
- act(() => {
217
- rerender({
218
- state: createState([
219
- createAssistantMessage(
220
- '{"query":"London","longitude":-0.125,"latitude":51.5072}',
221
- { query: "London", longitude: -0.125, latitude: 51.5072 },
222
- ),
223
- ]),
224
- });
225
- });
226
-
227
- await waitFor(() => {
228
- expect(execute).toHaveBeenCalledTimes(1);
229
- });
230
- await waitFor(() => {
231
- expect(statuses["tool-1"]).toEqual({ type: "executing" });
232
- });
233
-
234
- act(() => {
235
- result.current.reset();
236
- });
237
-
238
- await waitFor(() => {
239
- expect(statuses).toEqual({});
240
- });
241
- expect(Object.keys(statuses)).not.toContain("tool-1:rewrite:0");
242
- });
243
-
244
- it("does not execute tool calls loaded asynchronously with existing results", async () => {
245
- const execute = vi.fn(async () => ({ forecast: "ok" }));
246
- const getTools = () => ({
247
- weatherSearch: {
248
- parameters: { type: "object", properties: {} },
249
- execute,
250
- } satisfies Tool,
251
- });
252
- const onResult = vi.fn();
253
- const setToolStatuses = vi.fn();
254
-
255
- const { rerender } = renderHook(
256
- ({ state }: { state: AssistantTransportState }) =>
257
- useToolInvocations({
258
- state,
259
- getTools,
260
- onResult,
261
- setToolStatuses,
262
- }),
263
- {
264
- initialProps: {
265
- state: createState([]),
266
- },
267
- },
268
- );
269
-
270
- act(() => {
271
- rerender({
272
- state: createState([
273
- createAssistantMessage(
274
- '{"query":"London"}',
275
- { query: "London" },
276
- { result: { source: "history" } },
277
- ),
278
- ]),
279
- });
280
- });
281
-
282
- await waitFor(() => {
283
- expect(execute).not.toHaveBeenCalled();
284
- expect(onResult).not.toHaveBeenCalled();
285
- });
286
- });
287
-
288
- it("does not re-execute asynchronously loaded resolved tool calls after reset", async () => {
289
- const execute = vi.fn(async () => ({ forecast: "ok" }));
290
- const getTools = () => ({
291
- weatherSearch: {
292
- parameters: { type: "object", properties: {} },
293
- execute,
294
- } satisfies Tool,
295
- });
296
- const onResult = vi.fn();
297
- const setToolStatuses = vi.fn();
298
-
299
- const { result, rerender } = renderHook(
300
- ({ state }: { state: AssistantTransportState }) =>
301
- useToolInvocations({
302
- state,
303
- getTools,
304
- onResult,
305
- setToolStatuses,
306
- }),
307
- {
308
- initialProps: {
309
- state: createState([]),
310
- },
311
- },
312
- );
313
-
314
- act(() => {
315
- rerender({
316
- state: createState([
317
- createAssistantMessage('{"query":"London"}', { query: "London" }),
318
- ]),
319
- });
320
- });
321
-
322
- await waitFor(() => {
323
- expect(execute).toHaveBeenCalledTimes(1);
324
- });
325
-
326
- act(() => {
327
- result.current.reset();
328
- });
329
-
330
- await act(async () => {
331
- await Promise.resolve();
332
- });
333
-
334
- act(() => {
335
- rerender({
336
- state: createState([]),
337
- });
338
- });
339
-
340
- act(() => {
341
- rerender({
342
- state: createState([
343
- createAssistantMessage(
344
- '{"query":"London"}',
345
- { query: "London" },
346
- { result: { source: "history" } },
347
- ),
348
- ]),
349
- });
350
- });
351
-
352
- await waitFor(() => {
353
- expect(execute).toHaveBeenCalledTimes(1);
354
- expect(onResult).toHaveBeenCalledTimes(1);
355
- });
356
- });
357
-
358
- it("still processes nested unresolved tool calls when the parent tool call is already resolved", async () => {
359
- const executeParent = vi.fn(async () => ({ scope: "parent" }));
360
- const executeChild = vi.fn(async () => ({ scope: "child" }));
361
- const getTools = () => ({
362
- resolvedOnly: {
363
- parameters: { type: "object", properties: {} },
364
- execute: executeParent,
365
- } satisfies Tool,
366
- childTool: {
367
- parameters: { type: "object", properties: {} },
368
- execute: executeChild,
369
- } satisfies Tool,
370
- });
371
- const onResult = vi.fn();
372
- const setToolStatuses = vi.fn();
373
-
374
- const nestedMessage = createAssistantMessage(
375
- '{"query":"nested"}',
376
- { query: "nested" },
377
- {
378
- toolCallId: "tool-child",
379
- toolName: "childTool",
380
- },
381
- );
382
-
383
- const { rerender } = renderHook(
384
- ({ state }: { state: AssistantTransportState }) =>
385
- useToolInvocations({
386
- state,
387
- getTools,
388
- onResult,
389
- setToolStatuses,
390
- }),
391
- {
392
- initialProps: {
393
- state: createState([]),
394
- },
395
- },
396
- );
397
-
398
- act(() => {
399
- rerender({
400
- state: createState([
401
- createAssistantMessage(
402
- '{"query":"parent"}',
403
- { query: "parent" },
404
- {
405
- result: { source: "history" },
406
- toolName: "resolvedOnly",
407
- nestedMessages: [nestedMessage],
408
- },
409
- ),
410
- ]),
411
- });
412
- });
413
-
414
- await waitFor(() => {
415
- expect(executeParent).not.toHaveBeenCalled();
416
- expect(executeChild).toHaveBeenCalledTimes(1);
417
- });
418
- });
419
-
420
- it("does not close args stream early for non-executable tool snapshots", () => {
421
- const getTools = () => ({
422
- weatherSearch: {
423
- parameters: { type: "object", properties: {} },
424
- } satisfies Tool,
425
- });
426
- const onResult = vi.fn();
427
- const setToolStatuses = vi.fn();
428
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
429
-
430
- try {
431
- const { rerender } = renderHook(
432
- ({ state }: { state: AssistantTransportState }) =>
433
- useToolInvocations({
434
- state,
435
- getTools,
436
- onResult,
437
- setToolStatuses,
438
- }),
439
- {
440
- initialProps: {
441
- state: createState([]),
442
- },
443
- },
444
- );
445
-
446
- act(() => {
447
- rerender({
448
- state: createState([createAssistantMessage("{}", {})]),
449
- });
450
- });
451
-
452
- act(() => {
453
- rerender({
454
- state: createState([
455
- createAssistantMessage('{"title":"Weekly"', {
456
- title: "Weekly",
457
- }),
458
- ]),
459
- });
460
- });
461
-
462
- act(() => {
463
- rerender({
464
- state: createState([
465
- createAssistantMessage('{"title":"Weekly","columns":["name"]}', {
466
- title: "Weekly",
467
- columns: ["name"],
468
- }),
469
- ]),
470
- });
471
- });
472
-
473
- expect(warnSpy).not.toHaveBeenCalledWith(
474
- "argsText updated after controller was closed:",
475
- expect.anything(),
476
- );
477
- expect(warnSpy).not.toHaveBeenCalledWith(
478
- "argsText updated after controller was closed, restarting tool args stream:",
479
- expect.anything(),
480
- );
481
- expect(onResult).not.toHaveBeenCalled();
482
- } finally {
483
- warnSpy.mockRestore();
484
- }
485
- });
486
-
487
- it("closes non-executable complete args stream after run settles", () => {
488
- const getTools = () => ({
489
- weatherSearch: {
490
- parameters: { type: "object", properties: {} },
491
- } satisfies Tool,
492
- });
493
- const onResult = vi.fn();
494
- const setToolStatuses = vi.fn();
495
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
496
-
497
- try {
498
- const { rerender } = renderHook(
499
- ({ state }: { state: AssistantTransportState }) =>
500
- useToolInvocations({
501
- state,
502
- getTools,
503
- onResult,
504
- setToolStatuses,
505
- }),
506
- {
507
- initialProps: {
508
- state: createState([]),
509
- },
510
- },
511
- );
512
-
513
- act(() => {
514
- rerender({
515
- state: createState(
516
- [
517
- createAssistantMessage('{"title":"Weekly"}', {
518
- title: "Weekly",
519
- }),
520
- ],
521
- true,
522
- ),
523
- });
524
- });
525
-
526
- act(() => {
527
- rerender({
528
- state: createState(
529
- [
530
- createAssistantMessage('{"title":"Weekly"}', {
531
- title: "Weekly",
532
- }),
533
- ],
534
- false,
535
- ),
536
- });
537
- });
538
-
539
- act(() => {
540
- rerender({
541
- state: createState(
542
- [
543
- createAssistantMessage('{"title":"Weekly","columns":["name"]}', {
544
- title: "Weekly",
545
- columns: ["name"],
546
- }),
547
- ],
548
- false,
549
- ),
550
- });
551
- });
552
-
553
- expect(warnSpy).toHaveBeenCalledWith(
554
- "argsText updated after controller was closed, restarting tool args stream:",
555
- expect.objectContaining({
556
- previous: '{"title":"Weekly"}',
557
- next: '{"title":"Weekly","columns":["name"]}',
558
- }),
559
- );
560
- expect(onResult).not.toHaveBeenCalled();
561
- } finally {
562
- warnSpy.mockRestore();
563
- }
564
- });
565
-
566
- it("handles backend result when equivalent complete argsText reorders keys", async () => {
567
- let resolveExecute: ((value: unknown) => void) | undefined;
568
- const execute = vi.fn(
569
- () =>
570
- new Promise<unknown>((resolve) => {
571
- resolveExecute = resolve;
572
- }),
573
- );
574
- const getTools = () => ({
575
- weatherSearch: {
576
- parameters: { type: "object", properties: {} },
577
- execute,
578
- } satisfies Tool,
579
- });
580
- const onResult = vi.fn();
581
- const setToolStatuses = vi.fn();
582
-
583
- const { rerender } = renderHook(
584
- ({ state }: { state: AssistantTransportState }) =>
585
- useToolInvocations({
586
- state,
587
- getTools,
588
- onResult,
589
- setToolStatuses,
590
- }),
591
- {
592
- initialProps: {
593
- state: createState([]),
594
- },
595
- },
596
- );
597
-
598
- act(() => {
599
- rerender({
600
- state: createState([
601
- createAssistantMessage('{"a":1,"b":2}', {
602
- a: 1,
603
- b: 2,
604
- }),
605
- ]),
606
- });
607
- });
608
-
609
- await waitFor(() => {
610
- expect(execute).toHaveBeenCalledTimes(1);
611
- });
612
-
613
- act(() => {
614
- rerender({
615
- state: createState([
616
- createAssistantMessage(
617
- '{"b":2,"a":1}',
618
- {
619
- a: 1,
620
- b: 2,
621
- },
622
- {
623
- result: { source: "backend" },
624
- },
625
- ),
626
- ]),
627
- });
628
- });
629
-
630
- await act(async () => {
631
- resolveExecute?.({ source: "client" });
632
- });
633
-
634
- await waitFor(() => {
635
- expect(onResult).not.toHaveBeenCalled();
636
- });
637
- });
638
-
639
- it("fires streamCall for already-resolved tool calls loaded after the initial snapshot", async () => {
640
- const execute = vi.fn(async () => ({ forecast: "ok" }));
641
- const streamCall = vi.fn();
642
- const getTools = () => ({
643
- weatherSearch: {
644
- parameters: { type: "object", properties: {} },
645
- execute,
646
- streamCall,
647
- } satisfies Tool,
648
- });
649
- const onResult = vi.fn();
650
- const setToolStatuses = vi.fn();
651
-
652
- const { rerender } = renderHook(
653
- ({ state }: { state: AssistantTransportState }) =>
654
- useToolInvocations({
655
- state,
656
- getTools,
657
- onResult,
658
- setToolStatuses,
659
- }),
660
- {
661
- initialProps: {
662
- state: createState([]),
663
- },
664
- },
665
- );
666
-
667
- act(() => {
668
- rerender({
669
- state: createState([
670
- createAssistantMessage(
671
- '{"query":"London"}',
672
- { query: "London" },
673
- { result: { source: "history" } },
674
- ),
675
- ]),
676
- });
677
- });
678
-
679
- await waitFor(() => {
680
- expect(streamCall).toHaveBeenCalledTimes(1);
681
- });
682
-
683
- const [reader] = streamCall.mock.calls[0]!;
684
- await expect(reader.args.get("query")).resolves.toBe("London");
685
- const response = await reader.response.get();
686
- expect(response.result).toEqual({ source: "history" });
687
-
688
- expect(execute).not.toHaveBeenCalled();
689
- expect(onResult).not.toHaveBeenCalled();
690
- expect(setToolStatuses).not.toHaveBeenCalled();
691
- });
692
-
693
- it("does not fire streamCall for tool calls present in the initial snapshot", async () => {
694
- const streamCall = vi.fn();
695
- const getTools = () => ({
696
- weatherSearch: {
697
- parameters: { type: "object", properties: {} },
698
- streamCall,
699
- } satisfies Tool,
700
- });
701
- const onResult = vi.fn();
702
- const setToolStatuses = vi.fn();
703
-
704
- renderHook(
705
- ({ state }: { state: AssistantTransportState }) =>
706
- useToolInvocations({
707
- state,
708
- getTools,
709
- onResult,
710
- setToolStatuses,
711
- }),
712
- {
713
- initialProps: {
714
- state: createState([
715
- createAssistantMessage(
716
- '{"query":"London"}',
717
- { query: "London" },
718
- { result: { source: "history" } },
719
- ),
720
- ]),
721
- },
722
- },
723
- );
724
-
725
- await act(async () => {});
726
- expect(streamCall).not.toHaveBeenCalled();
727
- expect(onResult).not.toHaveBeenCalled();
728
- });
729
-
730
- it("promotes an in-progress tool call from the initial snapshot when it changes", async () => {
731
- const execute = vi.fn(async () => ({ forecast: "ok" }));
732
- const streamCall = vi.fn();
733
- const getTools = () => ({
734
- weatherSearch: {
735
- parameters: { type: "object", properties: {} },
736
- execute,
737
- streamCall,
738
- } satisfies Tool,
739
- });
740
- const onResult = vi.fn();
741
- const setToolStatuses = vi.fn();
742
-
743
- const { rerender } = renderHook(
744
- ({ state }: { state: AssistantTransportState }) =>
745
- useToolInvocations({
746
- state,
747
- getTools,
748
- onResult,
749
- setToolStatuses,
750
- }),
751
- {
752
- initialProps: {
753
- state: createState([
754
- createAssistantMessage('{"query":"Lon', { query: "Lon" }),
755
- ]),
756
- },
757
- },
758
- );
759
-
760
- await act(async () => {});
761
- expect(streamCall).not.toHaveBeenCalled();
762
-
763
- act(() => {
764
- rerender({
765
- state: createState([
766
- createAssistantMessage(
767
- '{"query":"London"}',
768
- { query: "London" },
769
- { result: { source: "history" } },
770
- ),
771
- ]),
772
- });
773
- });
774
-
775
- await waitFor(() => {
776
- expect(streamCall).toHaveBeenCalledTimes(1);
777
- });
778
-
779
- const [reader] = streamCall.mock.calls[0]!;
780
- await expect(reader.args.get("query")).resolves.toBe("London");
781
- const response = await reader.response.get();
782
- expect(response.result).toEqual({ source: "history" });
783
-
784
- expect(execute).not.toHaveBeenCalled();
785
- expect(onResult).not.toHaveBeenCalled();
786
- });
787
-
788
- it("does not re-fire streamCall when an initial-snapshot tool call is unchanged in later snapshots", async () => {
789
- const streamCall = vi.fn();
790
- const getTools = () => ({
791
- weatherSearch: {
792
- parameters: { type: "object", properties: {} },
793
- streamCall,
794
- } satisfies Tool,
795
- });
796
- const onResult = vi.fn();
797
- const setToolStatuses = vi.fn();
798
-
799
- const { rerender } = renderHook(
800
- ({ state }: { state: AssistantTransportState }) =>
801
- useToolInvocations({
802
- state,
803
- getTools,
804
- onResult,
805
- setToolStatuses,
806
- }),
807
- {
808
- initialProps: {
809
- state: createState([
810
- createAssistantMessage(
811
- '{"query":"London"}',
812
- { query: "London" },
813
- { result: { source: "history" } },
814
- ),
815
- ]),
816
- },
817
- },
818
- );
819
-
820
- act(() => {
821
- rerender({
822
- state: createState([
823
- createAssistantMessage(
824
- '{"query":"London"}',
825
- { query: "London" },
826
- { result: { source: "history" } },
827
- ),
828
- ]),
829
- });
830
- });
831
-
832
- await act(async () => {});
833
- expect(streamCall).not.toHaveBeenCalled();
834
- });
835
-
836
- it("does not emit a cancellation onResult for pre-resolved tool calls aborted by reset", async () => {
837
- const streamCall = vi.fn();
838
- const getTools = () => ({
839
- weatherSearch: {
840
- parameters: { type: "object", properties: {} },
841
- execute: vi.fn(async () => ({ forecast: "ok" })),
842
- streamCall,
843
- } satisfies Tool,
844
- });
845
- const onResult = vi.fn();
846
- const setToolStatuses = vi.fn();
847
-
848
- const { result, rerender } = renderHook(
849
- ({ state }: { state: AssistantTransportState }) =>
850
- useToolInvocations({
851
- state,
852
- getTools,
853
- onResult,
854
- setToolStatuses,
855
- }),
856
- {
857
- initialProps: {
858
- state: createState([]),
859
- },
860
- },
861
- );
862
-
863
- act(() => {
864
- rerender({
865
- state: createState([
866
- createAssistantMessage(
867
- '{"query":"London"}',
868
- { query: "London" },
869
- { result: { source: "history" } },
870
- ),
871
- ]),
872
- });
873
- });
874
-
875
- await waitFor(() => {
876
- expect(streamCall).toHaveBeenCalledTimes(1);
877
- });
878
-
879
- act(() => {
880
- result.current.reset();
881
- });
882
-
883
- // Flush microtasks through the executor's abort race + the stream
884
- // pipeline so any cancellation `result` chunk has a chance to land
885
- // before we assert it didn't.
886
- for (let i = 0; i < 5; i++) {
887
- await act(async () => {});
888
- }
889
-
890
- expect(onResult).not.toHaveBeenCalled();
891
- });
892
- });