@assistant-ui/ai-sdk 0.0.7 → 0.0.9
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.
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/adapters/vercelAttachmentAdapter.js +3 -3
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
- package/dist/aiSDKExtras.d.ts +4 -6
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +56 -12
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +6 -7
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +2 -4
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +20 -22
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +17 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.native.d.ts +15 -12
- package/dist/index.native.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.d.ts +2 -4
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -3
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -4
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKChat.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -31
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +22 -23
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +1 -3
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +76 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +311 -77
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +6 -8
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +14 -10
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +94 -89
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +44 -15
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +7 -7
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +231 -60
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -3
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useResourceCleanup.js +38 -14
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +3 -5
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +21 -23
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +24 -26
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.node.d.ts +2 -2
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +14 -16
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.js +9 -2
- package/dist/transport/AssistantChatTransport.js.map +1 -1
- package/dist/transport/resumable.d.ts +25 -27
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +8 -10
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +2 -4
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -3
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +541 -3
- package/src/converters/convertMessage.ts +123 -15
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.ts +0 -4
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +105 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
- package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +533 -60
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
- package/src/runtime/useChatRuntime.test.ts +11 -7
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.test.ts +166 -2
- package/src/runtime/useChatThread.transport.test.tsx +152 -0
- package/src/runtime/useChatThread.ts +89 -22
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +378 -15
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/transport/AssistantChatTransport.test.ts +164 -0
- package/src/transport/AssistantChatTransport.ts +22 -2
- package/src/usage.ts +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { act, renderHook } from "@testing-library/react";
|
|
4
|
-
import
|
|
5
|
-
|
|
3
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import {
|
|
5
|
+
bindExternalStoreMessage,
|
|
6
|
+
type ExternalStoreAdapter,
|
|
7
|
+
type ThreadAssistantMessage,
|
|
8
|
+
type ThreadMessage,
|
|
9
|
+
} from "@assistant-ui/core";
|
|
10
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
11
|
|
|
7
12
|
const mocks = vi.hoisted(() => ({
|
|
8
13
|
adapter: undefined as ExternalStoreAdapter | undefined,
|
|
14
|
+
persistToolApprovalResponses: vi.fn(),
|
|
15
|
+
threadMessages: [] as ThreadMessage[],
|
|
9
16
|
}));
|
|
10
17
|
|
|
11
18
|
vi.mock("@assistant-ui/core/react", async (importOriginal) => {
|
|
@@ -15,7 +22,11 @@ vi.mock("@assistant-ui/core/react", async (importOriginal) => {
|
|
|
15
22
|
...original,
|
|
16
23
|
useExternalStoreRuntime: vi.fn((adapter: ExternalStoreAdapter) => {
|
|
17
24
|
mocks.adapter = adapter;
|
|
18
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
thread: {
|
|
27
|
+
getState: () => ({ messages: mocks.threadMessages }),
|
|
28
|
+
},
|
|
29
|
+
} as never;
|
|
19
30
|
}),
|
|
20
31
|
useRuntimeAdapters: vi.fn(() => ({})),
|
|
21
32
|
};
|
|
@@ -29,13 +40,30 @@ vi.mock("./useExternalHistory", async (importOriginal) => {
|
|
|
29
40
|
useExternalHistory: vi.fn(() => ({
|
|
30
41
|
isLoading: false,
|
|
31
42
|
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
43
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
44
|
+
persistToolApprovalResponses: mocks.persistToolApprovalResponses,
|
|
32
45
|
})),
|
|
33
46
|
};
|
|
34
47
|
});
|
|
35
48
|
|
|
36
49
|
import { useAISDKRuntime } from "./useAISDKRuntime";
|
|
50
|
+
import { useExternalHistory } from "./useExternalHistory";
|
|
37
51
|
|
|
38
52
|
describe("useAISDKRuntime tool approvals", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mocks.adapter = undefined;
|
|
55
|
+
mocks.threadMessages = [];
|
|
56
|
+
mocks.persistToolApprovalResponses.mockReset().mockResolvedValue(undefined);
|
|
57
|
+
vi.mocked(useExternalHistory)
|
|
58
|
+
.mockReset()
|
|
59
|
+
.mockImplementation(() => ({
|
|
60
|
+
isLoading: false,
|
|
61
|
+
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
62
|
+
persistToolInteractions: vi.fn().mockResolvedValue(undefined),
|
|
63
|
+
persistToolApprovalResponses: mocks.persistToolApprovalResponses,
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
|
|
39
67
|
it("forwards the AI SDK approval promise to the external-store adapter", () => {
|
|
40
68
|
const approvalPromise = Promise.resolve();
|
|
41
69
|
const addToolApprovalResponse = vi.fn(() => approvalPromise);
|
|
@@ -88,6 +116,7 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
88
116
|
},
|
|
89
117
|
];
|
|
90
118
|
const setMessages = vi.fn();
|
|
119
|
+
const sendMessage = vi.fn();
|
|
91
120
|
const addToolApprovalResponse = vi.fn();
|
|
92
121
|
const chat = {
|
|
93
122
|
id: "chat-1",
|
|
@@ -95,7 +124,7 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
95
124
|
error: undefined,
|
|
96
125
|
messages,
|
|
97
126
|
setMessages,
|
|
98
|
-
sendMessage
|
|
127
|
+
sendMessage,
|
|
99
128
|
regenerate: vi.fn(),
|
|
100
129
|
addToolOutput: vi.fn(),
|
|
101
130
|
addToolApprovalResponse,
|
|
@@ -116,17 +145,29 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
116
145
|
}) => mocks.adapter?.onRespondToToolApproval?.(response),
|
|
117
146
|
setMessages,
|
|
118
147
|
addToolApprovalResponse,
|
|
148
|
+
sendMessage,
|
|
149
|
+
messages,
|
|
119
150
|
getApproval: () =>
|
|
120
151
|
mocks.adapter?.messages?.[0]?.content.find(
|
|
121
152
|
(part) => part.type === "tool-call",
|
|
122
153
|
)?.approval,
|
|
154
|
+
getToolCall: () =>
|
|
155
|
+
mocks.adapter?.messages?.[0]?.content.find(
|
|
156
|
+
(part) => part.type === "tool-call",
|
|
157
|
+
),
|
|
123
158
|
};
|
|
124
159
|
};
|
|
125
160
|
|
|
126
|
-
it("
|
|
161
|
+
it("stores a host answer after the handler resolves without starting a run", async () => {
|
|
127
162
|
const onRespondToToolApproval = vi.fn(async () => {});
|
|
128
|
-
const {
|
|
129
|
-
|
|
163
|
+
const {
|
|
164
|
+
respond,
|
|
165
|
+
setMessages,
|
|
166
|
+
addToolApprovalResponse,
|
|
167
|
+
sendMessage,
|
|
168
|
+
messages,
|
|
169
|
+
getApproval,
|
|
170
|
+
} = setupPendingApproval(onRespondToToolApproval);
|
|
130
171
|
|
|
131
172
|
const response = {
|
|
132
173
|
approvalId: "approval-1",
|
|
@@ -146,6 +187,13 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
146
187
|
});
|
|
147
188
|
expect(addToolApprovalResponse).not.toHaveBeenCalled();
|
|
148
189
|
expect(setMessages).not.toHaveBeenCalled();
|
|
190
|
+
expect(sendMessage).not.toHaveBeenCalled();
|
|
191
|
+
expect(mocks.persistToolApprovalResponses).toHaveBeenCalledExactlyOnceWith(
|
|
192
|
+
"message-1",
|
|
193
|
+
);
|
|
194
|
+
expect(messages[0]).not.toHaveProperty(
|
|
195
|
+
"metadata.__aui_toolApprovalResponses",
|
|
196
|
+
);
|
|
149
197
|
expect(getApproval()).toEqual({
|
|
150
198
|
id: "approval-1",
|
|
151
199
|
approved: true,
|
|
@@ -155,7 +203,7 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
155
203
|
});
|
|
156
204
|
});
|
|
157
205
|
|
|
158
|
-
it("
|
|
206
|
+
it("does not store a request the handler hands back through the AI SDK", async () => {
|
|
159
207
|
const { respond, addToolApprovalResponse } = setupPendingApproval(
|
|
160
208
|
(_response, { respondViaAISDK }) => respondViaAISDK(),
|
|
161
209
|
);
|
|
@@ -175,6 +223,27 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
175
223
|
reason: "Not now",
|
|
176
224
|
options: { metadata: undefined },
|
|
177
225
|
});
|
|
226
|
+
expect(mocks.persistToolApprovalResponses).not.toHaveBeenCalled();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("does not store a host answer when the handler rejects", async () => {
|
|
230
|
+
const { respond, messages, getApproval } = setupPendingApproval(
|
|
231
|
+
async () => {
|
|
232
|
+
throw new Error("resume failed");
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await expect(
|
|
237
|
+
act(async () => {
|
|
238
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
239
|
+
}),
|
|
240
|
+
).rejects.toThrow("resume failed");
|
|
241
|
+
|
|
242
|
+
expect(mocks.persistToolApprovalResponses).not.toHaveBeenCalled();
|
|
243
|
+
expect(getApproval()).toEqual({ id: "approval-1" });
|
|
244
|
+
expect(messages[0]).not.toHaveProperty(
|
|
245
|
+
"metadata.__aui_toolApprovalResponses",
|
|
246
|
+
);
|
|
178
247
|
});
|
|
179
248
|
|
|
180
249
|
it("reopens a request when a handed-back AI SDK response fails inside the handler", async () => {
|
|
@@ -254,6 +323,180 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
254
323
|
expect(onRespondToToolApproval).toHaveBeenCalledTimes(2);
|
|
255
324
|
});
|
|
256
325
|
|
|
326
|
+
it("restores a host answer and refuses a second response", async () => {
|
|
327
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
328
|
+
const { respond, getApproval, messages } = setupPendingApproval(
|
|
329
|
+
onRespondToToolApproval,
|
|
330
|
+
);
|
|
331
|
+
const historyCall = vi.mocked(useExternalHistory).mock.calls.at(-1)!;
|
|
332
|
+
const toolApprovalResponses = historyCall[9] as Map<
|
|
333
|
+
string,
|
|
334
|
+
{ approvalId: string; approved: boolean; reason?: string }
|
|
335
|
+
>;
|
|
336
|
+
const onToolApprovalResponsesRestored = historyCall[10] as () => void;
|
|
337
|
+
|
|
338
|
+
await act(async () => {
|
|
339
|
+
toolApprovalResponses.set("approval-1", {
|
|
340
|
+
approvalId: "approval-1",
|
|
341
|
+
approved: true,
|
|
342
|
+
reason: "Approved by operator",
|
|
343
|
+
});
|
|
344
|
+
onToolApprovalResponsesRestored();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await waitFor(() =>
|
|
348
|
+
expect(getApproval()).toEqual({
|
|
349
|
+
id: "approval-1",
|
|
350
|
+
approved: true,
|
|
351
|
+
reason: "Approved by operator",
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
await expect(
|
|
355
|
+
respond({ approvalId: "approval-1", approved: true }),
|
|
356
|
+
).rejects.toThrow(
|
|
357
|
+
"Tool approval approval-1 is not waiting for a response.",
|
|
358
|
+
);
|
|
359
|
+
expect(onRespondToToolApproval).not.toHaveBeenCalled();
|
|
360
|
+
expect(messages[0]).not.toHaveProperty(
|
|
361
|
+
"metadata.__aui_toolApprovalResponses",
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("reopens a restored approval when history clears its response map", async () => {
|
|
366
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
367
|
+
const { respond, getApproval } = setupPendingApproval(
|
|
368
|
+
onRespondToToolApproval,
|
|
369
|
+
);
|
|
370
|
+
const historyCall = vi.mocked(useExternalHistory).mock.calls.at(-1)!;
|
|
371
|
+
const toolApprovalResponses = historyCall[9] as Map<
|
|
372
|
+
string,
|
|
373
|
+
{ approvalId: string; approved: boolean }
|
|
374
|
+
>;
|
|
375
|
+
const onToolApprovalResponsesRestored = historyCall[10] as () => void;
|
|
376
|
+
|
|
377
|
+
await act(async () => {
|
|
378
|
+
toolApprovalResponses.set("approval-1", {
|
|
379
|
+
approvalId: "approval-1",
|
|
380
|
+
approved: true,
|
|
381
|
+
});
|
|
382
|
+
onToolApprovalResponsesRestored();
|
|
383
|
+
});
|
|
384
|
+
await waitFor(() =>
|
|
385
|
+
expect(getApproval()).toEqual({ id: "approval-1", approved: true }),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
await act(async () => {
|
|
389
|
+
toolApprovalResponses.clear();
|
|
390
|
+
onToolApprovalResponsesRestored();
|
|
391
|
+
});
|
|
392
|
+
await waitFor(() => expect(getApproval()).toEqual({ id: "approval-1" }));
|
|
393
|
+
|
|
394
|
+
await act(async () => {
|
|
395
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
396
|
+
});
|
|
397
|
+
expect(onRespondToToolApproval).toHaveBeenCalledOnce();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("clears deleted tool sidecars and its host approval reservation", async () => {
|
|
401
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
402
|
+
const { getApproval, getToolCall, messages, respond } =
|
|
403
|
+
setupPendingApproval(onRespondToToolApproval);
|
|
404
|
+
const threadMessage: ThreadAssistantMessage = {
|
|
405
|
+
id: "message-1",
|
|
406
|
+
role: "assistant",
|
|
407
|
+
content: [
|
|
408
|
+
{
|
|
409
|
+
type: "tool-call",
|
|
410
|
+
toolCallId: "tool-1",
|
|
411
|
+
toolName: "deploy",
|
|
412
|
+
args: {},
|
|
413
|
+
argsText: "{}",
|
|
414
|
+
result: undefined,
|
|
415
|
+
isError: false,
|
|
416
|
+
approval: { id: "approval-1" },
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
createdAt: new Date(),
|
|
420
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
421
|
+
metadata: {
|
|
422
|
+
unstable_state: null,
|
|
423
|
+
unstable_annotations: [],
|
|
424
|
+
unstable_data: [],
|
|
425
|
+
steps: [],
|
|
426
|
+
custom: {},
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
bindExternalStoreMessage(threadMessage, messages[0]!);
|
|
430
|
+
mocks.threadMessages = [threadMessage];
|
|
431
|
+
|
|
432
|
+
await act(async () => {
|
|
433
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
434
|
+
await mocks.adapter?.onAddToolResult?.({
|
|
435
|
+
messageId: "message-1",
|
|
436
|
+
toolCallId: "tool-1",
|
|
437
|
+
toolName: "deploy",
|
|
438
|
+
result: "deployed",
|
|
439
|
+
artifact: { preview: "deployment complete" },
|
|
440
|
+
isError: false,
|
|
441
|
+
});
|
|
442
|
+
await mocks.adapter?.unstable_onRecordToolInteraction?.({
|
|
443
|
+
messageId: "message-1",
|
|
444
|
+
toolCallId: "tool-1",
|
|
445
|
+
interaction: {
|
|
446
|
+
type: "action",
|
|
447
|
+
occurredAt: 1,
|
|
448
|
+
payload: { copied: true },
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
expect(getToolCall()).toMatchObject({
|
|
453
|
+
artifact: { preview: "deployment complete" },
|
|
454
|
+
unstable_interactions: {
|
|
455
|
+
entries: [{ type: "action", occurredAt: 1, payload: { copied: true } }],
|
|
456
|
+
},
|
|
457
|
+
approval: { id: "approval-1", approved: true },
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await act(async () => {
|
|
461
|
+
await mocks.adapter?.onDelete?.("message-1");
|
|
462
|
+
});
|
|
463
|
+
expect(getToolCall()).not.toHaveProperty("artifact");
|
|
464
|
+
expect(getToolCall()).not.toHaveProperty("unstable_interactions");
|
|
465
|
+
expect(getApproval()).toEqual({ id: "approval-1" });
|
|
466
|
+
|
|
467
|
+
await act(async () => {
|
|
468
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
469
|
+
});
|
|
470
|
+
expect(onRespondToToolApproval).toHaveBeenCalledTimes(2);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("answers through the host past an earlier approval-requested part without an approval", async () => {
|
|
474
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
475
|
+
const { respond, messages } = setupPendingApproval(onRespondToToolApproval);
|
|
476
|
+
messages.unshift({
|
|
477
|
+
id: "message-0",
|
|
478
|
+
role: "assistant",
|
|
479
|
+
parts: [
|
|
480
|
+
{
|
|
481
|
+
type: "tool-legacy",
|
|
482
|
+
toolCallId: "tool-0",
|
|
483
|
+
state: "approval-requested",
|
|
484
|
+
input: {},
|
|
485
|
+
} as (typeof messages)[number]["parts"][number],
|
|
486
|
+
],
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await act(async () => {
|
|
490
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
expect(onRespondToToolApproval).toHaveBeenCalledOnce();
|
|
494
|
+
expect(onRespondToToolApproval).toHaveBeenCalledWith(
|
|
495
|
+
expect.objectContaining({ approvalId: "approval-1" }),
|
|
496
|
+
expect.objectContaining({ toolCallId: "tool-1" }),
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
257
500
|
it("rejects an approval that is not waiting for a response", async () => {
|
|
258
501
|
const onRespondToToolApproval = vi.fn();
|
|
259
502
|
const { respond } = setupPendingApproval(onRespondToToolApproval);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { Activity, act, version } from "react";
|
|
4
|
+
import type { UIMessage } from "ai";
|
|
5
|
+
import type { SuggestionAdapter } from "@assistant-ui/core";
|
|
6
|
+
import { afterAll, afterEach, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
const onReact18 = version.startsWith("18.");
|
|
9
|
+
|
|
10
|
+
type Family = { current: unknown };
|
|
11
|
+
type RendererInternals = {
|
|
12
|
+
setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
|
|
13
|
+
scheduleRefresh: (
|
|
14
|
+
root: unknown,
|
|
15
|
+
update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
|
|
16
|
+
) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let renderer: RendererInternals | undefined;
|
|
20
|
+
const fiberRoots = new Set<unknown>();
|
|
21
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
22
|
+
vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
|
|
23
|
+
supportsFiber: true,
|
|
24
|
+
inject: (internals: RendererInternals) => {
|
|
25
|
+
renderer = internals;
|
|
26
|
+
return 1;
|
|
27
|
+
},
|
|
28
|
+
onScheduleFiberRoot: () => {},
|
|
29
|
+
onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
|
|
30
|
+
onCommitFiberUnmount: () => {},
|
|
31
|
+
});
|
|
32
|
+
const { cleanup, render, waitFor } = await import("@testing-library/react");
|
|
33
|
+
const { useAISDKRuntime } = await import("./useAISDKRuntime");
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
cleanup();
|
|
37
|
+
renderer!.setRefreshHandler(() => undefined);
|
|
38
|
+
fiberRoots.clear();
|
|
39
|
+
});
|
|
40
|
+
afterAll(() => vi.unstubAllGlobals());
|
|
41
|
+
|
|
42
|
+
const refresh = async (Before: unknown, After: unknown) => {
|
|
43
|
+
const family: Family = { current: After };
|
|
44
|
+
renderer!.setRefreshHandler((type) =>
|
|
45
|
+
type === Before || type === After ? family : undefined,
|
|
46
|
+
);
|
|
47
|
+
await act(async () => {
|
|
48
|
+
for (const root of fiberRoots) {
|
|
49
|
+
renderer!.scheduleRefresh(root, {
|
|
50
|
+
staleFamilies: new Set(),
|
|
51
|
+
updatedFamilies: new Set([family]),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
await act(async () => {});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const createChat = () => ({
|
|
59
|
+
id: "chat-1",
|
|
60
|
+
status: "submitted",
|
|
61
|
+
error: null,
|
|
62
|
+
messages: [
|
|
63
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
64
|
+
] as UIMessage[],
|
|
65
|
+
setMessages: vi.fn(),
|
|
66
|
+
sendMessage: vi.fn(async () => {}),
|
|
67
|
+
regenerate: vi.fn(async () => {}),
|
|
68
|
+
addToolResult: vi.fn(),
|
|
69
|
+
addToolOutput: vi.fn(),
|
|
70
|
+
stop: vi.fn(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const settleChat = (chat: ReturnType<typeof createChat>) => {
|
|
74
|
+
chat.status = "ready";
|
|
75
|
+
chat.messages = [
|
|
76
|
+
...chat.messages,
|
|
77
|
+
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "hello" }] },
|
|
78
|
+
];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const makeGeneration = () => {
|
|
82
|
+
let resolve!: (value: readonly { prompt: string }[]) => void;
|
|
83
|
+
const generate = vi.fn(
|
|
84
|
+
(_input: Parameters<SuggestionAdapter["generate"]>[0]) =>
|
|
85
|
+
new Promise<readonly { prompt: string }[]>((done) => {
|
|
86
|
+
resolve = done;
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
generate,
|
|
91
|
+
finish: (value: readonly { prompt: string }[]) => resolve(value),
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
it("completes AI SDK suggestion generation after Fast Refresh", async () => {
|
|
96
|
+
const chat = createChat();
|
|
97
|
+
const { generate, finish } = makeGeneration();
|
|
98
|
+
let runtime!: ReturnType<typeof useAISDKRuntime>;
|
|
99
|
+
let rendered = "";
|
|
100
|
+
const Before = () => {
|
|
101
|
+
rendered = "before";
|
|
102
|
+
runtime = useAISDKRuntime(chat as never, {
|
|
103
|
+
adapters: { suggestion: { generate } },
|
|
104
|
+
});
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
const After = () => {
|
|
108
|
+
rendered = "after";
|
|
109
|
+
runtime = useAISDKRuntime(chat as never, {
|
|
110
|
+
adapters: { suggestion: { generate } },
|
|
111
|
+
});
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
const view = render(<Before />);
|
|
115
|
+
settleChat(chat);
|
|
116
|
+
view.rerender(<Before />);
|
|
117
|
+
await waitFor(() => expect(generate).toHaveBeenCalledTimes(1));
|
|
118
|
+
const signal = generate.mock.calls[0]![0].signal!;
|
|
119
|
+
|
|
120
|
+
await refresh(Before, After);
|
|
121
|
+
expect(rendered).toBe("after");
|
|
122
|
+
expect(signal.aborted).toBe(false);
|
|
123
|
+
await act(async () => finish([{ prompt: "next" }]));
|
|
124
|
+
await waitFor(() =>
|
|
125
|
+
expect(runtime.thread.getState().suggestions).toEqual([{ prompt: "next" }]),
|
|
126
|
+
);
|
|
127
|
+
view.unmount();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("aborts pending AI SDK suggestion generation on unmount", async () => {
|
|
131
|
+
const chat = createChat();
|
|
132
|
+
const { generate } = makeGeneration();
|
|
133
|
+
const Host = () => {
|
|
134
|
+
useAISDKRuntime(chat as never, { adapters: { suggestion: { generate } } });
|
|
135
|
+
return null;
|
|
136
|
+
};
|
|
137
|
+
const view = render(<Host />);
|
|
138
|
+
settleChat(chat);
|
|
139
|
+
view.rerender(<Host />);
|
|
140
|
+
await waitFor(() => expect(generate).toHaveBeenCalledTimes(1));
|
|
141
|
+
const signal = generate.mock.calls[0]![0].signal!;
|
|
142
|
+
|
|
143
|
+
view.unmount();
|
|
144
|
+
await act(async () => {});
|
|
145
|
+
expect(signal.aborted).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Activity is React 19 only.
|
|
149
|
+
it.skipIf(onReact18)(
|
|
150
|
+
"aborts pending AI SDK suggestion generation when Activity hides",
|
|
151
|
+
async () => {
|
|
152
|
+
const chat = createChat();
|
|
153
|
+
const { generate } = makeGeneration();
|
|
154
|
+
const Host = () => {
|
|
155
|
+
useAISDKRuntime(chat as never, {
|
|
156
|
+
adapters: { suggestion: { generate } },
|
|
157
|
+
});
|
|
158
|
+
return null;
|
|
159
|
+
};
|
|
160
|
+
const view = render(
|
|
161
|
+
<Activity mode="visible">
|
|
162
|
+
<Host />
|
|
163
|
+
</Activity>,
|
|
164
|
+
);
|
|
165
|
+
settleChat(chat);
|
|
166
|
+
view.rerender(
|
|
167
|
+
<Activity mode="visible">
|
|
168
|
+
<Host />
|
|
169
|
+
</Activity>,
|
|
170
|
+
);
|
|
171
|
+
await waitFor(() => expect(generate).toHaveBeenCalledTimes(1));
|
|
172
|
+
const signal = generate.mock.calls[0]![0].signal!;
|
|
173
|
+
|
|
174
|
+
view.rerender(
|
|
175
|
+
<Activity mode="hidden">
|
|
176
|
+
<Host />
|
|
177
|
+
</Activity>,
|
|
178
|
+
);
|
|
179
|
+
await act(async () => {});
|
|
180
|
+
expect(signal.aborted).toBe(true);
|
|
181
|
+
},
|
|
182
|
+
);
|