@assistant-ui/react-google-adk 0.0.34 → 0.0.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.
Files changed (65) hide show
  1. package/README.md +12 -2
  2. package/dist/AdkClient.d.ts +4 -0
  3. package/dist/AdkClient.d.ts.map +1 -1
  4. package/dist/AdkClient.js +12 -7
  5. package/dist/AdkClient.js.map +1 -1
  6. package/dist/AdkSessionAdapter.d.ts.map +1 -1
  7. package/dist/AdkSessionAdapter.js +7 -7
  8. package/dist/AdkSessionAdapter.js.map +1 -1
  9. package/dist/AdkThreadController.d.ts +15 -0
  10. package/dist/AdkThreadController.d.ts.map +1 -0
  11. package/dist/AdkThreadController.js +35 -0
  12. package/dist/AdkThreadController.js.map +1 -0
  13. package/dist/adkThreadState.d.ts +54 -0
  14. package/dist/adkThreadState.d.ts.map +1 -0
  15. package/dist/adkThreadState.js +93 -0
  16. package/dist/adkThreadState.js.map +1 -0
  17. package/dist/convertToAdkMessages.js +1 -1
  18. package/dist/convertToAdkMessages.js.map +1 -1
  19. package/dist/sdkIdentity.js +1 -1
  20. package/dist/server/createAdkApiRoute.d.ts +37 -6
  21. package/dist/server/createAdkApiRoute.d.ts.map +1 -1
  22. package/dist/server/createAdkApiRoute.js +55 -5
  23. package/dist/server/createAdkApiRoute.js.map +1 -1
  24. package/dist/server/parseAdkRequest.d.ts +4 -1
  25. package/dist/server/parseAdkRequest.d.ts.map +1 -1
  26. package/dist/server/parseAdkRequest.js +5 -1
  27. package/dist/server/parseAdkRequest.js.map +1 -1
  28. package/dist/useAdkMessages.d.ts +29 -4
  29. package/dist/useAdkMessages.d.ts.map +1 -1
  30. package/dist/useAdkMessages.js +62 -76
  31. package/dist/useAdkMessages.js.map +1 -1
  32. package/dist/useAdkRuntime.d.ts +7 -1
  33. package/dist/useAdkRuntime.d.ts.map +1 -1
  34. package/dist/useAdkRuntime.js +182 -54
  35. package/dist/useAdkRuntime.js.map +1 -1
  36. package/package.json +6 -5
  37. package/src/AdkClient.test.ts +78 -2
  38. package/src/AdkClient.ts +24 -6
  39. package/src/AdkSessionAdapter.ts +1 -1
  40. package/src/AdkThreadController.test.ts +90 -0
  41. package/src/AdkThreadController.ts +45 -0
  42. package/src/adkThreadState.test.ts +207 -0
  43. package/src/adkThreadState.ts +124 -0
  44. package/src/convertToAdkMessages.test.ts +19 -0
  45. package/src/convertToAdkMessages.ts +1 -1
  46. package/src/hooks.test.tsx +1 -0
  47. package/src/server/createAdkApiRoute.controls.test.ts +66 -0
  48. package/src/server/createAdkApiRoute.test.ts +282 -0
  49. package/src/server/createAdkApiRoute.ts +119 -11
  50. package/src/server/parseAdkRequest.test.ts +11 -3
  51. package/src/server/parseAdkRequest.ts +7 -1
  52. package/src/useAdkMessages.test.ts +43 -0
  53. package/src/useAdkMessages.ts +89 -96
  54. package/src/useAdkRuntime.cancellation.test.tsx +4 -3
  55. package/src/useAdkRuntime.cloud-options.test.tsx +59 -0
  56. package/src/useAdkRuntime.refetch.test.tsx +548 -4
  57. package/src/useAdkRuntime.replacement.test.tsx +718 -1
  58. package/src/useAdkRuntime.ts +253 -75
  59. package/src/useAdkRuntimeApproval.test.tsx +390 -35
  60. package/dist/raceWithAbortSignal.d.ts +0 -2
  61. package/dist/raceWithAbortSignal.d.ts.map +0 -1
  62. package/dist/raceWithAbortSignal.js +0 -45
  63. package/dist/raceWithAbortSignal.js.map +0 -1
  64. package/src/raceWithAbortSignal.test.ts +0 -73
  65. package/src/raceWithAbortSignal.ts +0 -48
@@ -28,11 +28,18 @@ describe("parseAdkRequest", () => {
28
28
  });
29
29
  });
