@happyvertical/smrt-chat 0.53.1 → 0.53.2
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/dist/index.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/package.json +13 -13
- package/dist/__tests__/chat-test-db.d.ts +0 -8
- package/dist/__tests__/chat-test-db.d.ts.map +0 -1
- package/dist/__tests__/raw-collections.d.ts +0 -35
- package/dist/__tests__/raw-collections.d.ts.map +0 -1
- package/dist/svelte/components/agent/__tests__/ToolCallDisplay.test.js +0 -135
- package/dist/svelte/components/agent/message-blocks.test.js +0 -31
- package/dist/svelte/components/assistant/__tests__/AssistantComposer.test.js +0 -75
- package/dist/svelte/components/assistant/__tests__/AssistantDock.test.js +0 -515
- package/dist/svelte/components/assistant/__tests__/AssistantThreadList.test.js +0 -45
- package/dist/svelte/components/assistant/__tests__/action-status.test.js +0 -25
- package/dist/svelte/components/assistant/__tests__/assistant-composer-dictation.test.js +0 -197
- package/dist/svelte/components/assistant/__tests__/assistant-dock-action-outcome.test.js +0 -320
- package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-pending.test.js +0 -224
- package/dist/svelte/components/assistant/__tests__/assistant-dock-choices-preview.test.js +0 -312
- package/dist/svelte/components/assistant/__tests__/assistant-dock-choices.test.js +0 -338
- package/dist/svelte/components/assistant/__tests__/assistant-dock-client-tools.test.js +0 -442
- package/dist/svelte/components/assistant/__tests__/assistant-dock-draft-settled.test.js +0 -448
- package/dist/svelte/components/assistant/__tests__/assistant-dock-host-actions.test.js +0 -185
- package/dist/svelte/components/assistant/__tests__/assistant-dock-run.test.js +0 -303
- package/dist/svelte/components/assistant/__tests__/assistant-dock-settle.test.js +0 -146
- package/dist/svelte/components/assistant/__tests__/assistant-transport.test.js +0 -253
- package/dist/svelte/components/assistant/__tests__/attachment-href.test.js +0 -31
- package/dist/svelte/components/assistant/__tests__/create-assistant-dock-controller.test.js +0 -2491
- package/dist/svelte/components/layout/__tests__/ChatLayout.test.js +0 -67
- package/dist/svelte/components/messages/__tests__/MessageInput.test.js +0 -38
- package/dist/svelte/components/messages/__tests__/MessageItem.test.js +0 -108
- package/dist/svelte/components/shared/__tests__/Avatar.test.js +0 -20
- package/dist/svelte/components/shared/__tests__/LinkPreview.test.js +0 -29
- package/dist/svelte/components/shared/__tests__/MentionAutocomplete.test.js +0 -66
- package/dist/svelte/components/shared/__tests__/ModelPicker.test.js +0 -36
- package/dist/svelte/components/shared/__tests__/ReactionPicker.test.js +0 -35
- package/dist/svelte/components/shared/__tests__/ReadReceipts.test.js +0 -28
- package/dist/svelte/components/shared/__tests__/UserPresence.test.js +0 -23
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* #2908: streamed turns and browser-executed tools in the dock controller —
|
|
4
|
-
* the client-tool round trip through a transport that suspends and resumes,
|
|
5
|
-
* the effect rules (read runs, write waits for the user unless a consent-gated
|
|
6
|
-
* registry owns it, destructive always waits), cancellation, the generic
|
|
7
|
-
* status, and the dock's own proposal tool.
|
|
8
|
-
*/
|
|
9
|
-
import { createDataSurfaceRegistry, } from '@happyvertical/smrt-ui/data-surface';
|
|
10
|
-
import { render, screen, userEvent } from '@happyvertical/smrt-vitest/svelte';
|
|
11
|
-
import { flushSync } from 'svelte';
|
|
12
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
-
import AssistantDock from '../AssistantDock.svelte';
|
|
14
|
-
import { ASSISTANT_PROPOSE_ACTION_TOOL, defaultClientToolPolicy, } from '../client-tools.js';
|
|
15
|
-
import { createAssistantDockController } from '../create-assistant-dock-controller.svelte.js';
|
|
16
|
-
function message(id, role, content) {
|
|
17
|
-
return {
|
|
18
|
-
id,
|
|
19
|
-
threadId: 't1',
|
|
20
|
-
role,
|
|
21
|
-
content,
|
|
22
|
-
createdAt: new Date(0).toISOString(),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* A transport whose first send suspends on `calls`, and whose resume answers
|
|
27
|
-
* with a reply. Records what the dock sent.
|
|
28
|
-
*/
|
|
29
|
-
function suspendingTransport(calls) {
|
|
30
|
-
const sent = [];
|
|
31
|
-
const resumed = [];
|
|
32
|
-
const transport = {
|
|
33
|
-
sent,
|
|
34
|
-
resumed,
|
|
35
|
-
async listThreads() {
|
|
36
|
-
return [{ id: 't1', title: 'T', isResolved: false, messageCount: 0 }];
|
|
37
|
-
},
|
|
38
|
-
async createThread() {
|
|
39
|
-
return { id: 't1', title: 'T', isResolved: false, messageCount: 0 };
|
|
40
|
-
},
|
|
41
|
-
async loadMessages() {
|
|
42
|
-
return [];
|
|
43
|
-
},
|
|
44
|
-
async uploadAttachment() {
|
|
45
|
-
throw new Error('no');
|
|
46
|
-
},
|
|
47
|
-
async sendMessage(input) {
|
|
48
|
-
sent.push(input);
|
|
49
|
-
input.onEvent?.({
|
|
50
|
-
type: 'status',
|
|
51
|
-
status: { state: 'working', label: 'Thinking…' },
|
|
52
|
-
});
|
|
53
|
-
input.onEvent?.({
|
|
54
|
-
type: 'step',
|
|
55
|
-
step: {
|
|
56
|
-
kind: 'tool_call',
|
|
57
|
-
callId: calls[0]?.id ?? 'x',
|
|
58
|
-
tool: calls[0]?.name ?? 'x',
|
|
59
|
-
label: 'Looking',
|
|
60
|
-
location: 'client',
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
return {
|
|
64
|
-
inProgress: false,
|
|
65
|
-
userMessage: message('u1', 'user', input.content),
|
|
66
|
-
clientToolCalls: { continuationId: 'cont-1', calls },
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
async resumeTurn(input) {
|
|
70
|
-
resumed.push(input);
|
|
71
|
-
if (input.signal?.aborted) {
|
|
72
|
-
throw Object.assign(new Error('aborted'), { name: 'AbortError' });
|
|
73
|
-
}
|
|
74
|
-
input.onEvent?.({ type: 'token', text: 'All ' });
|
|
75
|
-
const reply = message('a1', 'assistant', 'All done.');
|
|
76
|
-
input.onEvent?.({ type: 'message', message: reply });
|
|
77
|
-
return { inProgress: false, assistantMessage: reply };
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
return transport;
|
|
81
|
-
}
|
|
82
|
-
function pageTools(tools, outputs = {}) {
|
|
83
|
-
const executed = [];
|
|
84
|
-
const source = {
|
|
85
|
-
executed,
|
|
86
|
-
list: () => tools,
|
|
87
|
-
async execute(name, args) {
|
|
88
|
-
executed.push({ name, args });
|
|
89
|
-
return outputs[name] ?? '{"ok":true}';
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
return source;
|
|
93
|
-
}
|
|
94
|
-
const READ_TOOL = {
|
|
95
|
-
name: 'smrt_ui_list_form_controls',
|
|
96
|
-
description: 'List the form fields.',
|
|
97
|
-
inputSchema: { type: 'object' },
|
|
98
|
-
effect: 'read',
|
|
99
|
-
owner: 'ui',
|
|
100
|
-
};
|
|
101
|
-
const STAGE_TOOL = {
|
|
102
|
-
name: 'smrt_ui_execute_form_control',
|
|
103
|
-
description: 'Fill in a form field.',
|
|
104
|
-
inputSchema: { type: 'object' },
|
|
105
|
-
effect: 'write',
|
|
106
|
-
owner: 'ui',
|
|
107
|
-
proposal: true,
|
|
108
|
-
};
|
|
109
|
-
/** A bespoke tool that set the public `owner: 'intent'` diagnostic label. */
|
|
110
|
-
const SPOOFED_INTENT_TOOL = {
|
|
111
|
-
name: 'wire_money',
|
|
112
|
-
description: 'Acts directly.',
|
|
113
|
-
inputSchema: { type: 'object' },
|
|
114
|
-
effect: 'write',
|
|
115
|
-
owner: 'intent',
|
|
116
|
-
proposal: false,
|
|
117
|
-
};
|
|
118
|
-
const WRITE_TOOL = {
|
|
119
|
-
name: 'articles_update',
|
|
120
|
-
description: 'Update an article.',
|
|
121
|
-
inputSchema: { type: 'object' },
|
|
122
|
-
effect: 'write',
|
|
123
|
-
owner: 'generated',
|
|
124
|
-
};
|
|
125
|
-
const DELETE_TOOL = {
|
|
126
|
-
name: 'articles_delete',
|
|
127
|
-
description: 'Delete an article.',
|
|
128
|
-
inputSchema: { type: 'object' },
|
|
129
|
-
effect: 'destructive',
|
|
130
|
-
owner: 'generated',
|
|
131
|
-
};
|
|
132
|
-
async function openController(transport, tools, extra = {}) {
|
|
133
|
-
const controller = createAssistantDockController({
|
|
134
|
-
transport,
|
|
135
|
-
registry: createDataSurfaceRegistry(),
|
|
136
|
-
pageTools: tools,
|
|
137
|
-
...extra,
|
|
138
|
-
});
|
|
139
|
-
await controller.loadThreads();
|
|
140
|
-
await controller.openThread('t1');
|
|
141
|
-
return controller;
|
|
142
|
-
}
|
|
143
|
-
async function until(check) {
|
|
144
|
-
for (let i = 0; i < 50 && !check(); i += 1) {
|
|
145
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
describe('dock browser tools (#2908)', () => {
|
|
149
|
-
it('offers the page tools and runs a read call without asking', async () => {
|
|
150
|
-
const transport = suspendingTransport([
|
|
151
|
-
{ id: 'c1', name: READ_TOOL.name, args: {}, effect: 'read' },
|
|
152
|
-
]);
|
|
153
|
-
const tools = pageTools([READ_TOOL], {
|
|
154
|
-
[READ_TOOL.name]: '{"fields":["title"]}',
|
|
155
|
-
});
|
|
156
|
-
const controller = await openController(transport, tools);
|
|
157
|
-
await controller.send('what fields are there?');
|
|
158
|
-
expect(transport.sent[0].clientTools?.map((t) => t.name)).toEqual([
|
|
159
|
-
READ_TOOL.name,
|
|
160
|
-
]);
|
|
161
|
-
expect(tools.executed).toEqual([{ name: READ_TOOL.name, args: {} }]);
|
|
162
|
-
expect(transport.resumed[0]).toMatchObject({
|
|
163
|
-
continuationId: 'cont-1',
|
|
164
|
-
clientRequestId: transport.sent[0].clientRequestId,
|
|
165
|
-
results: [{ id: 'c1', ok: true, result: '{"fields":["title"]}' }],
|
|
166
|
-
});
|
|
167
|
-
expect(controller.messages.map((m) => m.id)).toEqual(['u1', 'a1']);
|
|
168
|
-
expect(controller.pendingSends).toEqual([]);
|
|
169
|
-
expect(controller.status).toMatchObject({
|
|
170
|
-
state: 'done',
|
|
171
|
-
label: 'Done',
|
|
172
|
-
changes: 0,
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
it('runs a consent-gated registry write (form staging) without asking', async () => {
|
|
176
|
-
const transport = suspendingTransport([
|
|
177
|
-
{
|
|
178
|
-
id: 'c1',
|
|
179
|
-
name: STAGE_TOOL.name,
|
|
180
|
-
args: { action: 'stage' },
|
|
181
|
-
effect: 'write',
|
|
182
|
-
},
|
|
183
|
-
]);
|
|
184
|
-
const tools = pageTools([STAGE_TOOL]);
|
|
185
|
-
const controller = await openController(transport, tools);
|
|
186
|
-
await controller.send('fill in the title');
|
|
187
|
-
expect(tools.executed).toHaveLength(1);
|
|
188
|
-
expect(controller.toolRequests).toEqual([]);
|
|
189
|
-
});
|
|
190
|
-
it('still asks for a write whose owner label claims intent without the proposal brand', async () => {
|
|
191
|
-
expect(defaultClientToolPolicy(SPOOFED_INTENT_TOOL)).toBe('confirm');
|
|
192
|
-
expect(defaultClientToolPolicy({ effect: 'write', owner: 'ui' })).toBe('confirm');
|
|
193
|
-
expect(defaultClientToolPolicy(STAGE_TOOL)).toBe('run');
|
|
194
|
-
const transport = suspendingTransport([
|
|
195
|
-
{ id: 'c1', name: SPOOFED_INTENT_TOOL.name, args: {}, effect: 'write' },
|
|
196
|
-
]);
|
|
197
|
-
const tools = pageTools([SPOOFED_INTENT_TOOL]);
|
|
198
|
-
const controller = await openController(transport, tools);
|
|
199
|
-
const sending = controller.send('send it');
|
|
200
|
-
await until(() => controller.toolRequests.length > 0);
|
|
201
|
-
expect(controller.toolRequests[0]).toMatchObject({ status: 'waiting' });
|
|
202
|
-
expect(tools.executed).toEqual([]);
|
|
203
|
-
controller.declineToolRequest('c1');
|
|
204
|
-
await sending;
|
|
205
|
-
});
|
|
206
|
-
it('waits for the user before an acting write, and reports a decline', async () => {
|
|
207
|
-
const transport = suspendingTransport([
|
|
208
|
-
{ id: 'c1', name: WRITE_TOOL.name, args: { id: 'a' }, effect: 'write' },
|
|
209
|
-
]);
|
|
210
|
-
const tools = pageTools([WRITE_TOOL]);
|
|
211
|
-
const controller = await openController(transport, tools);
|
|
212
|
-
const sending = controller.send('rename it');
|
|
213
|
-
await until(() => controller.toolRequests.length > 0);
|
|
214
|
-
expect(controller.toolRequests[0]).toMatchObject({
|
|
215
|
-
status: 'waiting',
|
|
216
|
-
description: 'Update an article.',
|
|
217
|
-
});
|
|
218
|
-
expect(controller.status).toMatchObject({
|
|
219
|
-
state: 'working',
|
|
220
|
-
label: 'Waiting for your OK…',
|
|
221
|
-
});
|
|
222
|
-
expect(tools.executed).toEqual([]);
|
|
223
|
-
controller.declineToolRequest('c1');
|
|
224
|
-
await sending;
|
|
225
|
-
expect(tools.executed).toEqual([]);
|
|
226
|
-
expect(transport.resumed[0].results).toEqual([
|
|
227
|
-
{ id: 'c1', ok: false, error: 'declined' },
|
|
228
|
-
]);
|
|
229
|
-
});
|
|
230
|
-
it('runs an acting write once the user allows it and counts the change', async () => {
|
|
231
|
-
const transport = suspendingTransport([
|
|
232
|
-
{ id: 'c1', name: WRITE_TOOL.name, args: { id: 'a' }, effect: 'write' },
|
|
233
|
-
]);
|
|
234
|
-
const tools = pageTools([WRITE_TOOL]);
|
|
235
|
-
const controller = await openController(transport, tools);
|
|
236
|
-
const sending = controller.send('rename it');
|
|
237
|
-
await until(() => controller.toolRequests.length > 0);
|
|
238
|
-
controller.approveToolRequest('c1');
|
|
239
|
-
await sending;
|
|
240
|
-
expect(tools.executed).toHaveLength(1);
|
|
241
|
-
expect(controller.status).toMatchObject({ state: 'done', changes: 1 });
|
|
242
|
-
});
|
|
243
|
-
it('never auto-runs a destructive tool, even when the host policy says run', async () => {
|
|
244
|
-
const transport = suspendingTransport([
|
|
245
|
-
{ id: 'c1', name: DELETE_TOOL.name, args: { id: 'a' }, effect: 'read' },
|
|
246
|
-
]);
|
|
247
|
-
const tools = pageTools([DELETE_TOOL]);
|
|
248
|
-
const controller = await openController(transport, tools, {
|
|
249
|
-
clientToolPolicy: () => 'run',
|
|
250
|
-
});
|
|
251
|
-
const sending = controller.send('delete it');
|
|
252
|
-
await until(() => controller.toolRequests.length > 0);
|
|
253
|
-
// The server echoed `read`, but the page registry says destructive.
|
|
254
|
-
expect(controller.toolRequests[0]).toMatchObject({
|
|
255
|
-
effect: 'destructive',
|
|
256
|
-
status: 'waiting',
|
|
257
|
-
});
|
|
258
|
-
expect(tools.executed).toEqual([]);
|
|
259
|
-
controller.declineToolRequest('c1');
|
|
260
|
-
await sending;
|
|
261
|
-
});
|
|
262
|
-
it('answers a call to a tool the page no longer has with not_available', async () => {
|
|
263
|
-
const transport = suspendingTransport([
|
|
264
|
-
{ id: 'c1', name: 'gone_tool', args: {}, effect: 'read' },
|
|
265
|
-
]);
|
|
266
|
-
const controller = await openController(transport, pageTools([READ_TOOL]));
|
|
267
|
-
await controller.send('go');
|
|
268
|
-
expect(transport.resumed[0].results).toEqual([
|
|
269
|
-
{ id: 'c1', ok: false, error: 'not_available' },
|
|
270
|
-
]);
|
|
271
|
-
});
|
|
272
|
-
it('cancel() declines waiting calls, stops the turn, and reports Stopped', async () => {
|
|
273
|
-
const transport = suspendingTransport([
|
|
274
|
-
{ id: 'c1', name: WRITE_TOOL.name, args: {}, effect: 'write' },
|
|
275
|
-
]);
|
|
276
|
-
const tools = pageTools([WRITE_TOOL]);
|
|
277
|
-
const controller = await openController(transport, tools);
|
|
278
|
-
const sending = controller.send('rename it');
|
|
279
|
-
await until(() => controller.toolRequests.length > 0);
|
|
280
|
-
expect(controller.status.cancellable).toBe(true);
|
|
281
|
-
controller.cancel();
|
|
282
|
-
await sending;
|
|
283
|
-
expect(tools.executed).toEqual([]);
|
|
284
|
-
expect(transport.resumed).toEqual([]);
|
|
285
|
-
expect(controller.pendingSends).toEqual([]);
|
|
286
|
-
expect(controller.status).toMatchObject({
|
|
287
|
-
state: 'idle',
|
|
288
|
-
label: 'Stopped',
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
it('reports status changes to onStatus', async () => {
|
|
292
|
-
const seen = [];
|
|
293
|
-
const transport = suspendingTransport([
|
|
294
|
-
{ id: 'c1', name: READ_TOOL.name, args: {}, effect: 'read' },
|
|
295
|
-
]);
|
|
296
|
-
const controller = await openController(transport, pageTools([READ_TOOL]), {
|
|
297
|
-
onStatus: (status) => seen.push(status),
|
|
298
|
-
});
|
|
299
|
-
await controller.send('hi');
|
|
300
|
-
flushSync();
|
|
301
|
-
expect(seen.some((s) => s.state === 'working')).toBe(true);
|
|
302
|
-
expect(seen.at(-1)).toMatchObject({ state: 'done' });
|
|
303
|
-
controller.dispose();
|
|
304
|
-
});
|
|
305
|
-
it('proposes a data-surface action through the dock’s own tool, never applying it', async () => {
|
|
306
|
-
const registry = createDataSurfaceRegistry();
|
|
307
|
-
const identity = { surfaceId: 'articles', kind: 'table' };
|
|
308
|
-
const descriptor = {
|
|
309
|
-
version: 1,
|
|
310
|
-
identity,
|
|
311
|
-
schemaVersion: 1,
|
|
312
|
-
label: 'Articles',
|
|
313
|
-
rowKey: 'id',
|
|
314
|
-
columns: [
|
|
315
|
-
{ id: 'id', label: 'ID', capabilities: ['read'], role: 'row-key' },
|
|
316
|
-
],
|
|
317
|
-
query: { modes: ['rows'], projectableColumnIds: ['id'] },
|
|
318
|
-
actions: [
|
|
319
|
-
{ id: 'publish', label: 'Publish', selectionScopes: ['explicit-ids'] },
|
|
320
|
-
],
|
|
321
|
-
controls: [],
|
|
322
|
-
limits: { maxQueryRows: 10, maxQueryBytes: 10_000, maxSelectionSize: 10 },
|
|
323
|
-
};
|
|
324
|
-
registry.register({
|
|
325
|
-
descriptor,
|
|
326
|
-
getSnapshot: () => ({ revision: 1, state: {} }),
|
|
327
|
-
});
|
|
328
|
-
const previewed = [];
|
|
329
|
-
const apply = vi.fn();
|
|
330
|
-
const transport = suspendingTransport([
|
|
331
|
-
{
|
|
332
|
-
id: 'p1',
|
|
333
|
-
name: ASSISTANT_PROPOSE_ACTION_TOOL,
|
|
334
|
-
args: { surfaceId: 'articles', actionId: 'publish', rowIds: ['a1'] },
|
|
335
|
-
effect: 'read',
|
|
336
|
-
},
|
|
337
|
-
]);
|
|
338
|
-
const controller = createAssistantDockController({
|
|
339
|
-
transport,
|
|
340
|
-
registry,
|
|
341
|
-
actionClient: {
|
|
342
|
-
async preview(request) {
|
|
343
|
-
previewed.push(request);
|
|
344
|
-
return {
|
|
345
|
-
version: 1,
|
|
346
|
-
requestId: request.requestId,
|
|
347
|
-
identity: request.identity,
|
|
348
|
-
actionId: request.actionId,
|
|
349
|
-
phase: 'preview',
|
|
350
|
-
ok: true,
|
|
351
|
-
details: { rows: 1 },
|
|
352
|
-
};
|
|
353
|
-
},
|
|
354
|
-
apply,
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
await controller.loadThreads();
|
|
358
|
-
await controller.openThread('t1');
|
|
359
|
-
await controller.send('publish a1');
|
|
360
|
-
expect(transport.sent[0].clientTools?.map((t) => t.name)).toEqual([
|
|
361
|
-
ASSISTANT_PROPOSE_ACTION_TOOL,
|
|
362
|
-
]);
|
|
363
|
-
expect(previewed[0]).toMatchObject({
|
|
364
|
-
actionId: 'publish',
|
|
365
|
-
selection: { scope: 'explicit-ids', rowIds: ['a1'] },
|
|
366
|
-
});
|
|
367
|
-
expect(controller.actions.get('assistant-p1')?.status).toBe('previewed');
|
|
368
|
-
expect(apply).not.toHaveBeenCalled();
|
|
369
|
-
const result = transport.resumed[0].results[0];
|
|
370
|
-
expect(result.ok).toBe(true);
|
|
371
|
-
expect(JSON.parse(String(result.result))).toMatchObject({
|
|
372
|
-
proposed: true,
|
|
373
|
-
waitingForUser: true,
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
it('renders a waiting call with Allow / Don’t allow in the dock', async () => {
|
|
377
|
-
const transport = suspendingTransport([
|
|
378
|
-
{
|
|
379
|
-
id: 'c1',
|
|
380
|
-
name: DELETE_TOOL.name,
|
|
381
|
-
args: { id: 'a' },
|
|
382
|
-
effect: 'destructive',
|
|
383
|
-
},
|
|
384
|
-
]);
|
|
385
|
-
const tools = pageTools([DELETE_TOOL]);
|
|
386
|
-
let controller;
|
|
387
|
-
render(AssistantDock, {
|
|
388
|
-
props: {
|
|
389
|
-
transport,
|
|
390
|
-
registry: createDataSurfaceRegistry(),
|
|
391
|
-
pageTools: tools,
|
|
392
|
-
oncontroller: (c) => {
|
|
393
|
-
controller = c;
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
await until(() => Boolean(controller));
|
|
398
|
-
await controller?.openThread('t1');
|
|
399
|
-
const sending = controller?.send('delete it');
|
|
400
|
-
expect(await screen.findByText('Delete an article.')).toBeTruthy();
|
|
401
|
-
expect(screen.getByText("This can't be undone.")).toBeTruthy();
|
|
402
|
-
await userEvent.click(screen.getByRole('button', { name: 'Allow' }));
|
|
403
|
-
await sending;
|
|
404
|
-
expect(tools.executed).toHaveLength(1);
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
describe('readAssistantTurnResult (#2908)', () => {
|
|
408
|
-
it('maps a streamed suspension and persisted messages into a send result', async () => {
|
|
409
|
-
const { encodeAssistantTurnEvent } = await import('../../../../assistant-turn-events.js');
|
|
410
|
-
const { readAssistantTurnResult } = await import('../assistant-transport.js');
|
|
411
|
-
const frames = [
|
|
412
|
-
encodeAssistantTurnEvent({
|
|
413
|
-
type: 'status',
|
|
414
|
-
status: { state: 'working', label: 'Thinking…' },
|
|
415
|
-
}),
|
|
416
|
-
': heartbeat\n\n',
|
|
417
|
-
encodeAssistantTurnEvent({
|
|
418
|
-
type: 'message',
|
|
419
|
-
message: { id: 'm1', role: 'tool', content: 'x' },
|
|
420
|
-
}),
|
|
421
|
-
encodeAssistantTurnEvent({
|
|
422
|
-
type: 'client_tool_calls',
|
|
423
|
-
continuationId: 'k',
|
|
424
|
-
calls: [{ id: 'c', name: 'n', args: {}, effect: 'read' }],
|
|
425
|
-
}),
|
|
426
|
-
].join('');
|
|
427
|
-
const response = new Response(frames, {
|
|
428
|
-
headers: { 'content-type': 'text/event-stream' },
|
|
429
|
-
});
|
|
430
|
-
const seen = [];
|
|
431
|
-
const result = await readAssistantTurnResult(response, {
|
|
432
|
-
mapMessage: (wire) => message(wire.id, wire.role, wire.content),
|
|
433
|
-
onEvent: (event) => seen.push(event.type),
|
|
434
|
-
});
|
|
435
|
-
expect(seen).toEqual(['status', 'message', 'client_tool_calls']);
|
|
436
|
-
expect(result.messages?.map((m) => m.id)).toEqual(['m1']);
|
|
437
|
-
expect(result.clientToolCalls).toEqual({
|
|
438
|
-
continuationId: 'k',
|
|
439
|
-
calls: [{ id: 'c', name: 'n', args: {}, effect: 'read' }],
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
});
|