@assistant-ui/react-google-adk 0.0.28 → 0.0.29

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,138 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, render, waitFor } from "@testing-library/react";
4
+ import { type FC } from "react";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
7
+ import type {
8
+ AssistantRuntime,
9
+ RemoteThreadListAdapter,
10
+ } from "@assistant-ui/core";
11
+ import { useAdkRuntime } from "./useAdkRuntime";
12
+ import type { AdkEvent } from "./types";
13
+
14
+ const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
15
+ list: vi.fn(async () => ({
16
+ threads: [
17
+ {
18
+ status: "regular" as const,
19
+ remoteId: "adk-1",
20
+ externalId: "adk-1",
21
+ title: "ADK session",
22
+ },
23
+ ],
24
+ })),
25
+ initialize: vi.fn(async () => ({
26
+ remoteId: "adk-1",
27
+ externalId: "adk-1",
28
+ })),
29
+ rename: vi.fn(async () => {}),
30
+ archive: vi.fn(async () => {}),
31
+ unarchive: vi.fn(async () => {}),
32
+ delete: vi.fn(async () => {}),
33
+ generateTitle: vi.fn(async () => new ReadableStream() as never),
34
+ fetch: vi.fn(async () => ({
35
+ status: "regular" as const,
36
+ remoteId: "adk-1",
37
+ externalId: "adk-1",
38
+ title: "ADK session",
39
+ })),
40
+ });
41
+
42
+ const deferred = () => {
43
+ let resolve!: () => void;
44
+ const promise = new Promise<void>((r) => {
45
+ resolve = r;
46
+ });
47
+ return { promise, resolve };
48
+ };
49
+
50
+ describe("useAdkRuntime replacement runs", () => {
51
+ it.each([
52
+ { label: "events after cancellation", cancelFirst: true, failFirst: false },
53
+ {
54
+ label: "events without cancellation",
55
+ cancelFirst: false,
56
+ failFirst: false,
57
+ },
58
+ {
59
+ label: "errors without cancellation",
60
+ cancelFirst: false,
61
+ failFirst: true,
62
+ },
63
+ ])("ignores superseded run $label", async ({ cancelFirst, failFirst }) => {
64
+ const gates = [deferred(), deferred()];
65
+ let calls = 0;
66
+ const stream = vi.fn(async function* (): AsyncGenerator<AdkEvent> {
67
+ const call = calls++;
68
+ await gates[call]!.promise;
69
+ if (call === 0 && failFirst) throw new Error("stale run failed");
70
+ yield {
71
+ id: `event-${call}`,
72
+ invocationId: `run-${call}`,
73
+ author: "agent",
74
+ content: { role: "model", parts: [{ text: `done-${call}` }] },
75
+ };
76
+ });
77
+ const sessionAdapter = makeThreadListAdapter();
78
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
79
+
80
+ const Inner: FC = () => {
81
+ const runtime = useAdkRuntime({
82
+ stream,
83
+ sessionAdapter,
84
+ unstable_allowCancellation: true,
85
+ });
86
+ capture.runtime = runtime;
87
+ return <AssistantRuntimeProvider runtime={runtime} />;
88
+ };
89
+
90
+ await act(async () => {
91
+ render(<Inner />);
92
+ });
93
+ await waitFor(() => expect(capture.runtime).not.toBeNull());
94
+ await act(async () => {
95
+ await capture.runtime!.threads.switchToThread("adk-1");
96
+ });
97
+
98
+ let firstSend!: Promise<void>;
99
+ act(() => {
100
+ firstSend = capture.runtime!.thread.append({
101
+ role: "user",
102
+ content: [{ type: "text", text: "first" }],
103
+ });
104
+ });
105
+ await waitFor(() => expect(stream).toHaveBeenCalledTimes(1));
106
+
107
+ let secondSend!: Promise<void>;
108
+ await act(async () => {
109
+ if (cancelFirst) await capture.runtime!.thread.cancelRun();
110
+ secondSend = capture.runtime!.thread.append({
111
+ role: "user",
112
+ content: [{ type: "text", text: "second" }],
113
+ });
114
+ });
115
+ await waitFor(() => expect(stream).toHaveBeenCalledTimes(2));
116
+
117
+ await act(async () => {
118
+ gates[0]!.resolve();
119
+ await firstSend;
120
+ });
121
+
122
+ const messagesAfterFirstSettles = JSON.stringify(
123
+ capture.runtime!.thread.getState().messages,
124
+ );
125
+ expect(messagesAfterFirstSettles).toContain("second");
126
+ expect(messagesAfterFirstSettles).not.toContain("done-0");
127
+ expect(capture.runtime!.thread.getState().isRunning).toBe(true);
128
+
129
+ await act(async () => {
130
+ gates[1]!.resolve();
131
+ await secondSend;
132
+ });
133
+ expect(
134
+ JSON.stringify(capture.runtime!.thread.getState().messages),
135
+ ).toContain("done-1");
136
+ expect(capture.runtime!.thread.getState().isRunning).toBe(false);
137
+ });
138
+ });
@@ -168,16 +168,18 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
168
168
  useInsertionEffect(() => {
169
169
  isRunningRef.current = effectiveIsRunning;
170
170
  }, [effectiveIsRunning]);
171
+ const runGenerationRef = useRef(0);
171
172
 
172
173
  const handleSendMessage = async (
173
174
  msgs: AdkMessage[],
174
175
  config: AdkSendMessageConfig,
175
176
  ) => {
177
+ const generation = ++runGenerationRef.current;
176
178
  try {
177
179
  setIsRunning(true);
178
180
  await sendMessage(msgs, config);
179
181
  } finally {
180
- setIsRunning(false);
182
+ if (runGenerationRef.current === generation) setIsRunning(false);
181
183
  }
182
184
  };
183
185