@assistant-ui/eve 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/dist/convertEveMessages.d.ts +32 -3
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +75 -4
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +63 -16
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +14 -10
- package/src/convertEveMessages.test.ts +1041 -5
- package/src/convertEveMessages.ts +155 -6
- package/src/index.ts +5 -1
- package/src/useEveAgentRuntime.test.tsx +622 -0
- package/src/useEveAgentRuntime.ts +90 -24
- package/LICENSE +0 -21
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const { mockUseEveAgent } = vi.hoisted(() => ({
|
|
7
|
+
mockUseEveAgent: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("eve/react", async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import("eve/react")>()),
|
|
12
|
+
useEveAgent: mockUseEveAgent,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import type { EveMessageData } from "eve/react";
|
|
16
|
+
import { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
17
|
+
|
|
18
|
+
const stuckStreamingData: EveMessageData = {
|
|
19
|
+
messages: [
|
|
20
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
21
|
+
{
|
|
22
|
+
id: "a1",
|
|
23
|
+
role: "assistant",
|
|
24
|
+
metadata: { status: "streaming" },
|
|
25
|
+
parts: [{ type: "text", text: "Let me th" }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const createAgent = (overrides: Record<string, unknown>) => ({
|
|
31
|
+
data: stuckStreamingData,
|
|
32
|
+
error: undefined,
|
|
33
|
+
events: [],
|
|
34
|
+
session: undefined,
|
|
35
|
+
status: "ready",
|
|
36
|
+
send: vi.fn(),
|
|
37
|
+
stop: vi.fn(),
|
|
38
|
+
reset: vi.fn(),
|
|
39
|
+
...overrides,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("useEveAgentRuntime status forwarding", () => {
|
|
43
|
+
it("maps the session error onto the interrupted assistant message", () => {
|
|
44
|
+
mockUseEveAgent.mockReturnValue(
|
|
45
|
+
createAgent({ status: "error", error: new Error("boom") }) as never,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
49
|
+
|
|
50
|
+
expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
|
|
51
|
+
type: "incomplete",
|
|
52
|
+
reason: "error",
|
|
53
|
+
error: { code: "unknown", message: "boom" },
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("settles an aborted turn to cancelled once the agent is idle", () => {
|
|
58
|
+
mockUseEveAgent.mockReturnValue(createAgent({ status: "ready" }) as never);
|
|
59
|
+
|
|
60
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
61
|
+
|
|
62
|
+
expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
|
|
63
|
+
type: "incomplete",
|
|
64
|
+
reason: "cancelled",
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("recomputes statuses when only the session error changes", () => {
|
|
69
|
+
const idle = createAgent({ status: "ready" });
|
|
70
|
+
mockUseEveAgent.mockReturnValue(idle as never);
|
|
71
|
+
|
|
72
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
73
|
+
|
|
74
|
+
expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
|
|
75
|
+
type: "incomplete",
|
|
76
|
+
reason: "cancelled",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
mockUseEveAgent.mockReturnValue({
|
|
80
|
+
...idle,
|
|
81
|
+
status: "error",
|
|
82
|
+
error: new Error("boom"),
|
|
83
|
+
} as never);
|
|
84
|
+
rerender();
|
|
85
|
+
|
|
86
|
+
expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
|
|
87
|
+
type: "incomplete",
|
|
88
|
+
reason: "error",
|
|
89
|
+
error: { code: "unknown", message: "boom" },
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("forwards runConfig to eve as one-turn client context when sending", async () => {
|
|
94
|
+
const agent = createAgent({
|
|
95
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
96
|
+
});
|
|
97
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
98
|
+
|
|
99
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
result.current.thread.append({
|
|
103
|
+
role: "user",
|
|
104
|
+
content: [{ type: "text", text: "hello" }],
|
|
105
|
+
runConfig: { custom: { page: "/pricing" } },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
111
|
+
message: "hello",
|
|
112
|
+
clientContext: { page: "/pricing" },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("omits clientContext when no runConfig is provided", async () => {
|
|
118
|
+
const agent = createAgent({
|
|
119
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
120
|
+
});
|
|
121
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
122
|
+
|
|
123
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
124
|
+
|
|
125
|
+
act(() => {
|
|
126
|
+
result.current.thread.append({
|
|
127
|
+
role: "user",
|
|
128
|
+
content: [{ type: "text", text: "hello" }],
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("omits clientContext when runConfig.custom is empty", async () => {
|
|
138
|
+
const agent = createAgent({
|
|
139
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
140
|
+
});
|
|
141
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
142
|
+
|
|
143
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
144
|
+
|
|
145
|
+
act(() => {
|
|
146
|
+
result.current.thread.append({
|
|
147
|
+
role: "user",
|
|
148
|
+
content: [{ type: "text", text: "hello" }],
|
|
149
|
+
runConfig: { custom: {} },
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("prefers the reload-time runConfig over the staged one", async () => {
|
|
159
|
+
const agent = createAgent({
|
|
160
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
161
|
+
});
|
|
162
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
163
|
+
|
|
164
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
165
|
+
|
|
166
|
+
act(() => {
|
|
167
|
+
result.current.thread.append({
|
|
168
|
+
role: "user",
|
|
169
|
+
content: [{ type: "text", text: "hello" }],
|
|
170
|
+
startRun: false,
|
|
171
|
+
runConfig: { custom: { page: "/staged" } },
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
const stagedId = result.current.thread.getState().messages[0]!.id;
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
result.current.thread.startRun({
|
|
182
|
+
parentId: stagedId,
|
|
183
|
+
runConfig: { custom: { page: "/reload" } },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
189
|
+
message: "hello",
|
|
190
|
+
clientContext: { page: "/reload" },
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("falls back to the staged runConfig when reload passes none", async () => {
|
|
196
|
+
const agent = createAgent({
|
|
197
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
198
|
+
});
|
|
199
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
200
|
+
|
|
201
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
202
|
+
|
|
203
|
+
act(() => {
|
|
204
|
+
result.current.thread.append({
|
|
205
|
+
role: "user",
|
|
206
|
+
content: [{ type: "text", text: "hello" }],
|
|
207
|
+
startRun: false,
|
|
208
|
+
runConfig: { custom: { page: "/staged" } },
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
214
|
+
});
|
|
215
|
+
const stagedId = result.current.thread.getState().messages[0]!.id;
|
|
216
|
+
|
|
217
|
+
act(() => {
|
|
218
|
+
result.current.thread.startRun({ parentId: stagedId });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
223
|
+
message: "hello",
|
|
224
|
+
clientContext: { page: "/staged" },
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Known limitation: core normalizes an omitted reload runConfig to {}
|
|
230
|
+
// (toStartRunConfig in core/src/runtime/api/thread-runtime.ts), so an
|
|
231
|
+
// explicit empty reload config is indistinguishable from an omitted one at
|
|
232
|
+
// the adapter and cannot clear the staged context.
|
|
233
|
+
it("cannot clear the staged runConfig with an explicit empty reload config", async () => {
|
|
234
|
+
const agent = createAgent({
|
|
235
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
236
|
+
});
|
|
237
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
238
|
+
|
|
239
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
result.current.thread.append({
|
|
243
|
+
role: "user",
|
|
244
|
+
content: [{ type: "text", text: "hello" }],
|
|
245
|
+
startRun: false,
|
|
246
|
+
runConfig: { custom: { page: "/staged" } },
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
const stagedId = result.current.thread.getState().messages[0]!.id;
|
|
254
|
+
|
|
255
|
+
act(() => {
|
|
256
|
+
result.current.thread.startRun({ parentId: stagedId, runConfig: {} });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
261
|
+
message: "hello",
|
|
262
|
+
clientContext: { page: "/staged" },
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("keeps the last assistant message running while streaming", () => {
|
|
268
|
+
mockUseEveAgent.mockReturnValue(
|
|
269
|
+
createAgent({ status: "streaming" }) as never,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
273
|
+
|
|
274
|
+
expect(result.current.thread.getState().messages.at(-1)?.status).toEqual({
|
|
275
|
+
type: "running",
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const settledData: EveMessageData = {
|
|
281
|
+
messages: [
|
|
282
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
|
|
283
|
+
{
|
|
284
|
+
id: "a1",
|
|
285
|
+
role: "assistant",
|
|
286
|
+
parts: [{ type: "text", text: "earlier answer" }],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const getText = (runtime: ReturnType<typeof useEveAgentRuntime>) =>
|
|
292
|
+
runtime.thread
|
|
293
|
+
.getState()
|
|
294
|
+
.messages.map((message) =>
|
|
295
|
+
message.content
|
|
296
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
297
|
+
.join(""),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const stageMessage = async (
|
|
301
|
+
runtime: ReturnType<typeof useEveAgentRuntime>,
|
|
302
|
+
text: string,
|
|
303
|
+
) => {
|
|
304
|
+
await act(async () => {
|
|
305
|
+
runtime.thread.append({
|
|
306
|
+
role: "user",
|
|
307
|
+
content: [{ type: "text", text }],
|
|
308
|
+
startRun: false,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
describe("useEveAgentRuntime staged messages", () => {
|
|
314
|
+
it("stages a user message without submitting when startRun is false", async () => {
|
|
315
|
+
const agent = createAgent({ data: settledData });
|
|
316
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
317
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
318
|
+
|
|
319
|
+
await stageMessage(result.current, "staged draft");
|
|
320
|
+
|
|
321
|
+
await waitFor(() => {
|
|
322
|
+
expect(getText(result.current)).toEqual([
|
|
323
|
+
"earlier",
|
|
324
|
+
"earlier answer",
|
|
325
|
+
"staged draft",
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
expect(agent.send).not.toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("renders new turns and refloats staged drafts after a normal send", async () => {
|
|
332
|
+
const agent = createAgent({ data: settledData });
|
|
333
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
334
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
335
|
+
|
|
336
|
+
await stageMessage(result.current, "staged draft");
|
|
337
|
+
|
|
338
|
+
await act(async () => {
|
|
339
|
+
result.current.thread.append({
|
|
340
|
+
role: "user",
|
|
341
|
+
content: [{ type: "text", text: "hello" }],
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
345
|
+
message: "hello",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
mockUseEveAgent.mockReturnValue(
|
|
349
|
+
createAgent({
|
|
350
|
+
data: {
|
|
351
|
+
messages: [
|
|
352
|
+
...settledData.messages,
|
|
353
|
+
{
|
|
354
|
+
id: "u2",
|
|
355
|
+
role: "user",
|
|
356
|
+
parts: [{ type: "text", text: "hello" }],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "a2",
|
|
360
|
+
role: "assistant",
|
|
361
|
+
parts: [{ type: "text", text: "hi there" }],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
send: agent.send,
|
|
366
|
+
}) as never,
|
|
367
|
+
);
|
|
368
|
+
rerender();
|
|
369
|
+
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(getText(result.current)).toEqual([
|
|
372
|
+
"earlier",
|
|
373
|
+
"earlier answer",
|
|
374
|
+
"hello",
|
|
375
|
+
"hi there",
|
|
376
|
+
"staged draft",
|
|
377
|
+
]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("keeps both drafts when two appends stage in the same batch", async () => {
|
|
382
|
+
const agent = createAgent({ data: settledData });
|
|
383
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
384
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
385
|
+
|
|
386
|
+
await act(async () => {
|
|
387
|
+
result.current.thread.append({
|
|
388
|
+
role: "user",
|
|
389
|
+
content: [{ type: "text", text: "first staged" }],
|
|
390
|
+
startRun: false,
|
|
391
|
+
});
|
|
392
|
+
result.current.thread.append({
|
|
393
|
+
role: "user",
|
|
394
|
+
content: [{ type: "text", text: "second staged" }],
|
|
395
|
+
startRun: false,
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
await waitFor(() => {
|
|
400
|
+
expect(getText(result.current)).toEqual([
|
|
401
|
+
"earlier",
|
|
402
|
+
"earlier answer",
|
|
403
|
+
"first staged",
|
|
404
|
+
"second staged",
|
|
405
|
+
]);
|
|
406
|
+
});
|
|
407
|
+
expect(agent.send).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("restores staged drafts when a promoted send rejects and promotes them all on retry", async () => {
|
|
411
|
+
const send = vi
|
|
412
|
+
.fn()
|
|
413
|
+
.mockRejectedValueOnce(
|
|
414
|
+
new Error("eve session is already processing a turn."),
|
|
415
|
+
)
|
|
416
|
+
.mockResolvedValue(undefined);
|
|
417
|
+
const agent = createAgent({ data: settledData, send });
|
|
418
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
419
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
420
|
+
|
|
421
|
+
await stageMessage(result.current, "first staged");
|
|
422
|
+
await stageMessage(result.current, "second staged");
|
|
423
|
+
|
|
424
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
425
|
+
await expect(
|
|
426
|
+
Promise.resolve(
|
|
427
|
+
result.current.thread.startRun({
|
|
428
|
+
parentId: secondStagedId,
|
|
429
|
+
sourceId: null,
|
|
430
|
+
runConfig: {},
|
|
431
|
+
}),
|
|
432
|
+
),
|
|
433
|
+
).rejects.toThrow("eve session is already processing a turn.");
|
|
434
|
+
|
|
435
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
436
|
+
await waitFor(() => {
|
|
437
|
+
expect(getText(result.current)).toEqual([
|
|
438
|
+
"earlier",
|
|
439
|
+
"earlier answer",
|
|
440
|
+
"first staged",
|
|
441
|
+
"second staged",
|
|
442
|
+
]);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await act(async () => {
|
|
446
|
+
await result.current.thread.startRun({
|
|
447
|
+
parentId: secondStagedId,
|
|
448
|
+
sourceId: null,
|
|
449
|
+
runConfig: {},
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(send).toHaveBeenCalledTimes(3);
|
|
454
|
+
expect(send).toHaveBeenNthCalledWith(2, { message: "first staged" });
|
|
455
|
+
expect(send).toHaveBeenNthCalledWith(3, { message: "second staged" });
|
|
456
|
+
await waitFor(() => {
|
|
457
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("applies the reload runConfig only to the message being reloaded", async () => {
|
|
462
|
+
const agent = createAgent({ data: settledData });
|
|
463
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
464
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
465
|
+
|
|
466
|
+
const stageWithConfig = async (text: string, page: string) => {
|
|
467
|
+
await act(async () => {
|
|
468
|
+
result.current.thread.append({
|
|
469
|
+
role: "user",
|
|
470
|
+
content: [{ type: "text", text }],
|
|
471
|
+
startRun: false,
|
|
472
|
+
runConfig: { custom: { page } },
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
await stageWithConfig("first staged", "/first");
|
|
478
|
+
await stageWithConfig("second staged", "/second");
|
|
479
|
+
|
|
480
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
481
|
+
await act(async () => {
|
|
482
|
+
await result.current.thread.startRun({
|
|
483
|
+
parentId: secondStagedId,
|
|
484
|
+
sourceId: null,
|
|
485
|
+
runConfig: { custom: { page: "/reloaded" } },
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// the earlier draft keeps the context it was staged with; only the
|
|
490
|
+
// reloaded message takes the reload-time config
|
|
491
|
+
expect(agent.send).toHaveBeenNthCalledWith(1, {
|
|
492
|
+
message: "first staged",
|
|
493
|
+
clientContext: { page: "/first" },
|
|
494
|
+
});
|
|
495
|
+
expect(agent.send).toHaveBeenNthCalledWith(2, {
|
|
496
|
+
message: "second staged",
|
|
497
|
+
clientContext: { page: "/reloaded" },
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("keeps later staged messages visible after promoting one staged parent", async () => {
|
|
502
|
+
const agent = createAgent({ data: settledData });
|
|
503
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
504
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
505
|
+
|
|
506
|
+
await stageMessage(result.current, "first staged");
|
|
507
|
+
await stageMessage(result.current, "second staged");
|
|
508
|
+
|
|
509
|
+
const firstStagedId = result.current.thread.getState().messages[2]!.id;
|
|
510
|
+
await act(async () => {
|
|
511
|
+
await result.current.thread.startRun({
|
|
512
|
+
parentId: firstStagedId,
|
|
513
|
+
sourceId: null,
|
|
514
|
+
runConfig: {},
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
expect(agent.send).toHaveBeenCalledTimes(1);
|
|
519
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
520
|
+
message: "first staged",
|
|
521
|
+
});
|
|
522
|
+
await waitFor(() => {
|
|
523
|
+
expect(getText(result.current)).toEqual([
|
|
524
|
+
"earlier",
|
|
525
|
+
"earlier answer",
|
|
526
|
+
"second staged",
|
|
527
|
+
]);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("promotes the staged prefix sequentially when reloading the last staged message", async () => {
|
|
532
|
+
let resolveFirstSend!: () => void;
|
|
533
|
+
const send = vi
|
|
534
|
+
.fn()
|
|
535
|
+
.mockImplementationOnce(
|
|
536
|
+
() =>
|
|
537
|
+
new Promise<void>((resolve) => {
|
|
538
|
+
resolveFirstSend = resolve;
|
|
539
|
+
}),
|
|
540
|
+
)
|
|
541
|
+
.mockResolvedValue(undefined);
|
|
542
|
+
const agent = createAgent({ data: settledData, send });
|
|
543
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
544
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
545
|
+
|
|
546
|
+
await stageMessage(result.current, "first staged");
|
|
547
|
+
await stageMessage(result.current, "second staged");
|
|
548
|
+
|
|
549
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
550
|
+
act(() => {
|
|
551
|
+
void result.current.thread.startRun({
|
|
552
|
+
parentId: secondStagedId,
|
|
553
|
+
sourceId: null,
|
|
554
|
+
runConfig: {},
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
559
|
+
expect(send).toHaveBeenNthCalledWith(1, {
|
|
560
|
+
message: "first staged",
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await act(async () => {
|
|
564
|
+
resolveFirstSend();
|
|
565
|
+
});
|
|
566
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
567
|
+
expect(send).toHaveBeenNthCalledWith(2, {
|
|
568
|
+
message: "second staged",
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
await waitFor(() => {
|
|
572
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
await expect(
|
|
576
|
+
Promise.resolve(
|
|
577
|
+
result.current.thread.startRun({
|
|
578
|
+
parentId: secondStagedId,
|
|
579
|
+
sourceId: null,
|
|
580
|
+
runConfig: {},
|
|
581
|
+
}),
|
|
582
|
+
),
|
|
583
|
+
).rejects.toThrow("Runtime does not support reloading messages.");
|
|
584
|
+
expect(send).toHaveBeenCalledTimes(2);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("keeps staged drafts when the session collapses to empty", async () => {
|
|
588
|
+
const agent = createAgent({ data: settledData });
|
|
589
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
590
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
591
|
+
|
|
592
|
+
await stageMessage(result.current, "staged draft");
|
|
593
|
+
|
|
594
|
+
mockUseEveAgent.mockReturnValue(
|
|
595
|
+
createAgent({ data: { messages: [] }, send: agent.send }) as never,
|
|
596
|
+
);
|
|
597
|
+
rerender();
|
|
598
|
+
|
|
599
|
+
await waitFor(() => {
|
|
600
|
+
expect(getText(result.current)).toEqual(["staged draft"]);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("still rejects reloading a non-staged message", async () => {
|
|
605
|
+
const agent = createAgent({ data: settledData });
|
|
606
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
607
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
608
|
+
|
|
609
|
+
await stageMessage(result.current, "staged draft");
|
|
610
|
+
|
|
611
|
+
await expect(
|
|
612
|
+
Promise.resolve(
|
|
613
|
+
result.current.thread.startRun({
|
|
614
|
+
parentId: "a1",
|
|
615
|
+
sourceId: null,
|
|
616
|
+
runConfig: {},
|
|
617
|
+
}),
|
|
618
|
+
),
|
|
619
|
+
).rejects.toThrow("Runtime does not support reloading messages.");
|
|
620
|
+
expect(agent.send).not.toHaveBeenCalled();
|
|
621
|
+
});
|
|
622
|
+
});
|