@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,448 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* #2991: a host can seed the composer draft (`initialDraft`,
|
|
4
|
-
* `controller.setDraft`) without sending it, and pass a composer
|
|
5
|
-
* `placeholder` through the dock. Also the `onActionSettled` /
|
|
6
|
-
* `onactionsettled` outcome callback for applied, rejected, and unknown
|
|
7
|
-
* action outcomes.
|
|
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 AssistantComposer from '../AssistantComposer.svelte';
|
|
14
|
-
import AssistantDock from '../AssistantDock.svelte';
|
|
15
|
-
import { createInMemoryAssistantTransport } from '../assistant-transport.js';
|
|
16
|
-
import { createAssistantDockController, } from '../create-assistant-dock-controller.svelte.js';
|
|
17
|
-
const identity = {
|
|
18
|
-
surfaceId: 'assets',
|
|
19
|
-
kind: 'table',
|
|
20
|
-
subject: { type: 'tenant', id: 'tenant-a' },
|
|
21
|
-
};
|
|
22
|
-
function registryWithSurface() {
|
|
23
|
-
const registry = createDataSurfaceRegistry();
|
|
24
|
-
const descriptor = {
|
|
25
|
-
version: 1,
|
|
26
|
-
identity,
|
|
27
|
-
schemaVersion: 1,
|
|
28
|
-
label: 'Assets',
|
|
29
|
-
rowKey: 'id',
|
|
30
|
-
columns: [
|
|
31
|
-
{ id: 'id', label: 'ID', capabilities: ['read'], role: 'row-key' },
|
|
32
|
-
],
|
|
33
|
-
query: {
|
|
34
|
-
modes: ['rows'],
|
|
35
|
-
projectableColumnIds: ['id'],
|
|
36
|
-
searchableColumnIds: [],
|
|
37
|
-
filterableColumnIds: [],
|
|
38
|
-
sortableColumnIds: [],
|
|
39
|
-
},
|
|
40
|
-
actions: [],
|
|
41
|
-
controls: [],
|
|
42
|
-
limits: { maxQueryRows: 10, maxQueryBytes: 10_000, maxSelectionSize: 10 },
|
|
43
|
-
};
|
|
44
|
-
registry.register({
|
|
45
|
-
descriptor,
|
|
46
|
-
getSnapshot: () => ({ revision: 1, state: {} }),
|
|
47
|
-
});
|
|
48
|
-
return registry;
|
|
49
|
-
}
|
|
50
|
-
function proposal(requestId = 'req-1') {
|
|
51
|
-
return {
|
|
52
|
-
version: 1,
|
|
53
|
-
requestId,
|
|
54
|
-
identity,
|
|
55
|
-
actionId: 'approve',
|
|
56
|
-
phase: 'preview',
|
|
57
|
-
selection: { scope: 'explicit-ids', rowIds: ['row-1'] },
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function result(request, phase, ok, extra = {}) {
|
|
61
|
-
return {
|
|
62
|
-
version: 1,
|
|
63
|
-
requestId: request.requestId,
|
|
64
|
-
identity: request.identity,
|
|
65
|
-
actionId: request.actionId,
|
|
66
|
-
phase,
|
|
67
|
-
ok,
|
|
68
|
-
...extra,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
function client(apply) {
|
|
72
|
-
return {
|
|
73
|
-
preview: async (request) => result(request, 'preview', true, { confirmationToken: 'tok' }),
|
|
74
|
-
apply,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
function textarea() {
|
|
78
|
-
return screen.getByLabelText('Message');
|
|
79
|
-
}
|
|
80
|
-
describe('AssistantComposer value (#2991)', () => {
|
|
81
|
-
it('renders a seeded value without sending it', () => {
|
|
82
|
-
const onsend = vi.fn();
|
|
83
|
-
render(AssistantComposer, {
|
|
84
|
-
props: { onsend, onupload: vi.fn(), value: 'Remove the background' },
|
|
85
|
-
});
|
|
86
|
-
expect(textarea()).toHaveValue('Remove the background');
|
|
87
|
-
expect(onsend).not.toHaveBeenCalled();
|
|
88
|
-
});
|
|
89
|
-
it('sends the edited seed only when the user sends', async () => {
|
|
90
|
-
const onsend = vi.fn().mockResolvedValue(undefined);
|
|
91
|
-
render(AssistantComposer, {
|
|
92
|
-
props: { onsend, onupload: vi.fn(), value: 'Remove the background' },
|
|
93
|
-
});
|
|
94
|
-
await userEvent.type(textarea(), ' please');
|
|
95
|
-
expect(onsend).not.toHaveBeenCalled();
|
|
96
|
-
await userEvent.click(screen.getByRole('button', { name: 'Send' }));
|
|
97
|
-
expect(onsend).toHaveBeenCalledWith('Remove the background please', []);
|
|
98
|
-
expect(textarea()).toHaveValue('');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
describe('AssistantComposer in-flight edits (#2991)', () => {
|
|
102
|
-
it('keeps text typed while the send was in flight', async () => {
|
|
103
|
-
let release;
|
|
104
|
-
const onsend = vi.fn(() => new Promise((resolve) => {
|
|
105
|
-
release = resolve;
|
|
106
|
-
}));
|
|
107
|
-
render(AssistantComposer, {
|
|
108
|
-
props: { onsend, onupload: vi.fn(), value: 'first' },
|
|
109
|
-
});
|
|
110
|
-
await userEvent.click(screen.getByRole('button', { name: 'Send' }));
|
|
111
|
-
await userEvent.type(textarea(), ' and more');
|
|
112
|
-
release();
|
|
113
|
-
await vi.waitFor(() => expect(onsend).toHaveBeenCalledTimes(1));
|
|
114
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
115
|
-
expect(textarea()).toHaveValue('first and more');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
describe('controller draft (#2991)', () => {
|
|
119
|
-
it('starts from initialDraft and setDraft replaces it without sending', async () => {
|
|
120
|
-
const transport = createInMemoryAssistantTransport();
|
|
121
|
-
const sendMessage = vi.spyOn(transport, 'sendMessage');
|
|
122
|
-
const controller = createAssistantDockController({
|
|
123
|
-
transport,
|
|
124
|
-
registry: registryWithSurface(),
|
|
125
|
-
initialDraft: 'first',
|
|
126
|
-
});
|
|
127
|
-
expect(controller.draft).toBe('first');
|
|
128
|
-
controller.setDraft('second');
|
|
129
|
-
expect(controller.draft).toBe('second');
|
|
130
|
-
expect(sendMessage).not.toHaveBeenCalled();
|
|
131
|
-
controller.dispose();
|
|
132
|
-
});
|
|
133
|
-
it('keeps the draft across a registry or transport swap', () => {
|
|
134
|
-
let registry = registryWithSurface();
|
|
135
|
-
let transport = createInMemoryAssistantTransport();
|
|
136
|
-
const controller = createAssistantDockController({
|
|
137
|
-
get transport() {
|
|
138
|
-
return transport;
|
|
139
|
-
},
|
|
140
|
-
get registry() {
|
|
141
|
-
return registry;
|
|
142
|
-
},
|
|
143
|
-
initialDraft: 'unsent text',
|
|
144
|
-
});
|
|
145
|
-
registry = registryWithSurface();
|
|
146
|
-
controller.syncRegistry();
|
|
147
|
-
expect(controller.draft).toBe('unsent text');
|
|
148
|
-
transport = createInMemoryAssistantTransport();
|
|
149
|
-
controller.syncTransport();
|
|
150
|
-
expect(controller.draft).toBe('unsent text');
|
|
151
|
-
controller.dispose();
|
|
152
|
-
});
|
|
153
|
-
it('clears the draft after a headless send of that draft', async () => {
|
|
154
|
-
const controller = createAssistantDockController({
|
|
155
|
-
transport: createInMemoryAssistantTransport(),
|
|
156
|
-
registry: registryWithSurface(),
|
|
157
|
-
});
|
|
158
|
-
const thread = await controller.createThread('t');
|
|
159
|
-
await controller.openThread(thread.id);
|
|
160
|
-
controller.setDraft('hello');
|
|
161
|
-
await controller.send('hello');
|
|
162
|
-
expect(controller.draft).toBe('');
|
|
163
|
-
controller.dispose();
|
|
164
|
-
});
|
|
165
|
-
it('keeps a draft changed while the send was in flight', async () => {
|
|
166
|
-
const transport = createInMemoryAssistantTransport();
|
|
167
|
-
const controller = createAssistantDockController({
|
|
168
|
-
transport,
|
|
169
|
-
registry: registryWithSurface(),
|
|
170
|
-
});
|
|
171
|
-
const thread = await controller.createThread('t');
|
|
172
|
-
await controller.openThread(thread.id);
|
|
173
|
-
controller.setDraft('hello');
|
|
174
|
-
const sending = controller.send('hello');
|
|
175
|
-
controller.setDraft('next message');
|
|
176
|
-
await sending;
|
|
177
|
-
expect(controller.draft).toBe('next message');
|
|
178
|
-
controller.dispose();
|
|
179
|
-
});
|
|
180
|
-
it('does not clear a kept draft when a send completes after a swap', async () => {
|
|
181
|
-
let transport = createInMemoryAssistantTransport();
|
|
182
|
-
const controller = createAssistantDockController({
|
|
183
|
-
get transport() {
|
|
184
|
-
return transport;
|
|
185
|
-
},
|
|
186
|
-
registry: registryWithSurface(),
|
|
187
|
-
});
|
|
188
|
-
const thread = await controller.createThread('t');
|
|
189
|
-
await controller.openThread(thread.id);
|
|
190
|
-
controller.setDraft('hello');
|
|
191
|
-
const sending = controller.send('hello');
|
|
192
|
-
transport = createInMemoryAssistantTransport();
|
|
193
|
-
controller.syncTransport();
|
|
194
|
-
await sending.catch(() => undefined);
|
|
195
|
-
expect(controller.draft).toBe('hello');
|
|
196
|
-
controller.dispose();
|
|
197
|
-
});
|
|
198
|
-
it('defaults to an empty draft', () => {
|
|
199
|
-
const controller = createAssistantDockController({
|
|
200
|
-
transport: createInMemoryAssistantTransport(),
|
|
201
|
-
registry: registryWithSurface(),
|
|
202
|
-
});
|
|
203
|
-
expect(controller.draft).toBe('');
|
|
204
|
-
controller.dispose();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
describe('AssistantDock draft seeding and placeholder (#2991)', () => {
|
|
208
|
-
it('renders initialDraft in the composer and never sends it', async () => {
|
|
209
|
-
const transport = createInMemoryAssistantTransport();
|
|
210
|
-
const sendMessage = vi.spyOn(transport, 'sendMessage');
|
|
211
|
-
render(AssistantDock, {
|
|
212
|
-
props: {
|
|
213
|
-
transport,
|
|
214
|
-
registry: registryWithSurface(),
|
|
215
|
-
initialDraft: 'Remove the background from this photo',
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
expect(textarea()).toHaveValue('Remove the background from this photo');
|
|
219
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
220
|
-
expect(sendMessage).not.toHaveBeenCalled();
|
|
221
|
-
});
|
|
222
|
-
it('lets the host seed the draft through controller.setDraft', async () => {
|
|
223
|
-
let hostController;
|
|
224
|
-
const transport = createInMemoryAssistantTransport();
|
|
225
|
-
const sendMessage = vi.spyOn(transport, 'sendMessage');
|
|
226
|
-
render(AssistantDock, {
|
|
227
|
-
props: {
|
|
228
|
-
transport,
|
|
229
|
-
registry: registryWithSurface(),
|
|
230
|
-
oncontroller: (c) => {
|
|
231
|
-
hostController = c;
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
expect(textarea()).toHaveValue('');
|
|
236
|
-
hostController?.setDraft('Describe the edit');
|
|
237
|
-
flushSync();
|
|
238
|
-
expect(textarea()).toHaveValue('Describe the edit');
|
|
239
|
-
expect(sendMessage).not.toHaveBeenCalled();
|
|
240
|
-
});
|
|
241
|
-
it('reflects typed text back into controller.draft', async () => {
|
|
242
|
-
let hostController;
|
|
243
|
-
render(AssistantDock, {
|
|
244
|
-
props: {
|
|
245
|
-
transport: createInMemoryAssistantTransport(),
|
|
246
|
-
registry: registryWithSurface(),
|
|
247
|
-
initialDraft: 'seed',
|
|
248
|
-
oncontroller: (c) => {
|
|
249
|
-
hostController = c;
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
// The composer is disabled until a thread is open; open one first.
|
|
254
|
-
const thread = await hostController?.createThread('t');
|
|
255
|
-
if (thread)
|
|
256
|
-
await hostController?.openThread(thread.id);
|
|
257
|
-
flushSync();
|
|
258
|
-
await userEvent.type(textarea(), ' more');
|
|
259
|
-
expect(hostController?.draft).toBe('seed more');
|
|
260
|
-
});
|
|
261
|
-
it('forwards composerPlaceholder to the composer', () => {
|
|
262
|
-
render(AssistantDock, {
|
|
263
|
-
props: {
|
|
264
|
-
transport: createInMemoryAssistantTransport(),
|
|
265
|
-
registry: registryWithSurface(),
|
|
266
|
-
composerPlaceholder: 'Describe the image edit to generate…',
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
expect(textarea()).toHaveAttribute('placeholder', 'Describe the image edit to generate…');
|
|
270
|
-
});
|
|
271
|
-
it('keeps the composer default placeholder when none is passed', () => {
|
|
272
|
-
render(AssistantDock, {
|
|
273
|
-
props: {
|
|
274
|
-
transport: createInMemoryAssistantTransport(),
|
|
275
|
-
registry: registryWithSurface(),
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
expect(textarea()).toHaveAttribute('placeholder', 'Ask the assistant…');
|
|
279
|
-
});
|
|
280
|
-
});
|
|
281
|
-
describe('controller onActionSettled', () => {
|
|
282
|
-
function settledController(apply, onActionSettled) {
|
|
283
|
-
return createAssistantDockController({
|
|
284
|
-
transport: createInMemoryAssistantTransport(),
|
|
285
|
-
registry: registryWithSurface(),
|
|
286
|
-
actionClient: client(apply),
|
|
287
|
-
onActionSettled,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
it('reports an accepted apply as applied with the server result', async () => {
|
|
291
|
-
const onActionSettled = vi.fn();
|
|
292
|
-
const controller = settledController(async (request) => result(request, 'apply', true, { details: { assetId: 'srv-1' } }), onActionSettled);
|
|
293
|
-
await controller.previewAction(proposal());
|
|
294
|
-
await controller.applyAction('req-1');
|
|
295
|
-
expect(onActionSettled).toHaveBeenCalledTimes(1);
|
|
296
|
-
const [request, outcome] = onActionSettled.mock.calls[0];
|
|
297
|
-
expect(request.phase).toBe('apply');
|
|
298
|
-
expect(outcome.status).toBe('applied');
|
|
299
|
-
expect(outcome.result.details).toEqual({ assetId: 'srv-1' });
|
|
300
|
-
controller.dispose();
|
|
301
|
-
});
|
|
302
|
-
it('reports a server refusal as rejected by the server', async () => {
|
|
303
|
-
const onActionSettled = vi.fn();
|
|
304
|
-
const controller = settledController(async (request) => result(request, 'apply', false, { reason: 'stale_revision' }), onActionSettled);
|
|
305
|
-
await controller.previewAction(proposal());
|
|
306
|
-
await controller.applyAction('req-1');
|
|
307
|
-
expect(onActionSettled).toHaveBeenCalledTimes(1);
|
|
308
|
-
const outcome = onActionSettled.mock.calls[0][1];
|
|
309
|
-
expect(outcome).toMatchObject({ status: 'rejected', by: 'server' });
|
|
310
|
-
expect(outcome.result.reason).toBe('stale_revision');
|
|
311
|
-
controller.dispose();
|
|
312
|
-
});
|
|
313
|
-
it('reports a user reject as rejected by the user', async () => {
|
|
314
|
-
const onActionSettled = vi.fn();
|
|
315
|
-
const controller = settledController(async (request) => result(request, 'apply', true), onActionSettled);
|
|
316
|
-
await controller.previewAction(proposal());
|
|
317
|
-
controller.rejectAction('req-1');
|
|
318
|
-
expect(controller.actions.has('req-1')).toBe(false);
|
|
319
|
-
expect(onActionSettled).toHaveBeenCalledTimes(1);
|
|
320
|
-
const [request, outcome] = onActionSettled.mock.calls[0];
|
|
321
|
-
expect(request.requestId).toBe('req-1');
|
|
322
|
-
expect(outcome).toEqual({ status: 'rejected', by: 'user' });
|
|
323
|
-
controller.dispose();
|
|
324
|
-
});
|
|
325
|
-
it('reports a thrown apply as unknown, then the later decision', async () => {
|
|
326
|
-
const onActionSettled = vi.fn();
|
|
327
|
-
let mode = 'throw';
|
|
328
|
-
const controller = settledController(async (request) => {
|
|
329
|
-
if (mode === 'throw')
|
|
330
|
-
throw new Error('network down');
|
|
331
|
-
return result(request, 'apply', true);
|
|
332
|
-
}, onActionSettled);
|
|
333
|
-
await controller.previewAction(proposal());
|
|
334
|
-
await controller.applyAction('req-1');
|
|
335
|
-
expect(onActionSettled).toHaveBeenCalledTimes(1);
|
|
336
|
-
expect(onActionSettled.mock.calls[0][1]).toEqual({
|
|
337
|
-
status: 'unknown',
|
|
338
|
-
error: 'network down',
|
|
339
|
-
});
|
|
340
|
-
mode = 'ok';
|
|
341
|
-
await controller.applyAction('req-1');
|
|
342
|
-
expect(onActionSettled).toHaveBeenCalledTimes(2);
|
|
343
|
-
expect(onActionSettled.mock.calls[1][1].status).toBe('applied');
|
|
344
|
-
controller.dispose();
|
|
345
|
-
});
|
|
346
|
-
it('reports an unknown-outcome reason as unknown with the result', async () => {
|
|
347
|
-
const onActionSettled = vi.fn();
|
|
348
|
-
const controller = settledController(async (request) => result(request, 'apply', false, { reason: 'idempotency_in_progress' }), onActionSettled);
|
|
349
|
-
await controller.previewAction(proposal());
|
|
350
|
-
await controller.applyAction('req-1');
|
|
351
|
-
const outcome = onActionSettled.mock.calls[0][1];
|
|
352
|
-
expect(outcome.status).toBe('unknown');
|
|
353
|
-
expect(outcome.result.reason).toBe('idempotency_in_progress');
|
|
354
|
-
controller.dispose();
|
|
355
|
-
});
|
|
356
|
-
it('does not report a refused reject while the outcome is unknown', async () => {
|
|
357
|
-
const onActionSettled = vi.fn();
|
|
358
|
-
const controller = settledController(async () => {
|
|
359
|
-
throw new Error('timeout');
|
|
360
|
-
}, onActionSettled);
|
|
361
|
-
await controller.previewAction(proposal());
|
|
362
|
-
await controller.applyAction('req-1');
|
|
363
|
-
onActionSettled.mockClear();
|
|
364
|
-
controller.rejectAction('req-1');
|
|
365
|
-
expect(controller.actions.has('req-1')).toBe(true);
|
|
366
|
-
expect(onActionSettled).not.toHaveBeenCalled();
|
|
367
|
-
controller.dispose();
|
|
368
|
-
});
|
|
369
|
-
it('does not report an apply that lands after a context swap', async () => {
|
|
370
|
-
const onActionSettled = vi.fn();
|
|
371
|
-
let release;
|
|
372
|
-
let registry = registryWithSurface();
|
|
373
|
-
const controller = createAssistantDockController({
|
|
374
|
-
transport: createInMemoryAssistantTransport(),
|
|
375
|
-
get registry() {
|
|
376
|
-
return registry;
|
|
377
|
-
},
|
|
378
|
-
actionClient: client(async (request) => {
|
|
379
|
-
await new Promise((resolve) => {
|
|
380
|
-
release = resolve;
|
|
381
|
-
});
|
|
382
|
-
return result(request, 'apply', true);
|
|
383
|
-
}),
|
|
384
|
-
onActionSettled,
|
|
385
|
-
});
|
|
386
|
-
await controller.previewAction(proposal());
|
|
387
|
-
const applying = controller.applyAction('req-1');
|
|
388
|
-
registry = registryWithSurface();
|
|
389
|
-
controller.syncRegistry();
|
|
390
|
-
release();
|
|
391
|
-
await applying;
|
|
392
|
-
expect(onActionSettled).not.toHaveBeenCalled();
|
|
393
|
-
controller.dispose();
|
|
394
|
-
});
|
|
395
|
-
it('keeps action state when the host callback throws', async () => {
|
|
396
|
-
const onActionSettled = vi.fn(() => {
|
|
397
|
-
throw new Error('host bug');
|
|
398
|
-
});
|
|
399
|
-
const controller = settledController(async (request) => result(request, 'apply', true), onActionSettled);
|
|
400
|
-
await controller.previewAction(proposal());
|
|
401
|
-
await expect(controller.applyAction('req-1')).resolves.toBeUndefined();
|
|
402
|
-
expect(controller.actions.get('req-1')?.status).toBe('applied');
|
|
403
|
-
await controller.previewAction(proposal('req-2'));
|
|
404
|
-
expect(() => controller.rejectAction('req-2')).not.toThrow();
|
|
405
|
-
expect(controller.actions.has('req-2')).toBe(false);
|
|
406
|
-
controller.dispose();
|
|
407
|
-
});
|
|
408
|
-
it('still calls onActionApplied alongside onActionSettled', async () => {
|
|
409
|
-
const onActionApplied = vi.fn();
|
|
410
|
-
const onActionSettled = vi.fn();
|
|
411
|
-
const controller = createAssistantDockController({
|
|
412
|
-
transport: createInMemoryAssistantTransport(),
|
|
413
|
-
registry: registryWithSurface(),
|
|
414
|
-
actionClient: client(async (request) => result(request, 'apply', true)),
|
|
415
|
-
onActionApplied,
|
|
416
|
-
onActionSettled,
|
|
417
|
-
});
|
|
418
|
-
await controller.previewAction(proposal());
|
|
419
|
-
await controller.applyAction('req-1');
|
|
420
|
-
expect(onActionApplied).toHaveBeenCalledTimes(1);
|
|
421
|
-
expect(onActionSettled).toHaveBeenCalledTimes(1);
|
|
422
|
-
controller.dispose();
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
describe('AssistantDock onactionsettled', () => {
|
|
426
|
-
it('reports a Reject click to the host', async () => {
|
|
427
|
-
let hostController;
|
|
428
|
-
const onactionsettled = vi.fn();
|
|
429
|
-
render(AssistantDock, {
|
|
430
|
-
props: {
|
|
431
|
-
transport: createInMemoryAssistantTransport(),
|
|
432
|
-
registry: registryWithSurface(),
|
|
433
|
-
actionClient: client(async (request) => result(request, 'apply', true)),
|
|
434
|
-
oncontroller: (c) => {
|
|
435
|
-
hostController = c;
|
|
436
|
-
},
|
|
437
|
-
onactionsettled,
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
await hostController?.previewAction(proposal());
|
|
441
|
-
await userEvent.click(await screen.findByRole('button', { name: 'Reject' }));
|
|
442
|
-
await vi.waitFor(() => expect(onactionsettled).toHaveBeenCalledTimes(1));
|
|
443
|
-
expect(onactionsettled.mock.calls[0][1]).toEqual({
|
|
444
|
-
status: 'rejected',
|
|
445
|
-
by: 'user',
|
|
446
|
-
});
|
|
447
|
-
});
|
|
448
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
/**
|
|
3
|
-
* #2989: a host can propose an action into the dock and observe an applied
|
|
4
|
-
* outcome — controller `onActionApplied`, and AssistantDock's
|
|
5
|
-
* `oncontroller` / `onactionapplied` props.
|
|
6
|
-
*/
|
|
7
|
-
import { createDataSurfaceRegistry, } from '@happyvertical/smrt-ui/data-surface';
|
|
8
|
-
import { render, screen, userEvent } from '@happyvertical/smrt-vitest/svelte';
|
|
9
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import AssistantDock from '../AssistantDock.svelte';
|
|
11
|
-
import { createInMemoryAssistantTransport } from '../assistant-transport.js';
|
|
12
|
-
import { createAssistantDockController, } from '../create-assistant-dock-controller.svelte.js';
|
|
13
|
-
const identity = {
|
|
14
|
-
surfaceId: 'assets',
|
|
15
|
-
kind: 'table',
|
|
16
|
-
subject: { type: 'tenant', id: 'tenant-a' },
|
|
17
|
-
};
|
|
18
|
-
function registryWithSurface() {
|
|
19
|
-
const registry = createDataSurfaceRegistry();
|
|
20
|
-
const descriptor = {
|
|
21
|
-
version: 1,
|
|
22
|
-
identity,
|
|
23
|
-
schemaVersion: 1,
|
|
24
|
-
label: 'Assets',
|
|
25
|
-
rowKey: 'id',
|
|
26
|
-
columns: [
|
|
27
|
-
{ id: 'id', label: 'ID', capabilities: ['read'], role: 'row-key' },
|
|
28
|
-
],
|
|
29
|
-
query: {
|
|
30
|
-
modes: ['rows'],
|
|
31
|
-
projectableColumnIds: ['id'],
|
|
32
|
-
searchableColumnIds: [],
|
|
33
|
-
filterableColumnIds: [],
|
|
34
|
-
sortableColumnIds: [],
|
|
35
|
-
},
|
|
36
|
-
actions: [],
|
|
37
|
-
controls: [],
|
|
38
|
-
limits: { maxQueryRows: 10, maxQueryBytes: 10_000, maxSelectionSize: 10 },
|
|
39
|
-
};
|
|
40
|
-
registry.register({
|
|
41
|
-
descriptor,
|
|
42
|
-
getSnapshot: () => ({ revision: 1, state: {} }),
|
|
43
|
-
});
|
|
44
|
-
return registry;
|
|
45
|
-
}
|
|
46
|
-
function proposal(requestId = 'req-approve') {
|
|
47
|
-
return {
|
|
48
|
-
version: 1,
|
|
49
|
-
requestId,
|
|
50
|
-
identity,
|
|
51
|
-
actionId: 'approve',
|
|
52
|
-
phase: 'preview',
|
|
53
|
-
selection: { scope: 'explicit-ids', rowIds: ['row-sent'] },
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function result(request, phase, ok, extra = {}) {
|
|
57
|
-
return {
|
|
58
|
-
version: 1,
|
|
59
|
-
requestId: request.requestId,
|
|
60
|
-
identity: request.identity,
|
|
61
|
-
actionId: request.actionId,
|
|
62
|
-
phase,
|
|
63
|
-
ok,
|
|
64
|
-
...extra,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function client(apply) {
|
|
68
|
-
return {
|
|
69
|
-
preview: async (request) => result(request, 'preview', true, { confirmationToken: 'tok' }),
|
|
70
|
-
apply,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
describe('controller onActionApplied (#2989)', () => {
|
|
74
|
-
it('fires once with the server result for an accepted apply', async () => {
|
|
75
|
-
const onActionApplied = vi.fn();
|
|
76
|
-
const controller = createAssistantDockController({
|
|
77
|
-
transport: createInMemoryAssistantTransport(),
|
|
78
|
-
registry: registryWithSurface(),
|
|
79
|
-
actionClient: client(async (request) => result(request, 'apply', true, {
|
|
80
|
-
details: { approvedAssetId: 'asset-from-server' },
|
|
81
|
-
})),
|
|
82
|
-
onActionApplied,
|
|
83
|
-
});
|
|
84
|
-
await controller.previewAction(proposal());
|
|
85
|
-
await controller.applyAction('req-approve');
|
|
86
|
-
expect(controller.actions.get('req-approve')?.status).toBe('applied');
|
|
87
|
-
expect(onActionApplied).toHaveBeenCalledTimes(1);
|
|
88
|
-
const [request, applied] = onActionApplied.mock.calls[0];
|
|
89
|
-
expect(request.phase).toBe('apply');
|
|
90
|
-
expect(request.requestId).toBe('req-approve');
|
|
91
|
-
expect(applied.details).toEqual({ approvedAssetId: 'asset-from-server' });
|
|
92
|
-
controller.dispose();
|
|
93
|
-
});
|
|
94
|
-
it('does not fire for a refusal or a thrown apply', async () => {
|
|
95
|
-
const onActionApplied = vi.fn();
|
|
96
|
-
let mode = 'refuse';
|
|
97
|
-
const controller = createAssistantDockController({
|
|
98
|
-
transport: createInMemoryAssistantTransport(),
|
|
99
|
-
registry: registryWithSurface(),
|
|
100
|
-
actionClient: client(async (request) => {
|
|
101
|
-
if (mode === 'throw')
|
|
102
|
-
throw new Error('network down');
|
|
103
|
-
return result(request, 'apply', false, { reason: 'denied' });
|
|
104
|
-
}),
|
|
105
|
-
onActionApplied,
|
|
106
|
-
});
|
|
107
|
-
await controller.previewAction(proposal('req-a'));
|
|
108
|
-
await controller.applyAction('req-a');
|
|
109
|
-
mode = 'throw';
|
|
110
|
-
await controller.previewAction(proposal('req-b'));
|
|
111
|
-
await controller.applyAction('req-b');
|
|
112
|
-
expect(onActionApplied).not.toHaveBeenCalled();
|
|
113
|
-
controller.dispose();
|
|
114
|
-
});
|
|
115
|
-
it('keeps the applied state when the host callback throws', async () => {
|
|
116
|
-
const onActionApplied = vi.fn(() => {
|
|
117
|
-
throw new Error('host bug');
|
|
118
|
-
});
|
|
119
|
-
const controller = createAssistantDockController({
|
|
120
|
-
transport: createInMemoryAssistantTransport(),
|
|
121
|
-
registry: registryWithSurface(),
|
|
122
|
-
actionClient: client(async (request) => result(request, 'apply', true)),
|
|
123
|
-
onActionApplied,
|
|
124
|
-
});
|
|
125
|
-
await controller.previewAction(proposal());
|
|
126
|
-
await expect(controller.applyAction('req-approve')).resolves.toBeUndefined();
|
|
127
|
-
expect(onActionApplied).toHaveBeenCalledTimes(1);
|
|
128
|
-
expect(controller.actions.get('req-approve')?.status).toBe('applied');
|
|
129
|
-
controller.dispose();
|
|
130
|
-
});
|
|
131
|
-
it('does not fire for an apply that lands after a context swap', async () => {
|
|
132
|
-
const onActionApplied = vi.fn();
|
|
133
|
-
let release;
|
|
134
|
-
let registry = registryWithSurface();
|
|
135
|
-
const controller = createAssistantDockController({
|
|
136
|
-
transport: createInMemoryAssistantTransport(),
|
|
137
|
-
get registry() {
|
|
138
|
-
return registry;
|
|
139
|
-
},
|
|
140
|
-
actionClient: client(async (request) => {
|
|
141
|
-
await new Promise((resolve) => {
|
|
142
|
-
release = resolve;
|
|
143
|
-
});
|
|
144
|
-
return result(request, 'apply', true);
|
|
145
|
-
}),
|
|
146
|
-
onActionApplied,
|
|
147
|
-
});
|
|
148
|
-
await controller.previewAction(proposal());
|
|
149
|
-
const applying = controller.applyAction('req-approve');
|
|
150
|
-
registry = registryWithSurface();
|
|
151
|
-
controller.syncRegistry();
|
|
152
|
-
release();
|
|
153
|
-
await applying;
|
|
154
|
-
expect(onActionApplied).not.toHaveBeenCalled();
|
|
155
|
-
controller.dispose();
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('AssistantDock oncontroller / onactionapplied (#2989)', () => {
|
|
159
|
-
it('lets a host propose an action and observe the applied outcome', async () => {
|
|
160
|
-
let hostController;
|
|
161
|
-
const onactionapplied = vi.fn();
|
|
162
|
-
const oncontroller = vi.fn((c) => {
|
|
163
|
-
hostController = c;
|
|
164
|
-
});
|
|
165
|
-
render(AssistantDock, {
|
|
166
|
-
props: {
|
|
167
|
-
transport: createInMemoryAssistantTransport(),
|
|
168
|
-
registry: registryWithSurface(),
|
|
169
|
-
actionClient: client(async (request) => result(request, 'apply', true, {
|
|
170
|
-
details: { approvedAssetId: 'asset-from-server' },
|
|
171
|
-
})),
|
|
172
|
-
oncontroller,
|
|
173
|
-
onactionapplied,
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
expect(oncontroller).toHaveBeenCalledTimes(1);
|
|
177
|
-
expect(hostController).toBeDefined();
|
|
178
|
-
await hostController?.previewAction(proposal());
|
|
179
|
-
await userEvent.click(await screen.findByRole('button', { name: 'Confirm' }));
|
|
180
|
-
await vi.waitFor(() => expect(onactionapplied).toHaveBeenCalledTimes(1));
|
|
181
|
-
expect(onactionapplied.mock.calls[0][1].details).toEqual({
|
|
182
|
-
approvedAssetId: 'asset-from-server',
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
});
|