30
30
 
31
- it("parses a message request with stateDelta", async () => {
31
+ it("parses a message request with sessionId and stateDelta", async () => {
32
32
  const result = await parseAdkRequest(
33
- makeRequest({ message: "Hello", stateDelta: { count: 1 } }),
33
+ makeRequest({
34
+ message: "Hello",
35
+ sessionId: "session-1",
36
+ stateDelta: { count: 1 },
37
+ }),
34
38
  );
35
- expect(result).toMatchObject({ stateDelta: { count: 1 } });
39
+ expect(result).toMatchObject({
40
+ sessionId: "session-1",
41
+ stateDelta: { count: 1 },
42
+ });
36
43
  });
37
44
 
38
45
  it("parses a message request with parts (multimodal)", async () => {
@@ -130,6 +137,7 @@ describe("parseAdkRequest", () => {
130
137
  [{ parts: [null] }, 'field "parts"'],
131
138
  [{ type: "unknown", message: "hello" }, 'field "type"'],
132
139
  [{ message: "hello", checkpointId: 42 }, 'field "checkpointId"'],
140
+ [{ message: "hello", sessionId: 42 }, 'field "sessionId"'],
133
141
  [{ message: "hello", stateDelta: [] }, 'field "stateDelta"'],
134
142
  ])("rejects malformed message requests %#", async (body, error) => {
135
143
  await expect(parseAdkRequest(makeRequest(body))).rejects.toThrow(error);
@@ -7,6 +7,7 @@ type ParsedAdkRequest =
7
7
  type: "message";
8
8
  text: string;
9
9
  parts?: Array<Record<string, unknown>> | undefined;
10
+ sessionId?: string | undefined;
10
11
  config: AdkSendMessageConfig;
11
12
  stateDelta?: Record<string, unknown> | undefined;
12
13
  }
@@ -16,6 +17,7 @@ type ParsedAdkRequest =
16
17
  toolName: string;
17
18
  result: unknown;
18
19
  isError: boolean;
20
+ sessionId?: string | undefined;
19
21
  config: AdkSendMessageConfig;
20
22
  stateDelta?: Record<string, unknown> | undefined;
21
23
  };
@@ -162,6 +164,7 @@ export const parseAdkRequest = async (
162
164
  if (body.runConfig !== undefined) config.runConfig = body.runConfig;
163
165
  const checkpointId = readOptionalString(body, "checkpointId");
164
166
  if (checkpointId !== undefined) config.checkpointId = checkpointId;
167
+ const sessionId = readOptionalString(body, "sessionId");
165
168
 
166
169
  const stateDelta = body.stateDelta;
167
170
  if (stateDelta !== undefined && !isRecord(stateDelta)) {
@@ -181,6 +184,7 @@ export const parseAdkRequest = async (
181
184
  toolName: readString(body, "toolName"),
182
185
  result: body.result,
183
186
  isError: body.isError ?? false,
187
+ ...(sessionId !== undefined && { sessionId }),
184
188
  config,
185
189
  ...(stateDelta != null && { stateDelta }),
186
190
  };
@@ -213,6 +217,7 @@ export const parseAdkRequest = async (
213
217
  type: "message",
214
218
  text: text ?? "",
215
219
  ...(parts !== undefined && { parts }),
220
+ ...(sessionId !== undefined && { sessionId }),
216
221
  config,
217
222
  ...(stateDelta != null && { stateDelta }),
218
223
  };
@@ -226,7 +231,8 @@ export const parseAdkRequest = async (
226
231
  * ```ts
227
232
  * const parsed = await parseAdkRequest(req);
228
233
  * const newMessage = toAdkContent(parsed);
229
- * const events = runner.runAsync({ userId, sessionId, newMessage, stateDelta: parsed.stateDelta });
234
+ * const stateDelta = validateSessionState(parsed.stateDelta);
235
+ * const events = runner.runAsync({ userId, sessionId, newMessage, stateDelta });
230
236
  * return adkEventStream(events);
231
237
  * ```
232
238
  */
@@ -1,3 +1,4 @@
1
+ /** @vitest-environment jsdom */
1
2
  import { act, renderHook } from "@testing-library/react";
2
3
  import { afterEach, describe, expect, it, vi } from "vitest";
3
4
 
@@ -17,6 +18,7 @@ import {
17
18
  messageToEvent,
18
19
  messagesToEvents,
19
20
  useAdkMessages,
21
+ useAdkMessagesInternal,
20
22
  } from "./useAdkMessages";
21
23
  import { projectAdkToolApprovals } from "./adkToolApproval";
22
24
  import { createAdkStream } from "./AdkClient";
@@ -212,6 +214,47 @@ describe("ADK runtime callbacks", () => {
212
214
  });
213
215
 
214
216
  describe("ADK stream lifecycle", () => {
217
+ it("reports streamed tool calls with the run config that produced them", async () => {
218
+ const runConfig = { custom: { model: "model-a" } };
219
+ const onMessages = vi.fn();
220
+ const stream: AdkStreamCallback = async function* () {
221
+ yield {
222
+ id: "event-1",
223
+ author: "agent",
224
+ content: {
225
+ role: "model",
226
+ parts: [
227
+ { functionCall: { id: "tool-1", name: "lookup", args: {} } },
228
+ { functionCall: { id: "tool-2", name: "search", args: {} } },
229
+ ],
230
+ },
231
+ };
232
+ };
233
+ const { result } = renderHook(() =>
234
+ useAdkMessagesInternal({ stream, onMessages }),
235
+ );
236
+
237
+ await act(async () => {
238
+ await result.current.sendMessage(
239
+ [{ id: "user-1", type: "human", content: "look it up" }],
240
+ { runConfig },
241
+ );
242
+ });
243
+
244
+ expect(onMessages).toHaveBeenLastCalledWith(
245
+ expect.arrayContaining([
246
+ expect.objectContaining({
247
+ type: "ai",
248
+ tool_calls: [
249
+ expect.objectContaining({ id: "tool-1" }),
250
+ expect.objectContaining({ id: "tool-2" }),
251
+ ],
252
+ }),
253
+ ]),
254
+ runConfig,
255
+ );
256
+ });
257
+
215
258
  it("settles a superseded send while its stream is still opening", async () => {
216
259
  const signals: AbortSignal[] = [];
217
260
  const parked = new Promise<AsyncGenerator<AdkEvent>>(() => {});
@@ -2,9 +2,9 @@ import { isRecord } from "@assistant-ui/core/internal";
2
2
  import {
3
3
  useState,
4
4
  useCallback,
5
- useInsertionEffect,
6
5
  useRef,
7
6
  useMemo,
7
+ useSyncExternalStore,
8
8
  } from "react";
9
9
  import { generateId } from "@assistant-ui/core";
10
10
  import { useReplaySafeEffect } from "@assistant-ui/store/internal";
@@ -15,16 +15,14 @@ import {
15
15
  openAbortableIterable,
16
16
  } from "@assistant-ui/core/internal";
17
17
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
18
+ import { AdkThreadController } from "./AdkThreadController";
18
19
  import { contentToParts } from "./contentToParts";
19
20
  import { toAdkFunctionResponse } from "./toAdkFunctionResponse";
20
21
  import type {
21
22
  AdkEvent,
22
23
  AdkMessage,
23
- AdkMessageMetadata,
24
24
  AdkSendMessageConfig,
25
25
  AdkStreamCallback,
26
- AdkToolConfirmation,
27
- AdkAuthRequest,
28
26
  AdkThreadSnapshot,
29
27
  OnAdkErrorCallback,
30
28
  OnAdkCustomEventCallback,
@@ -40,6 +38,10 @@ export type UseAdkMessagesOptions = {
40
38
  };
41
39
  };
42
40
 
41
+ type UseAdkMessagesInternalOptions = UseAdkMessagesOptions & {
42
+ onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
43
+ };
44
+
43
45
  type AdkRuntimeCallbackName = "onError" | "onCustomEvent" | "onAgentTransfer";
44
46
 
45
47
  const invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(
@@ -50,53 +52,34 @@ const invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(
50
52
  void invokeUserCallback("react-google-adk", name, callback, ...args);
51
53
  };
52
54
 
53
- export const useAdkMessages = ({
55
+ const useAdkMessagesInternal = ({
54
56
  stream,
55
57
  eventHandlers,
56
- }: UseAdkMessagesOptions) => {
57
- const [messages, _setMessages] = useState<AdkMessage[]>([]);
58
- const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});
59
- const [agentInfo, setAgentInfo] = useState<{
60
- name?: string | undefined;
61
- branch?: string | undefined;
62
- }>({});
63
- const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);
64
- const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(
65
- {},
58
+ onMessages,
59
+ }: UseAdkMessagesInternalOptions) => {
60
+ const [controller] = useState(() => new AdkThreadController());
61
+ const {
62
+ messages,
63
+ stateDelta,
64
+ agentInfo,
65
+ longRunningToolIds,
66
+ artifactDelta,
67
+ toolConfirmations,
68
+ authRequests,
69
+ escalated,
70
+ messageMetadata,
71
+ } = useSyncExternalStore(
72
+ controller.subscribe,
73
+ controller.getState,
74
+ controller.getState,
66
75
  );
67
- const [toolConfirmations, setToolConfirmations] = useState<
68
- AdkToolConfirmation[]
69
- >([]);
70
- const [authRequests, setAuthRequests] = useState<AdkAuthRequest[]>([]);
71
- const [escalated, setEscalated] = useState(false);
72
- const [messageMetadata, setMessageMetadata] = useState<
73
- Map<string, AdkMessageMetadata>
74
- >(new Map());
75
- // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.
76
- const messagesRef = useRef(messages);
77
- const longRunningToolIdsRef = useRef(longRunningToolIds);
78
- const stateDeltaRef = useRef(stateDelta);
79
- useInsertionEffect(() => {
80
- stateDeltaRef.current = stateDelta;
81
- }, [stateDelta]);
82
- const artifactDeltaRef = useRef(artifactDelta);
83
- useInsertionEffect(() => {
84
- artifactDeltaRef.current = artifactDelta;
85
- }, [artifactDelta]);
86
- const messageMetadataRef = useRef(messageMetadata);
87
- useInsertionEffect(() => {
88
- messageMetadataRef.current = messageMetadata;
89
- }, [messageMetadata]);
90
-
91
- const setMessagesImmediate = useCallback((msgs: AdkMessage[]) => {
92
- messagesRef.current = msgs;
93
- _setMessages(msgs);
94
- }, []);
95
- const setLongRunningToolIds = useCallback((ids: string[]) => {
96
- longRunningToolIdsRef.current = ids;
97
- _setLongRunningToolIds(ids);
98
- }, []);
99
76
 
77
+ const setMessagesImmediate = useCallback(
78
+ (msgs: AdkMessage[]) => {
79
+ controller.dispatch({ type: "messages.set", messages: msgs });
80
+ },
81
+ [controller],
82
+ );
100
83
  /**
101
84
  * Swap the thread over to a loaded snapshot in one commit. Unlike
102
85
  * {@link replaceMessages} this never passes through a cleared state, so a
@@ -105,17 +88,9 @@ export const useAdkMessages = ({
105
88
  */
106
89
  const applySnapshot = useCallback(
107
90
  (snapshot: AdkThreadSnapshot) => {
108
- setMessagesImmediate(snapshot.messages);
109
- setLongRunningToolIds(snapshot.longRunningToolIds ?? []);
110
- setToolConfirmations(snapshot.toolConfirmations ?? []);
111
- setAuthRequests(snapshot.authRequests ?? []);
112
- setEscalated(snapshot.escalated ?? false);
113
- setMessageMetadata(snapshot.messageMetadata ?? new Map());
114
- setStateDelta(snapshot.stateDelta ?? {});
115
- setArtifactDelta(snapshot.artifactDelta ?? {});
116
- setAgentInfo(snapshot.agentInfo ?? {});
91
+ controller.dispatch({ type: "snapshot.applied", snapshot });
117
92
  },
118
- [setLongRunningToolIds, setMessagesImmediate],
93
+ [controller],
119
94
  );
120
95
 
121
96
  // Replace the message list AND reset derived per-turn HITL state.
@@ -124,14 +99,9 @@ export const useAdkMessages = ({
124
99
  // into the next turn.
125
100
  const replaceMessages = useCallback(
126
101
  (msgs: AdkMessage[]) => {
127
- setMessagesImmediate(msgs);
128
- setLongRunningToolIds([]);
129
- setToolConfirmations([]);
130
- setAuthRequests([]);
131
- setEscalated(false);
132
- setMessageMetadata(new Map());
102
+ controller.dispatch({ type: "messages.replaced", messages: msgs });
133
103
  },
134
- [setLongRunningToolIds, setMessagesImmediate],
104
+ [controller],
135
105
  );
136
106
 
137
107
  const abortControllerRef = useRef<AbortController | null>(null);
@@ -159,9 +129,10 @@ export const useAdkMessages = ({
159
129
  m.type === "tool" ? [m.tool_call_id] : [],
160
130
  ),
161
131
  );
132
+ const currentState = controller.getState();
162
133
  const accumulator = new AdkEventAccumulator(
163
- messagesRef.current.filter((m) => !resentIds.has(m.id)),
164
- longRunningToolIdsRef.current.filter(
134
+ currentState.messages.filter((m) => !resentIds.has(m.id)),
135
+ currentState.longRunningToolIds.filter(
165
136
  (id) => !answeredToolCallIds.has(id),
166
137
  ),
167
138
  );
@@ -171,10 +142,13 @@ export const useAdkMessages = ({
171
142
  const initialMessages = accumulator.getMessages();
172
143
  const initialMessageIds = new Set(initialMessages.map((m) => m.id));
173
144
  const initialLongRunningToolIds = accumulator.getLongRunningToolIds();
174
- setMessagesImmediate(initialMessages);
175
- setLongRunningToolIds(initialLongRunningToolIds);
176
- setToolConfirmations(accumulator.getToolConfirmations());
177
- setAuthRequests(accumulator.getAuthRequests());
145
+ controller.dispatch({
146
+ type: "run.started",
147
+ messages: initialMessages,
148
+ longRunningToolIds: initialLongRunningToolIds,
149
+ toolConfirmations: accumulator.getToolConfirmations(),
150
+ authRequests: accumulator.getAuthRequests(),
151
+ });
178
152
  let lastTransferToAgent: string | undefined;
179
153
 
180
154
  // Google ADK replaces active runs, while React LangGraph queues sends.
@@ -206,28 +180,31 @@ export const useAdkMessages = ({
206
180
  break;
207
181
  }
208
182
  const updatedMessages = accumulator.processEvent(event);
209
- setMessagesImmediate(updatedMessages);
210
- setStateDelta({
211
- ...stateDeltaRef.current,
212
- ...accumulator.getStateDelta(),
213
- });
214
- setAgentInfo(accumulator.getAgentInfo());
215
- setLongRunningToolIds(accumulator.getLongRunningToolIds());
216
- setArtifactDelta({
217
- ...artifactDeltaRef.current,
218
- ...accumulator.getArtifactDelta(),
219
- });
220
- setToolConfirmations(accumulator.getToolConfirmations());
221
- setAuthRequests(accumulator.getAuthRequests());
222
- setEscalated(accumulator.isEscalated());
223
- {
224
- const newMeta = accumulator.getMessageMetadata();
225
- if (newMeta.size > 0) {
226
- setMessageMetadata(
227
- new Map([...messageMetadataRef.current, ...newMeta]),
228
- );
229
- }
183
+ // Each event part can append at most one message, and a function call
184
+ // stays on the current assistant message until a later part finalizes
185
+ // it, so every message touched by this event is within this tail.
186
+ const affectedMessageCount = Math.max(
187
+ event.content?.parts?.length ?? 0,
188
+ 1,
189
+ );
190
+ const affectedMessages = updatedMessages.slice(-affectedMessageCount);
191
+ if (affectedMessages.length > 0) {
192
+ onMessages?.(affectedMessages, config.runConfig);
230
193
  }
194
+ controller.dispatch({
195
+ type: "event.published",
196
+ state: {
197
+ messages: updatedMessages,
198
+ stateDelta: accumulator.getStateDelta(),
199
+ agentInfo: accumulator.getAgentInfo(),
200
+ longRunningToolIds: accumulator.getLongRunningToolIds(),
201
+ artifactDelta: accumulator.getArtifactDelta(),
202
+ toolConfirmations: accumulator.getToolConfirmations(),
203
+ authRequests: accumulator.getAuthRequests(),
204
+ escalated: accumulator.isEscalated(),
205
+ messageMetadata: accumulator.getMessageMetadata(),
206
+ },
207
+ });
231
208
 
232
209
  const transfer = accumulator.getLastTransferToAgent();
233
210
  if (transfer && transfer !== lastTransferToAgent) {
@@ -270,12 +247,13 @@ export const useAdkMessages = ({
270
247
  } finally {
271
248
  if (abortControllerRef.current === abortController) {
272
249
  if (abortController.signal.aborted) {
273
- setLongRunningToolIds(
274
- accumulator
250
+ controller.dispatch({
251
+ type: "longRunningToolIds.set",
252
+ ids: accumulator
275
253
  .getLongRunningToolIds()
276
254
  .filter((id) => initialLongRunningToolIds.includes(id)),
277
- );
278
- const updatedMessages = messagesRef.current;
255
+ });
256
+ const updatedMessages = controller.getState().messages;
279
257
  const lastAssistantMessage = updatedMessages.findLast(
280
258
  (m) => m.type === "ai",
281
259
  );
@@ -302,12 +280,13 @@ export const useAdkMessages = ({
302
280
  },
303
281
  [
304
282
  aui,
283
+ controller,
305
284
  setMessagesImmediate,
306
- setLongRunningToolIds,
307
285
  stream,
308
286
  onError,
309
287
  onCustomEvent,
310
288
  onAgentTransfer,
289
+ onMessages,
311
290
  ],
312
291
  );
313
292
 
@@ -320,6 +299,7 @@ export const useAdkMessages = ({
320
299
  useReplaySafeEffect(() => cancel, []);
321
300
 
322
301
  return {
302
+ controller,
323
303
  messages,
324
304
  stateDelta,
325
305
  agentInfo,
@@ -337,6 +317,19 @@ export const useAdkMessages = ({
337
317
  };
338
318
  };
339
319
 
320
+ export const useAdkMessages = ({
321
+ stream,
322
+ eventHandlers,
323
+ }: UseAdkMessagesOptions) => {
324
+ const { controller: _controller, ...result } = useAdkMessagesInternal({
325
+ stream,
326
+ ...(eventHandlers !== undefined && { eventHandlers }),
327
+ });
328
+ return result;
329
+ };
330
+
331
+ export { useAdkMessagesInternal };
332
+
340
333
  /**
341
334
  * Transport sends every human and tool message of one `send` call as a single
342
335
  * ADK `Content`, and ADK parses that event's function responses before running
@@ -11,6 +11,7 @@ import type {
11
11
  import { useAdkRuntime } from "./useAdkRuntime";
12
12
  import { useAdkLongRunningToolIds, useAdkSend } from "./hooks";
13
13
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
14
+ import { settleOutsideAct } from "./tests/settleOutsideAct";
14
15
  import type { AdkEvent, AdkStreamCallback, AdkThreadSnapshot } from "./types";
15
16
 
16
17
  const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
@@ -85,9 +86,9 @@ const renderAdkRuntime = async (
85
86
  render(<Inner />);
86
87
  });
87
88
  await waitFor(() => expect(capture.runtime).not.toBeNull());
88
- await act(async () => {
89
- await capture.runtime!.threads.switchToThread("adk-1");
90
- });
89
+ await settleOutsideAct(() =>
90
+ capture.runtime!.threads.switchToThread("adk-1"),
91
+ );
91
92
 
92
93
  return capture;
93
94
  };
@@ -0,0 +1,59 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { AssistantCloud } from "assistant-cloud";
6
+ import type { AssistantRuntime } from "@assistant-ui/core";
7
+
8
+ const mocks = vi.hoisted(() => ({
9
+ cloudAdapter: {},
10
+ runtime: {},
11
+ useCloudThreadListAdapter: vi.fn(() => ({})),
12
+ useRemoteThreadListRuntime: vi.fn(() => ({})),
13
+ }));
14
+
15
+ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
16
+ ...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
17
+ useCloudThreadListAdapter: mocks.useCloudThreadListAdapter,
18
+ useRemoteThreadListRuntime: mocks.useRemoteThreadListRuntime,
19
+ }));
20
+
21
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
22
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
23
+ useAui: () => ({
24
+ threadListItem: {
25
+ source: null,
26
+ getState: () => ({ externalId: undefined }),
27
+ initialize: vi.fn(),
28
+ },
29
+ }),
30
+ }));
31
+
32
+ import { ADK_SDK } from "./sdkIdentity";
33
+ import { useAdkRuntime } from "./useAdkRuntime";
34
+
35
+ describe("useAdkRuntime Cloud options", () => {
36
+ it("forwards the Cloud scope to the thread-list adapter", () => {
37
+ const cloud = {} as AssistantCloud;
38
+ mocks.useCloudThreadListAdapter.mockReturnValue(mocks.cloudAdapter);
39
+ mocks.useRemoteThreadListRuntime.mockReturnValue(
40
+ mocks.runtime as AssistantRuntime,
41
+ );
42
+
43
+ renderHook(() =>
44
+ useAdkRuntime({
45
+ stream: vi.fn(),
46
+ cloud,
47
+ scopeId: "workspace-1",
48
+ }),
49
+ );
50
+
51
+ expect(mocks.useCloudThreadListAdapter).toHaveBeenCalledWith(
52
+ expect.objectContaining({
53
+ cloud,
54
+ scopeId: "workspace-1",
55
+ sdk: ADK_SDK,
56
+ }),
57
+ );
58
+ });
59
+ });