@assistant-ui/react-a2a 0.2.34 → 0.2.36

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.
@@ -8,7 +8,10 @@ import {
8
8
  type PropsWithChildren,
9
9
  } from "react";
10
10
  import { afterEach, describe, expect, it, vi } from "vitest";
11
- import type { ThreadMessage } from "@assistant-ui/core";
11
+ import type {
12
+ ExportedMessageRepository,
13
+ ThreadMessage,
14
+ } from "@assistant-ui/core";
12
15
  import type { A2AClient } from "./A2AClient";
13
16
  import type { A2AStreamEvent } from "./types";
14
17
  import { useA2ARuntime } from "./useA2ARuntime";
@@ -74,7 +77,21 @@ const createFetchMock = () =>
74
77
  : input.url;
75
78
  if (url.endsWith("/.well-known/agent-card.json")) {
76
79
  return new Response(
77
- JSON.stringify({ capabilities: { streaming: true } }),
80
+ JSON.stringify({
81
+ name: "Test Agent",
82
+ version: "1.0",
83
+ supported_interfaces: [
84
+ {
85
+ url: "https://agent.test",
86
+ protocol_binding: "HTTP+JSON",
87
+ protocol_version: "1.0",
88
+ },
89
+ ],
90
+ capabilities: { streaming: true },
91
+ default_input_modes: ["text"],
92
+ default_output_modes: ["text"],
93
+ skills: [],
94
+ }),
78
95
  {
79
96
  status: 200,
80
97
  headers: { "Content-Type": "application/json" },
@@ -161,10 +178,12 @@ describe("useA2ARuntime", () => {
161
178
  rerender({ client: second.client });
162
179
 
163
180
  await waitFor(() => expect(second.getAgentCard).toHaveBeenCalledOnce());
164
- await waitFor(() => expect(history.load).toHaveBeenCalledTimes(2));
165
- expect(result.current.thread.getState().messages.map((m) => m.id)).toEqual([
166
- "restored",
167
- ]);
181
+ await waitFor(() =>
182
+ expect(
183
+ result.current.thread.getState().messages.map((m) => m.id),
184
+ ).toEqual(["restored"]),
185
+ );
186
+ expect(history.load).toHaveBeenCalledTimes(2);
168
187
  });
169
188
 
170
189
  it("switches provided clients and aborts the previous client run", async () => {
@@ -341,6 +360,93 @@ describe("useA2ARuntime", () => {
341
360
  ]);
342
361
  });
343
362
 
363
+ it("keeps pending history when a run is cancelled in the same thread", async () => {
364
+ const { client, streamMessage } = createMockClient(true);
365
+ let resolve!: (repo: ExportedMessageRepository) => void;
366
+ const pending = new Promise<ExportedMessageRepository>((res) => {
367
+ resolve = res;
368
+ });
369
+ const { result } = renderHook(() =>
370
+ useA2ARuntime({
371
+ client,
372
+ adapters: { history: { load: () => pending, append: async () => {} } },
373
+ }),
374
+ );
375
+ act(() => {
376
+ result.current.thread.append("Hello");
377
+ });
378
+ await waitFor(() => expect(streamMessage).toHaveBeenCalledOnce());
379
+ await act(async () => {
380
+ result.current.thread.cancelRun();
381
+ await new Promise((done) => setTimeout(done, 0));
382
+ });
383
+ const wasLoading = result.current.thread.getState().isLoading;
384
+ const restored = createThreadMessage("restored");
385
+ await act(async () => {
386
+ resolve({
387
+ headId: restored.id,
388
+ messages: [{ parentId: null, message: restored }],
389
+ });
390
+ await pending;
391
+ });
392
+ expect(wasLoading).toBe(true);
393
+ expect(result.current.thread.getState().messages).toEqual([restored]);
394
+ expect(result.current.thread.getState().isLoading).toBe(false);
395
+ });
396
+
397
+ it.each(["existing", "new"])(
398
+ "keeps the selected %s thread when initial history finishes later",
399
+ async (target) => {
400
+ const { client } = createMockClient();
401
+ let resolveHistory!: (repo: ExportedMessageRepository) => void;
402
+ const pending = new Promise<ExportedMessageRepository>((resolve) => {
403
+ resolveHistory = resolve;
404
+ });
405
+ const history = { load: () => pending, append: async () => {} };
406
+ const { result } = renderHook(() => {
407
+ const [threadId, setThreadId] = useState("thread-a");
408
+ return useA2ARuntime({
409
+ client,
410
+ adapters: {
411
+ history,
412
+ threadList: {
413
+ threadId,
414
+ onSwitchToThread: async (id) => {
415
+ setThreadId(id);
416
+ return { messages: [createThreadMessage("message-b")] };
417
+ },
418
+ onSwitchToNewThread: async () => {
419
+ setThreadId("thread-new");
420
+ },
421
+ },
422
+ },
423
+ });
424
+ });
425
+
426
+ await act(async () => {
427
+ if (target === "existing")
428
+ await result.current.threads.switchToThread("thread-b");
429
+ else await result.current.threads.switchToNewThread();
430
+ });
431
+ const selectedId = result.current.threads.getState().mainThreadId;
432
+ const selectedMessages = result.current.thread.getState().messages;
433
+ await act(async () => {
434
+ resolveHistory({
435
+ headId: "message-a",
436
+ messages: [
437
+ { parentId: null, message: createThreadMessage("message-a") },
438
+ ],
439
+ });
440
+ await pending;
441
+ });
442
+ expect(result.current.threads.getState().mainThreadId).toBe(selectedId);
443
+ expect(result.current.thread.getState().messages).toEqual(
444
+ selectedMessages,
445
+ );
446
+ expect(result.current.thread.getState().isLoading).toBe(false);
447
+ },
448
+ );
449
+
344
450
  it("ignores a thread load superseded by a new thread", async () => {
345
451
  const { client } = createMockClient();
346
452
  let resolveLoad!: (value: { messages: ThreadMessage[] }) => void;