@assistant-ui/ai-sdk 0.0.6 → 0.0.8
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/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 +2 -5
- 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 +6 -9
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -25
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +78 -8
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +10 -12
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +3 -6
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +22 -25
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +4 -7
- 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 +3 -6
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +2 -5
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +4 -7
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -32
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +26 -33
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +2 -5
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +72 -61
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +170 -56
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +8 -11
- 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 +100 -81
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +33 -13
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +8 -9
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +236 -58
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -4
- 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 +5 -8
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +24 -27
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +29 -32
- 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 -5
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +17 -20
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/resumable.d.ts +29 -32
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +12 -15
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +3 -6
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +2 -5
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +16 -14
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +616 -3
- package/src/converters/convertMessage.ts +187 -12
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.test.ts +4 -5
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +88 -51
- package/src/runtime/AISDKThreads.ts +4 -13
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +338 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +476 -4
- package/src/runtime/useAISDKRuntime.test.ts +282 -8
- package/src/runtime/useAISDKRuntime.ts +274 -46
- package/src/runtime/useAISDKRuntime.voice.test.tsx +319 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
- package/src/runtime/useChatRuntime.test.ts +77 -5
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +77 -10
- package/src/runtime/useExternalHistory.test.ts +989 -3
- package/src/runtime/useExternalHistory.ts +390 -24
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/tools/generativeTools.test.ts +7 -1
- package/src/usage.ts +1 -1
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { 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);
|
|
@@ -66,4 +94,448 @@ describe("useAISDKRuntime tool approvals", () => {
|
|
|
66
94
|
options: { metadata: undefined },
|
|
67
95
|
});
|
|
68
96
|
});
|
|
97
|
+
|
|
98
|
+
const setupPendingApproval = (
|
|
99
|
+
onRespondToToolApproval: NonNullable<
|
|
100
|
+
Parameters<typeof useAISDKRuntime>[1]
|
|
101
|
+
>["onRespondToToolApproval"],
|
|
102
|
+
) => {
|
|
103
|
+
const messages = [
|
|
104
|
+
{
|
|
105
|
+
id: "message-1",
|
|
106
|
+
role: "assistant",
|
|
107
|
+
parts: [
|
|
108
|
+
{
|
|
109
|
+
type: "tool-deploy",
|
|
110
|
+
toolCallId: "tool-1",
|
|
111
|
+
state: "approval-requested",
|
|
112
|
+
input: {},
|
|
113
|
+
approval: { id: "approval-1" },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
const setMessages = vi.fn();
|
|
119
|
+
const sendMessage = vi.fn();
|
|
120
|
+
const addToolApprovalResponse = vi.fn();
|
|
121
|
+
const chat = {
|
|
122
|
+
id: "chat-1",
|
|
123
|
+
status: "ready",
|
|
124
|
+
error: undefined,
|
|
125
|
+
messages,
|
|
126
|
+
setMessages,
|
|
127
|
+
sendMessage,
|
|
128
|
+
regenerate: vi.fn(),
|
|
129
|
+
addToolOutput: vi.fn(),
|
|
130
|
+
addToolApprovalResponse,
|
|
131
|
+
stop: vi.fn(),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
renderHook(() =>
|
|
135
|
+
useAISDKRuntime(chat as never, { onRespondToToolApproval }),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
respond: (response: {
|
|
140
|
+
approvalId: string;
|
|
141
|
+
approved: boolean;
|
|
142
|
+
optionId?: string;
|
|
143
|
+
text?: string;
|
|
144
|
+
reason?: string;
|
|
145
|
+
}) => mocks.adapter?.onRespondToToolApproval?.(response),
|
|
146
|
+
setMessages,
|
|
147
|
+
addToolApprovalResponse,
|
|
148
|
+
sendMessage,
|
|
149
|
+
messages,
|
|
150
|
+
getApproval: () =>
|
|
151
|
+
mocks.adapter?.messages?.[0]?.content.find(
|
|
152
|
+
(part) => part.type === "tool-call",
|
|
153
|
+
)?.approval,
|
|
154
|
+
getToolCall: () =>
|
|
155
|
+
mocks.adapter?.messages?.[0]?.content.find(
|
|
156
|
+
(part) => part.type === "tool-call",
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
it("stores a host answer after the handler resolves without starting a run", async () => {
|
|
162
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
163
|
+
const {
|
|
164
|
+
respond,
|
|
165
|
+
setMessages,
|
|
166
|
+
addToolApprovalResponse,
|
|
167
|
+
sendMessage,
|
|
168
|
+
messages,
|
|
169
|
+
getApproval,
|
|
170
|
+
} = setupPendingApproval(onRespondToToolApproval);
|
|
171
|
+
|
|
172
|
+
const response = {
|
|
173
|
+
approvalId: "approval-1",
|
|
174
|
+
approved: true,
|
|
175
|
+
optionId: "allow-session",
|
|
176
|
+
text: "Only for this environment",
|
|
177
|
+
reason: "Approved by operator",
|
|
178
|
+
};
|
|
179
|
+
await act(async () => {
|
|
180
|
+
await respond(response);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(onRespondToToolApproval).toHaveBeenCalledWith(response, {
|
|
184
|
+
toolCallId: "tool-1",
|
|
185
|
+
toolName: "deploy",
|
|
186
|
+
respondViaAISDK: expect.any(Function),
|
|
187
|
+
});
|
|
188
|
+
expect(addToolApprovalResponse).not.toHaveBeenCalled();
|
|
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
|
+
);
|
|
197
|
+
expect(getApproval()).toEqual({
|
|
198
|
+
id: "approval-1",
|
|
199
|
+
approved: true,
|
|
200
|
+
reason: "Approved by operator",
|
|
201
|
+
optionId: "allow-session",
|
|
202
|
+
text: "Only for this environment",
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("does not store a request the handler hands back through the AI SDK", async () => {
|
|
207
|
+
const { respond, addToolApprovalResponse } = setupPendingApproval(
|
|
208
|
+
(_response, { respondViaAISDK }) => respondViaAISDK(),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
await act(async () => {
|
|
212
|
+
await respond({
|
|
213
|
+
approvalId: "approval-1",
|
|
214
|
+
approved: false,
|
|
215
|
+
optionId: "reject-once",
|
|
216
|
+
reason: "Not now",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(addToolApprovalResponse).toHaveBeenCalledWith({
|
|
221
|
+
id: "approval-1",
|
|
222
|
+
approved: false,
|
|
223
|
+
reason: "Not now",
|
|
224
|
+
options: { metadata: undefined },
|
|
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
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("reopens a request when a handed-back AI SDK response fails inside the handler", async () => {
|
|
250
|
+
const { respond, addToolApprovalResponse, getApproval } =
|
|
251
|
+
setupPendingApproval(async (_response, { respondViaAISDK }) => {
|
|
252
|
+
await respondViaAISDK().catch(() => {});
|
|
253
|
+
});
|
|
254
|
+
addToolApprovalResponse.mockRejectedValueOnce(new Error("offline"));
|
|
255
|
+
|
|
256
|
+
await act(async () => {
|
|
257
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(getApproval()).toEqual({ id: "approval-1" });
|
|
261
|
+
await act(async () => {
|
|
262
|
+
await respond({ approvalId: "approval-1", approved: true });
|
|
263
|
+
});
|
|
264
|
+
expect(addToolApprovalResponse).toHaveBeenCalledTimes(2);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("keeps a host answer when the runtime switches chats and back", async () => {
|
|
268
|
+
const chatWith = (id: string, approvalId: string) => ({
|
|
269
|
+
id,
|
|
270
|
+
status: "ready",
|
|
271
|
+
error: undefined,
|
|
272
|
+
messages: [
|
|
273
|
+
{
|
|
274
|
+
id: `message-${id}`,
|
|
275
|
+
role: "assistant",
|
|
276
|
+
parts: [
|
|
277
|
+
{
|
|
278
|
+
type: "tool-deploy",
|
|
279
|
+
toolCallId: `tool-${id}`,
|
|
280
|
+
state: "approval-requested",
|
|
281
|
+
input: {},
|
|
282
|
+
approval: { id: approvalId },
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
setMessages: vi.fn(),
|
|
288
|
+
sendMessage: vi.fn(),
|
|
289
|
+
regenerate: vi.fn(),
|
|
290
|
+
addToolOutput: vi.fn(),
|
|
291
|
+
addToolApprovalResponse: vi.fn(),
|
|
292
|
+
stop: vi.fn(),
|
|
293
|
+
});
|
|
294
|
+
const chatA = chatWith("chat-a", "approval-a");
|
|
295
|
+
const chatB = chatWith("chat-b", "approval-b");
|
|
296
|
+
const onRespondToToolApproval = vi.fn(async () => {});
|
|
297
|
+
const { rerender } = renderHook(
|
|
298
|
+
({ chat }: { chat: typeof chatA }) =>
|
|
299
|
+
useAISDKRuntime(chat as never, { onRespondToToolApproval }),
|
|
300
|
+
{ initialProps: { chat: chatA } },
|
|
301
|
+
);
|
|
302
|
+
const respond = (approvalId: string) =>
|
|
303
|
+
act(async () => {
|
|
304
|
+
await mocks.adapter?.onRespondToToolApproval?.({
|
|
305
|
+
approvalId,
|
|
306
|
+
approved: true,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
const getApproval = () =>
|
|
310
|
+
mocks.adapter?.messages?.[0]?.content.find(
|
|
311
|
+
(part) => part.type === "tool-call",
|
|
312
|
+
)?.approval;
|
|
313
|
+
|
|
314
|
+
await respond("approval-a");
|
|
315
|
+
rerender({ chat: chatB });
|
|
316
|
+
await respond("approval-b");
|
|
317
|
+
rerender({ chat: chatA });
|
|
318
|
+
|
|
319
|
+
expect(getApproval()).toEqual({ id: "approval-a", approved: true });
|
|
320
|
+
await expect(respond("approval-a")).rejects.toThrow(
|
|
321
|
+
"Tool approval approval-a is not waiting for a response.",
|
|
322
|
+
);
|
|
323
|
+
expect(onRespondToToolApproval).toHaveBeenCalledTimes(2);
|
|
324
|
+
});
|
|
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("rejects an approval that is not waiting for a response", async () => {
|
|
474
|
+
const onRespondToToolApproval = vi.fn();
|
|
475
|
+
const { respond } = setupPendingApproval(onRespondToToolApproval);
|
|
476
|
+
|
|
477
|
+
await expect(
|
|
478
|
+
respond({ approvalId: "approval-2", approved: true }),
|
|
479
|
+
).rejects.toThrow(
|
|
480
|
+
"Tool approval approval-2 is not waiting for a response.",
|
|
481
|
+
);
|
|
482
|
+
expect(onRespondToToolApproval).not.toHaveBeenCalled();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("updates the rendered approval shape with the response channel", () => {
|
|
486
|
+
const onRespondToToolApproval = vi.fn();
|
|
487
|
+
const chat = {
|
|
488
|
+
id: "chat-1",
|
|
489
|
+
status: "ready",
|
|
490
|
+
error: undefined,
|
|
491
|
+
messages: [
|
|
492
|
+
{
|
|
493
|
+
id: "message-1",
|
|
494
|
+
role: "assistant",
|
|
495
|
+
parts: [
|
|
496
|
+
{
|
|
497
|
+
type: "tool-deploy",
|
|
498
|
+
toolCallId: "tool-1",
|
|
499
|
+
state: "approval-requested",
|
|
500
|
+
input: {},
|
|
501
|
+
approval: {
|
|
502
|
+
id: "approval-1",
|
|
503
|
+
display: "select",
|
|
504
|
+
options: [{ id: "allow-session", kind: "allow-once" }],
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
],
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
setMessages: vi.fn(),
|
|
511
|
+
sendMessage: vi.fn(),
|
|
512
|
+
regenerate: vi.fn(),
|
|
513
|
+
addToolOutput: vi.fn(),
|
|
514
|
+
addToolApprovalResponse: vi.fn(),
|
|
515
|
+
stop: vi.fn(),
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const { rerender } = renderHook(
|
|
519
|
+
({ useCustomHandler }: { useCustomHandler: boolean }) =>
|
|
520
|
+
useAISDKRuntime(chat as never, {
|
|
521
|
+
...(useCustomHandler && { onRespondToToolApproval }),
|
|
522
|
+
}),
|
|
523
|
+
{ initialProps: { useCustomHandler: false } },
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const getApproval = () =>
|
|
527
|
+
mocks.adapter?.messages?.[0]?.content.find(
|
|
528
|
+
(part) => part.type === "tool-call",
|
|
529
|
+
)?.approval;
|
|
530
|
+
|
|
531
|
+
expect(getApproval()).toEqual({ id: "approval-1" });
|
|
532
|
+
|
|
533
|
+
rerender({ useCustomHandler: true });
|
|
534
|
+
|
|
535
|
+
expect(getApproval()).toEqual({
|
|
536
|
+
id: "approval-1",
|
|
537
|
+
display: "select",
|
|
538
|
+
options: [{ id: "allow-session", kind: "allow-once" }],
|
|
539
|
+
});
|
|
540
|
+
});
|
|
69
541
|
});
|