@assistant-ui/eve 0.0.13 → 0.0.15
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 +45 -5
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +91 -14
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/eveExtras.d.ts +7 -0
- package/dist/eveExtras.d.ts.map +1 -0
- package/dist/eveExtras.js +7 -0
- package/dist/eveExtras.js.map +1 -0
- package/dist/hooks.d.ts +25 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +32 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +61 -26
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +7 -6
- package/src/convertEveMessages.test.ts +698 -6
- package/src/convertEveMessages.ts +185 -14
- package/src/eveExtras.ts +10 -0
- package/src/hooks.test.tsx +84 -0
- package/src/hooks.ts +36 -0
- package/src/index.ts +3 -0
- package/src/useEveAgentRuntime.refetch.test.tsx +89 -0
- package/src/useEveAgentRuntime.test.tsx +1037 -167
- package/src/useEveAgentRuntime.ts +148 -42
|
@@ -14,6 +14,7 @@ vi.mock("eve/react", async (importOriginal) => ({
|
|
|
14
14
|
|
|
15
15
|
import type { EveMessageData } from "eve/react";
|
|
16
16
|
import { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
17
|
+
import { eveExtras } from "./eveExtras";
|
|
17
18
|
|
|
18
19
|
const stuckStreamingData: EveMessageData = {
|
|
19
20
|
messages: [
|
|
@@ -34,6 +35,7 @@ const createAgent = (overrides: Record<string, unknown>) => ({
|
|
|
34
35
|
session: undefined,
|
|
35
36
|
status: "ready",
|
|
36
37
|
send: vi.fn(),
|
|
38
|
+
respond: vi.fn(),
|
|
37
39
|
stop: vi.fn(),
|
|
38
40
|
reset: vi.fn(),
|
|
39
41
|
...overrides,
|
|
@@ -160,8 +162,7 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
await waitFor(() => {
|
|
163
|
-
expect(agent.send).toHaveBeenCalledWith({
|
|
164
|
-
message: "hello",
|
|
165
|
+
expect(agent.send).toHaveBeenCalledWith("hello", {
|
|
165
166
|
clientContext: { page: "/pricing" },
|
|
166
167
|
});
|
|
167
168
|
});
|
|
@@ -183,7 +184,7 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
183
184
|
});
|
|
184
185
|
|
|
185
186
|
await waitFor(() => {
|
|
186
|
-
expect(agent.send).toHaveBeenCalledWith(
|
|
187
|
+
expect(agent.send).toHaveBeenCalledWith("hello", undefined);
|
|
187
188
|
});
|
|
188
189
|
});
|
|
189
190
|
|
|
@@ -204,7 +205,7 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
204
205
|
});
|
|
205
206
|
|
|
206
207
|
await waitFor(() => {
|
|
207
|
-
expect(agent.send).toHaveBeenCalledWith(
|
|
208
|
+
expect(agent.send).toHaveBeenCalledWith("hello", undefined);
|
|
208
209
|
});
|
|
209
210
|
});
|
|
210
211
|
|
|
@@ -238,8 +239,7 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
238
239
|
});
|
|
239
240
|
|
|
240
241
|
await waitFor(() => {
|
|
241
|
-
expect(agent.send).toHaveBeenCalledWith({
|
|
242
|
-
message: "hello",
|
|
242
|
+
expect(agent.send).toHaveBeenCalledWith("hello", {
|
|
243
243
|
clientContext: { page: "/reload" },
|
|
244
244
|
});
|
|
245
245
|
});
|
|
@@ -272,8 +272,7 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
272
272
|
});
|
|
273
273
|
|
|
274
274
|
await waitFor(() => {
|
|
275
|
-
expect(agent.send).toHaveBeenCalledWith({
|
|
276
|
-
message: "hello",
|
|
275
|
+
expect(agent.send).toHaveBeenCalledWith("hello", {
|
|
277
276
|
clientContext: { page: "/staged" },
|
|
278
277
|
});
|
|
279
278
|
});
|
|
@@ -310,8 +309,7 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
310
309
|
});
|
|
311
310
|
|
|
312
311
|
await waitFor(() => {
|
|
313
|
-
expect(agent.send).toHaveBeenCalledWith({
|
|
314
|
-
message: "hello",
|
|
312
|
+
expect(agent.send).toHaveBeenCalledWith("hello", {
|
|
315
313
|
clientContext: { page: "/staged" },
|
|
316
314
|
});
|
|
317
315
|
});
|
|
@@ -330,6 +328,163 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
330
328
|
});
|
|
331
329
|
});
|
|
332
330
|
|
|
331
|
+
describe("useEveAgentRuntime tool approval responses", () => {
|
|
332
|
+
const textRequestData: EveMessageData = {
|
|
333
|
+
messages: [
|
|
334
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
335
|
+
{
|
|
336
|
+
id: "a1",
|
|
337
|
+
role: "assistant",
|
|
338
|
+
parts: [
|
|
339
|
+
{
|
|
340
|
+
type: "dynamic-tool",
|
|
341
|
+
state: "approval-requested",
|
|
342
|
+
toolCallId: "call_1",
|
|
343
|
+
toolName: "ask_question",
|
|
344
|
+
input: {},
|
|
345
|
+
approval: { id: "req_1" },
|
|
346
|
+
toolMetadata: {
|
|
347
|
+
eve: {
|
|
348
|
+
kind: "tool-call",
|
|
349
|
+
name: "ask_question",
|
|
350
|
+
inputRequest: {
|
|
351
|
+
requestId: "req_1",
|
|
352
|
+
prompt: "What should the subject line be?",
|
|
353
|
+
kind: "question",
|
|
354
|
+
display: "text",
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const flushMicrotasks = () =>
|
|
365
|
+
new Promise((resolve) => setTimeout(resolve, 0));
|
|
366
|
+
|
|
367
|
+
const processEvents = process as unknown as {
|
|
368
|
+
on(event: "unhandledRejection", listener: (reason: unknown) => void): void;
|
|
369
|
+
off(event: "unhandledRejection", listener: (reason: unknown) => void): void;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const respondToTextRequest = (
|
|
373
|
+
result: { current: ReturnType<typeof useEveAgentRuntime> },
|
|
374
|
+
response: { approved: boolean; reason?: string; optionId?: string },
|
|
375
|
+
) =>
|
|
376
|
+
result.current.thread
|
|
377
|
+
.getMessageById("a1")
|
|
378
|
+
.getMessagePartByToolCallId("call_1")
|
|
379
|
+
.respondToToolApproval(response);
|
|
380
|
+
|
|
381
|
+
it("keeps an unanswered free-form request pending through the default controls", async () => {
|
|
382
|
+
const rejections: unknown[] = [];
|
|
383
|
+
const onUnhandledRejection = (reason: unknown) => rejections.push(reason);
|
|
384
|
+
processEvents.on("unhandledRejection", onUnhandledRejection);
|
|
385
|
+
const agent = createAgent({ data: textRequestData });
|
|
386
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
390
|
+
|
|
391
|
+
expect(() => respondToTextRequest(result, { approved: true })).toThrow(
|
|
392
|
+
/was not answered by this response/,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await flushMicrotasks();
|
|
396
|
+
await flushMicrotasks();
|
|
397
|
+
|
|
398
|
+
expect(agent.send).not.toHaveBeenCalled();
|
|
399
|
+
expect(agent.respond).not.toHaveBeenCalled();
|
|
400
|
+
expect(rejections).toEqual([]);
|
|
401
|
+
} finally {
|
|
402
|
+
processEvents.off("unhandledRejection", onUnhandledRejection);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const bareApprovalData = (toolMetadata?: {
|
|
407
|
+
eve: { kind: "tool-call"; name: string };
|
|
408
|
+
}): EveMessageData => ({
|
|
409
|
+
messages: [
|
|
410
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
411
|
+
{
|
|
412
|
+
id: "a1",
|
|
413
|
+
role: "assistant",
|
|
414
|
+
parts: [
|
|
415
|
+
{
|
|
416
|
+
type: "dynamic-tool",
|
|
417
|
+
state: "approval-requested",
|
|
418
|
+
toolCallId: "call_1",
|
|
419
|
+
toolName: "delete_file",
|
|
420
|
+
input: {},
|
|
421
|
+
approval: { id: "req_1" },
|
|
422
|
+
...(toolMetadata && { toolMetadata }),
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it.each([
|
|
430
|
+
["no eve tool metadata at all", undefined],
|
|
431
|
+
[
|
|
432
|
+
"eve metadata carrying no input request",
|
|
433
|
+
{ eve: { kind: "tool-call", name: "delete_file" } },
|
|
434
|
+
],
|
|
435
|
+
])(
|
|
436
|
+
"still answers an ordinary approval when the part has %s",
|
|
437
|
+
async (_label, toolMetadata) => {
|
|
438
|
+
const agent = createAgent({
|
|
439
|
+
data: bareApprovalData(toolMetadata as never),
|
|
440
|
+
});
|
|
441
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
442
|
+
|
|
443
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
444
|
+
expect(() =>
|
|
445
|
+
respondToTextRequest(result, { approved: true }),
|
|
446
|
+
).not.toThrow();
|
|
447
|
+
|
|
448
|
+
await flushMicrotasks();
|
|
449
|
+
|
|
450
|
+
expect(agent.respond).toHaveBeenCalledWith([
|
|
451
|
+
{ requestId: "req_1", optionId: "approve" },
|
|
452
|
+
]);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
it("maps a refusal on a request the data does not carry", async () => {
|
|
457
|
+
const agent = createAgent({ data: bareApprovalData(undefined) });
|
|
458
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
459
|
+
|
|
460
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
461
|
+
respondToTextRequest(result, { approved: false, reason: "not safe" });
|
|
462
|
+
|
|
463
|
+
await flushMicrotasks();
|
|
464
|
+
|
|
465
|
+
expect(agent.respond).toHaveBeenCalledWith([
|
|
466
|
+
{ requestId: "req_1", optionId: "cancel", text: "not safe" },
|
|
467
|
+
]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("submits a free-form answer as text without an option id", async () => {
|
|
471
|
+
const agent = createAgent({ data: textRequestData });
|
|
472
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
473
|
+
|
|
474
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
475
|
+
respondToTextRequest(result, {
|
|
476
|
+
approved: true,
|
|
477
|
+
reason: "Quarterly results",
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await flushMicrotasks();
|
|
481
|
+
|
|
482
|
+
expect(agent.respond).toHaveBeenCalledWith([
|
|
483
|
+
{ requestId: "req_1", text: "Quarterly results" },
|
|
484
|
+
]);
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
333
488
|
const settledData: EveMessageData = {
|
|
334
489
|
messages: [
|
|
335
490
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
|
|
@@ -341,6 +496,51 @@ const settledData: EveMessageData = {
|
|
|
341
496
|
],
|
|
342
497
|
};
|
|
343
498
|
|
|
499
|
+
const executingToolData: EveMessageData = {
|
|
500
|
+
messages: [
|
|
501
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "run it" }] },
|
|
502
|
+
{
|
|
503
|
+
id: "a1",
|
|
504
|
+
role: "assistant",
|
|
505
|
+
parts: [
|
|
506
|
+
{
|
|
507
|
+
type: "dynamic-tool",
|
|
508
|
+
state: "input-available",
|
|
509
|
+
toolCallId: "call_slow",
|
|
510
|
+
toolName: "slow_tool",
|
|
511
|
+
input: {},
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const twoExecutingToolsData: EveMessageData = {
|
|
519
|
+
messages: [
|
|
520
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "run them" }] },
|
|
521
|
+
{
|
|
522
|
+
id: "a1",
|
|
523
|
+
role: "assistant",
|
|
524
|
+
parts: [
|
|
525
|
+
{
|
|
526
|
+
type: "dynamic-tool",
|
|
527
|
+
state: "input-available",
|
|
528
|
+
toolCallId: "call_slow_a",
|
|
529
|
+
toolName: "slow_tool",
|
|
530
|
+
input: {},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
type: "dynamic-tool",
|
|
534
|
+
state: "input-available",
|
|
535
|
+
toolCallId: "call_slow_b",
|
|
536
|
+
toolName: "slow_tool",
|
|
537
|
+
input: {},
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
};
|
|
543
|
+
|
|
344
544
|
const getText = (runtime: ReturnType<typeof useEveAgentRuntime>) =>
|
|
345
545
|
runtime.thread
|
|
346
546
|
.getState()
|
|
@@ -394,9 +594,7 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
394
594
|
content: [{ type: "text", text: "hello" }],
|
|
395
595
|
});
|
|
396
596
|
});
|
|
397
|
-
expect(agent.send).toHaveBeenCalledWith(
|
|
398
|
-
message: "hello",
|
|
399
|
-
});
|
|
597
|
+
expect(agent.send).toHaveBeenCalledWith("hello", undefined);
|
|
400
598
|
|
|
401
599
|
mockUseEveAgent.mockReturnValue(
|
|
402
600
|
createAgent({
|
|
@@ -504,8 +702,8 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
504
702
|
});
|
|
505
703
|
|
|
506
704
|
expect(send).toHaveBeenCalledTimes(3);
|
|
507
|
-
expect(send).toHaveBeenNthCalledWith(2,
|
|
508
|
-
expect(send).toHaveBeenNthCalledWith(3,
|
|
705
|
+
expect(send).toHaveBeenNthCalledWith(2, "first staged", undefined);
|
|
706
|
+
expect(send).toHaveBeenNthCalledWith(3, "second staged", undefined);
|
|
509
707
|
await waitFor(() => {
|
|
510
708
|
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
511
709
|
});
|
|
@@ -541,12 +739,10 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
541
739
|
|
|
542
740
|
// the earlier draft keeps the context it was staged with; only the
|
|
543
741
|
// reloaded message takes the reload-time config
|
|
544
|
-
expect(agent.send).toHaveBeenNthCalledWith(1, {
|
|
545
|
-
message: "first staged",
|
|
742
|
+
expect(agent.send).toHaveBeenNthCalledWith(1, "first staged", {
|
|
546
743
|
clientContext: { page: "/first" },
|
|
547
744
|
});
|
|
548
|
-
expect(agent.send).toHaveBeenNthCalledWith(2, {
|
|
549
|
-
message: "second staged",
|
|
745
|
+
expect(agent.send).toHaveBeenNthCalledWith(2, "second staged", {
|
|
550
746
|
clientContext: { page: "/reloaded" },
|
|
551
747
|
});
|
|
552
748
|
});
|
|
@@ -569,9 +765,7 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
569
765
|
});
|
|
570
766
|
|
|
571
767
|
expect(agent.send).toHaveBeenCalledTimes(1);
|
|
572
|
-
expect(agent.send).toHaveBeenCalledWith(
|
|
573
|
-
message: "first staged",
|
|
574
|
-
});
|
|
768
|
+
expect(agent.send).toHaveBeenCalledWith("first staged", undefined);
|
|
575
769
|
await waitFor(() => {
|
|
576
770
|
expect(getText(result.current)).toEqual([
|
|
577
771
|
"earlier",
|
|
@@ -609,17 +803,13 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
609
803
|
});
|
|
610
804
|
|
|
611
805
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
612
|
-
expect(send).toHaveBeenNthCalledWith(1,
|
|
613
|
-
message: "first staged",
|
|
614
|
-
});
|
|
806
|
+
expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
|
|
615
807
|
|
|
616
808
|
await act(async () => {
|
|
617
809
|
resolveFirstSend();
|
|
618
810
|
});
|
|
619
811
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
620
|
-
expect(send).toHaveBeenNthCalledWith(2,
|
|
621
|
-
message: "second staged",
|
|
622
|
-
});
|
|
812
|
+
expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
|
|
623
813
|
|
|
624
814
|
await waitFor(() => {
|
|
625
815
|
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
@@ -681,7 +871,7 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
681
871
|
});
|
|
682
872
|
|
|
683
873
|
expect(send).toHaveBeenCalledTimes(1);
|
|
684
|
-
expect(send).toHaveBeenCalledWith(
|
|
874
|
+
expect(send).toHaveBeenCalledWith("first staged", undefined);
|
|
685
875
|
await waitFor(() => {
|
|
686
876
|
expect(getText(result.current)).toEqual([
|
|
687
877
|
"earlier",
|
|
@@ -756,129 +946,101 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
756
946
|
});
|
|
757
947
|
});
|
|
758
948
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
{
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
type: "dynamic-tool",
|
|
768
|
-
state: "approval-requested",
|
|
769
|
-
toolCallId: "call_1",
|
|
770
|
-
toolName: "send_email",
|
|
771
|
-
input: { to: "dev@example.com" },
|
|
772
|
-
approval: { id: "req_1" },
|
|
773
|
-
toolMetadata: {
|
|
774
|
-
eve: {
|
|
775
|
-
kind: "tool-call",
|
|
776
|
-
name: "send_email",
|
|
777
|
-
inputRequest: {
|
|
778
|
-
requestId: "req_1",
|
|
779
|
-
prompt: "Send the email?",
|
|
780
|
-
display: "confirmation",
|
|
781
|
-
options: [
|
|
782
|
-
{ id: "approve", label: "Approve" },
|
|
783
|
-
{ id: "deny", label: "Deny" },
|
|
784
|
-
],
|
|
785
|
-
},
|
|
786
|
-
},
|
|
787
|
-
},
|
|
788
|
-
},
|
|
789
|
-
],
|
|
790
|
-
},
|
|
791
|
-
],
|
|
792
|
-
};
|
|
949
|
+
describe("useEveAgentRuntime extras wiring", () => {
|
|
950
|
+
it("provides error, events, and session through the runtime extras", () => {
|
|
951
|
+
const error = new Error("boom");
|
|
952
|
+
const events = [{ type: "session.started" }];
|
|
953
|
+
const session = { sessionId: "s1" };
|
|
954
|
+
mockUseEveAgent.mockReturnValue(
|
|
955
|
+
createAgent({ status: "error", error, events, session }) as never,
|
|
956
|
+
);
|
|
793
957
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
}),
|
|
804
|
-
)
|
|
805
|
-
.mockResolvedValue(undefined);
|
|
806
|
-
const agent = createAgent({ data: approvalData, send });
|
|
958
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
959
|
+
|
|
960
|
+
expect(
|
|
961
|
+
eveExtras.tryGet(result.current.thread.getState().extras),
|
|
962
|
+
).toMatchObject({ error, events, session });
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it("discards staged inputs when reset is invoked", async () => {
|
|
966
|
+
const agent = createAgent({ data: settledData });
|
|
807
967
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
808
968
|
const { result } = renderHook(() => useEveAgentRuntime());
|
|
809
969
|
|
|
810
|
-
await
|
|
811
|
-
|
|
812
|
-
role: "user",
|
|
813
|
-
content: [{ type: "text", text: "go" }],
|
|
814
|
-
});
|
|
815
|
-
});
|
|
816
|
-
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
970
|
+
await stageMessage(result.current, "discarded draft");
|
|
971
|
+
const discardedId = result.current.thread.getState().messages[2]!.id;
|
|
817
972
|
|
|
818
973
|
act(() => {
|
|
819
|
-
result.current.thread
|
|
820
|
-
.getMessageByIndex(1)
|
|
821
|
-
.getMessagePartByToolCallId("call_1")
|
|
822
|
-
.respondToToolApproval({ optionId: "approve" });
|
|
823
|
-
});
|
|
824
|
-
expect(send).toHaveBeenCalledTimes(1);
|
|
825
|
-
|
|
826
|
-
await act(async () => {
|
|
827
|
-
resolveFirstSend();
|
|
974
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
828
975
|
});
|
|
829
|
-
await waitFor(() =>
|
|
830
|
-
|
|
831
|
-
inputResponses: [{ requestId: "req_1", optionId: "approve" }],
|
|
976
|
+
await waitFor(() => {
|
|
977
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
832
978
|
});
|
|
979
|
+
|
|
980
|
+
// `onReload` is only wired while staged messages exist, so a fresh draft
|
|
981
|
+
// is needed to reach the staged-run lookup that reads `stagedInputsRef`.
|
|
982
|
+
await stageMessage(result.current, "fresh draft");
|
|
983
|
+
|
|
984
|
+
await expect(
|
|
985
|
+
Promise.resolve(
|
|
986
|
+
result.current.thread.startRun({
|
|
987
|
+
parentId: discardedId,
|
|
988
|
+
sourceId: null,
|
|
989
|
+
runConfig: {},
|
|
990
|
+
}),
|
|
991
|
+
),
|
|
992
|
+
).rejects.toThrow("Runtime does not support reloading messages.");
|
|
993
|
+
expect(agent.send).not.toHaveBeenCalled();
|
|
994
|
+
expect(agent.reset).toHaveBeenCalledTimes(1);
|
|
833
995
|
});
|
|
834
996
|
|
|
835
|
-
it("
|
|
836
|
-
|
|
837
|
-
const send = vi
|
|
838
|
-
.fn()
|
|
839
|
-
.mockImplementationOnce(
|
|
840
|
-
() =>
|
|
841
|
-
new Promise<void>((resolve) => {
|
|
842
|
-
resolveFirstSend = resolve;
|
|
843
|
-
}),
|
|
844
|
-
)
|
|
845
|
-
.mockResolvedValue(undefined);
|
|
846
|
-
const agent = createAgent({ data: settledData, send });
|
|
997
|
+
it("clears executing tool state when reset is invoked", async () => {
|
|
998
|
+
const agent = createAgent({ data: settledData });
|
|
847
999
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
848
|
-
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1000
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
849
1001
|
|
|
850
|
-
|
|
851
|
-
result.current.
|
|
852
|
-
|
|
853
|
-
|
|
1002
|
+
act(() => {
|
|
1003
|
+
result.current.registerModelContextProvider({
|
|
1004
|
+
getModelContext: () => ({
|
|
1005
|
+
tools: {
|
|
1006
|
+
slow_tool: {
|
|
1007
|
+
parameters: { type: "object", properties: {} },
|
|
1008
|
+
execute: () => new Promise<never>(() => {}),
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
}),
|
|
854
1012
|
});
|
|
855
1013
|
});
|
|
856
|
-
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
857
1014
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1015
|
+
// The tracker treats its first snapshot as historical, so the tool call
|
|
1016
|
+
// has to arrive on a later one to actually execute.
|
|
1017
|
+
mockUseEveAgent.mockReturnValue(
|
|
1018
|
+
createAgent({ data: executingToolData, reset: agent.reset }) as never,
|
|
1019
|
+
);
|
|
1020
|
+
rerender();
|
|
1021
|
+
|
|
1022
|
+
await waitFor(() => {
|
|
1023
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
863
1024
|
});
|
|
864
|
-
expect(send).toHaveBeenCalledTimes(1);
|
|
865
1025
|
|
|
866
|
-
|
|
867
|
-
|
|
1026
|
+
act(() => {
|
|
1027
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
868
1028
|
});
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1029
|
+
|
|
1030
|
+
await waitFor(() => {
|
|
1031
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1032
|
+
});
|
|
1033
|
+
expect(agent.reset).toHaveBeenCalledTimes(1);
|
|
872
1034
|
});
|
|
873
1035
|
|
|
874
|
-
it("drops a queued
|
|
875
|
-
let
|
|
1036
|
+
it("drops a send queued behind an active turn when reset is invoked", async () => {
|
|
1037
|
+
let releaseFirstSend: (() => void) | undefined;
|
|
876
1038
|
const send = vi
|
|
877
1039
|
.fn()
|
|
878
1040
|
.mockImplementationOnce(
|
|
879
1041
|
() =>
|
|
880
1042
|
new Promise<void>((resolve) => {
|
|
881
|
-
|
|
1043
|
+
releaseFirstSend = resolve;
|
|
882
1044
|
}),
|
|
883
1045
|
)
|
|
884
1046
|
.mockResolvedValue(undefined);
|
|
@@ -886,39 +1048,52 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
886
1048
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
887
1049
|
const { result } = renderHook(() => useEveAgentRuntime());
|
|
888
1050
|
|
|
889
|
-
await
|
|
1051
|
+
await stageMessage(result.current, "queued draft");
|
|
1052
|
+
const queuedDraftId = result.current.thread.getState().messages[2]!.id;
|
|
1053
|
+
|
|
1054
|
+
act(() => {
|
|
890
1055
|
result.current.thread.append({
|
|
891
1056
|
role: "user",
|
|
892
1057
|
content: [{ type: "text", text: "first" }],
|
|
893
1058
|
});
|
|
894
1059
|
});
|
|
895
|
-
await waitFor(() =>
|
|
896
|
-
|
|
897
|
-
await act(async () => {
|
|
898
|
-
result.current.thread.append({
|
|
899
|
-
role: "user",
|
|
900
|
-
content: [{ type: "text", text: "queued" }],
|
|
901
|
-
});
|
|
1060
|
+
await waitFor(() => {
|
|
1061
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
902
1062
|
});
|
|
1063
|
+
|
|
1064
|
+
const queuedReload = Promise.resolve(
|
|
1065
|
+
result.current.thread.startRun({
|
|
1066
|
+
parentId: queuedDraftId,
|
|
1067
|
+
sourceId: null,
|
|
1068
|
+
runConfig: {},
|
|
1069
|
+
}),
|
|
1070
|
+
);
|
|
1071
|
+
// Let the reload reach `enqueueSend` and park behind the active turn.
|
|
1072
|
+
await act(async () => {});
|
|
1073
|
+
|
|
903
1074
|
act(() => {
|
|
904
|
-
result.current.thread.
|
|
1075
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
905
1076
|
});
|
|
906
|
-
expect(agent.stop).toHaveBeenCalledTimes(1);
|
|
907
1077
|
|
|
908
1078
|
await act(async () => {
|
|
909
|
-
|
|
1079
|
+
releaseFirstSend?.();
|
|
1080
|
+
await queuedReload;
|
|
910
1081
|
});
|
|
1082
|
+
|
|
911
1083
|
expect(send).toHaveBeenCalledTimes(1);
|
|
1084
|
+
await waitFor(() => {
|
|
1085
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
1086
|
+
});
|
|
912
1087
|
});
|
|
913
1088
|
|
|
914
|
-
it("
|
|
915
|
-
let
|
|
1089
|
+
it("keeps a staged draft when a queued send is cancelled instead of reset", async () => {
|
|
1090
|
+
let releaseFirstSend: (() => void) | undefined;
|
|
916
1091
|
const send = vi
|
|
917
1092
|
.fn()
|
|
918
1093
|
.mockImplementationOnce(
|
|
919
1094
|
() =>
|
|
920
1095
|
new Promise<void>((resolve) => {
|
|
921
|
-
|
|
1096
|
+
releaseFirstSend = resolve;
|
|
922
1097
|
}),
|
|
923
1098
|
)
|
|
924
1099
|
.mockResolvedValue(undefined);
|
|
@@ -926,35 +1101,449 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
926
1101
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
927
1102
|
const { result } = renderHook(() => useEveAgentRuntime());
|
|
928
1103
|
|
|
929
|
-
await
|
|
1104
|
+
await stageMessage(result.current, "queued draft");
|
|
1105
|
+
const queuedDraftId = result.current.thread.getState().messages[2]!.id;
|
|
1106
|
+
|
|
1107
|
+
act(() => {
|
|
930
1108
|
result.current.thread.append({
|
|
931
1109
|
role: "user",
|
|
932
1110
|
content: [{ type: "text", text: "first" }],
|
|
933
1111
|
});
|
|
934
1112
|
});
|
|
935
|
-
await waitFor(() =>
|
|
936
|
-
|
|
937
|
-
await act(async () => {
|
|
938
|
-
result.current.thread.composer.setText("queued");
|
|
939
|
-
result.current.thread.composer.send();
|
|
1113
|
+
await waitFor(() => {
|
|
1114
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
940
1115
|
});
|
|
941
|
-
|
|
1116
|
+
|
|
1117
|
+
const queuedReload = Promise.resolve(
|
|
1118
|
+
result.current.thread.startRun({
|
|
1119
|
+
parentId: queuedDraftId,
|
|
1120
|
+
sourceId: null,
|
|
1121
|
+
runConfig: {},
|
|
1122
|
+
}),
|
|
1123
|
+
);
|
|
1124
|
+
await act(async () => {});
|
|
942
1125
|
|
|
943
1126
|
act(() => {
|
|
944
1127
|
result.current.thread.cancelRun();
|
|
945
1128
|
});
|
|
1129
|
+
|
|
946
1130
|
await act(async () => {
|
|
947
|
-
|
|
1131
|
+
releaseFirstSend?.();
|
|
1132
|
+
await queuedReload;
|
|
948
1133
|
});
|
|
949
1134
|
|
|
1135
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1136
|
+
expect(agent.stop).toHaveBeenCalledTimes(1);
|
|
950
1137
|
await waitFor(() => {
|
|
951
|
-
expect(result.current
|
|
1138
|
+
expect(getText(result.current)).toEqual([
|
|
1139
|
+
"earlier",
|
|
1140
|
+
"earlier answer",
|
|
1141
|
+
"queued draft",
|
|
1142
|
+
]);
|
|
952
1143
|
});
|
|
953
|
-
expect(send).toHaveBeenCalledTimes(1);
|
|
954
|
-
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
955
1144
|
});
|
|
956
1145
|
|
|
957
|
-
it("
|
|
1146
|
+
it("aborts client tool executions when reset is invoked", async () => {
|
|
1147
|
+
const agentReset = vi.fn(() => {
|
|
1148
|
+
mockUseEveAgent.mockReturnValue(
|
|
1149
|
+
createAgent({ data: { messages: [] }, reset: agentReset }) as never,
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
1152
|
+
const agent = createAgent({ data: settledData, reset: agentReset });
|
|
1153
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1154
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1155
|
+
|
|
1156
|
+
const abortReasons: string[] = [];
|
|
1157
|
+
act(() => {
|
|
1158
|
+
result.current.registerModelContextProvider({
|
|
1159
|
+
getModelContext: () => ({
|
|
1160
|
+
tools: {
|
|
1161
|
+
slow_tool: {
|
|
1162
|
+
parameters: { type: "object", properties: {} },
|
|
1163
|
+
execute: (
|
|
1164
|
+
_args: unknown,
|
|
1165
|
+
context: { toolCallId: string; abortSignal: AbortSignal },
|
|
1166
|
+
) =>
|
|
1167
|
+
new Promise<never>(() => {
|
|
1168
|
+
context.abortSignal.addEventListener("abort", () => {
|
|
1169
|
+
abortReasons.push(context.toolCallId);
|
|
1170
|
+
});
|
|
1171
|
+
}),
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
}),
|
|
1175
|
+
});
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
mockUseEveAgent.mockReturnValue(
|
|
1179
|
+
createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
|
|
1180
|
+
);
|
|
1181
|
+
rerender();
|
|
1182
|
+
|
|
1183
|
+
await waitFor(() => {
|
|
1184
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
act(() => {
|
|
1188
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1189
|
+
});
|
|
1190
|
+
rerender();
|
|
1191
|
+
|
|
1192
|
+
await waitFor(() => {
|
|
1193
|
+
expect(abortReasons.sort()).toEqual(["call_slow_a", "call_slow_b"]);
|
|
1194
|
+
});
|
|
1195
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it("rejects a pending human-input request when reset is invoked", async () => {
|
|
1199
|
+
const agentReset = vi.fn(() => {
|
|
1200
|
+
mockUseEveAgent.mockReturnValue(
|
|
1201
|
+
createAgent({ data: { messages: [] }, reset: agentReset }) as never,
|
|
1202
|
+
);
|
|
1203
|
+
});
|
|
1204
|
+
const agent = createAgent({ data: settledData, reset: agentReset });
|
|
1205
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1206
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1207
|
+
|
|
1208
|
+
const humanRejections: unknown[] = [];
|
|
1209
|
+
act(() => {
|
|
1210
|
+
result.current.registerModelContextProvider({
|
|
1211
|
+
getModelContext: () => ({
|
|
1212
|
+
tools: {
|
|
1213
|
+
slow_tool: {
|
|
1214
|
+
parameters: { type: "object", properties: {} },
|
|
1215
|
+
execute: async (
|
|
1216
|
+
_args: unknown,
|
|
1217
|
+
context: { human: (payload: unknown) => Promise<unknown> },
|
|
1218
|
+
) => {
|
|
1219
|
+
try {
|
|
1220
|
+
return await context.human({ request: "approve" });
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
humanRejections.push(error);
|
|
1223
|
+
throw error;
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
},
|
|
1227
|
+
},
|
|
1228
|
+
}),
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
mockUseEveAgent.mockReturnValue(
|
|
1233
|
+
createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
|
|
1234
|
+
);
|
|
1235
|
+
rerender();
|
|
1236
|
+
|
|
1237
|
+
await waitFor(() => {
|
|
1238
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
act(() => {
|
|
1242
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1243
|
+
});
|
|
1244
|
+
rerender();
|
|
1245
|
+
|
|
1246
|
+
await waitFor(() => {
|
|
1247
|
+
expect(humanRejections).toHaveLength(2);
|
|
1248
|
+
});
|
|
1249
|
+
expect((humanRejections[0] as Error).message).toBe(
|
|
1250
|
+
"Tool execution aborted",
|
|
1251
|
+
);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it("ignores tool statuses left over from a discarded session", async () => {
|
|
1255
|
+
const releases: Record<string, () => void> = {};
|
|
1256
|
+
const agentReset = vi.fn(() => {
|
|
1257
|
+
mockUseEveAgent.mockReturnValue(
|
|
1258
|
+
createAgent({ data: { messages: [] }, reset: agentReset }) as never,
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
const agent = createAgent({ data: settledData, reset: agentReset });
|
|
1262
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1263
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1264
|
+
|
|
1265
|
+
act(() => {
|
|
1266
|
+
result.current.registerModelContextProvider({
|
|
1267
|
+
getModelContext: () => ({
|
|
1268
|
+
tools: {
|
|
1269
|
+
slow_tool: {
|
|
1270
|
+
parameters: { type: "object", properties: {} },
|
|
1271
|
+
execute: (_args: unknown, context: { toolCallId: string }) =>
|
|
1272
|
+
new Promise<never>((_resolve, reject) => {
|
|
1273
|
+
releases[context.toolCallId] = () =>
|
|
1274
|
+
reject(new Error("aborted"));
|
|
1275
|
+
}),
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
}),
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
mockUseEveAgent.mockReturnValue(
|
|
1283
|
+
createAgent({
|
|
1284
|
+
data: twoExecutingToolsData,
|
|
1285
|
+
reset: agentReset,
|
|
1286
|
+
}) as never,
|
|
1287
|
+
);
|
|
1288
|
+
rerender();
|
|
1289
|
+
|
|
1290
|
+
await waitFor(() => {
|
|
1291
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
act(() => {
|
|
1295
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1296
|
+
});
|
|
1297
|
+
rerender();
|
|
1298
|
+
|
|
1299
|
+
await waitFor(() => {
|
|
1300
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
await act(async () => {
|
|
1304
|
+
releases["call_slow_a"]?.();
|
|
1305
|
+
await Promise.resolve();
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
await waitFor(() => {
|
|
1309
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1310
|
+
});
|
|
1311
|
+
expect(agentReset).toHaveBeenCalledTimes(1);
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it("promotes every staged draft after reset when the discarded run failed", async () => {
|
|
1315
|
+
const send = vi.fn().mockResolvedValue(undefined);
|
|
1316
|
+
const agent = createAgent({ data: settledData, send });
|
|
1317
|
+
let capturedOptions: { onFinish?: (snapshot: unknown) => void } = {};
|
|
1318
|
+
mockUseEveAgent.mockImplementation((options) => {
|
|
1319
|
+
capturedOptions = options as typeof capturedOptions;
|
|
1320
|
+
return agent as never;
|
|
1321
|
+
});
|
|
1322
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1323
|
+
|
|
1324
|
+
act(() => {
|
|
1325
|
+
capturedOptions.onFinish?.({ status: "error" });
|
|
1326
|
+
});
|
|
1327
|
+
act(() => {
|
|
1328
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
await stageMessage(result.current, "first staged");
|
|
1332
|
+
await stageMessage(result.current, "second staged");
|
|
1333
|
+
|
|
1334
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
1335
|
+
await act(async () => {
|
|
1336
|
+
await result.current.thread.startRun({
|
|
1337
|
+
parentId: secondStagedId,
|
|
1338
|
+
sourceId: null,
|
|
1339
|
+
runConfig: {},
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
expect(send).toHaveBeenCalledTimes(2);
|
|
1344
|
+
expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
|
|
1345
|
+
expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const approvalData: EveMessageData = {
|
|
1350
|
+
messages: [
|
|
1351
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "send it" }] },
|
|
1352
|
+
{
|
|
1353
|
+
id: "a1",
|
|
1354
|
+
role: "assistant",
|
|
1355
|
+
parts: [
|
|
1356
|
+
{
|
|
1357
|
+
type: "dynamic-tool",
|
|
1358
|
+
state: "approval-requested",
|
|
1359
|
+
toolCallId: "call_1",
|
|
1360
|
+
toolName: "send_email",
|
|
1361
|
+
input: { to: "dev@example.com" },
|
|
1362
|
+
approval: { id: "req_1" },
|
|
1363
|
+
toolMetadata: {
|
|
1364
|
+
eve: {
|
|
1365
|
+
kind: "tool-call",
|
|
1366
|
+
name: "send_email",
|
|
1367
|
+
inputRequest: {
|
|
1368
|
+
requestId: "req_1",
|
|
1369
|
+
kind: "tool-approval",
|
|
1370
|
+
prompt: "Send the email?",
|
|
1371
|
+
display: "confirmation",
|
|
1372
|
+
options: [
|
|
1373
|
+
{ id: "approve", label: "Approve" },
|
|
1374
|
+
{ id: "cancel", label: "Cancel" },
|
|
1375
|
+
],
|
|
1376
|
+
},
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
],
|
|
1381
|
+
},
|
|
1382
|
+
],
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
describe("useEveAgentRuntime concurrent sends", () => {
|
|
1386
|
+
it("defers an approval clicked while a turn is in flight until the turn parks", async () => {
|
|
1387
|
+
let resolveFirstSend!: () => void;
|
|
1388
|
+
const send = vi.fn().mockImplementationOnce(
|
|
1389
|
+
() =>
|
|
1390
|
+
new Promise<void>((resolve) => {
|
|
1391
|
+
resolveFirstSend = resolve;
|
|
1392
|
+
}),
|
|
1393
|
+
);
|
|
1394
|
+
const respond = vi.fn().mockResolvedValue(undefined);
|
|
1395
|
+
const agent = createAgent({ data: approvalData, send, respond });
|
|
1396
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1397
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1398
|
+
|
|
1399
|
+
await act(async () => {
|
|
1400
|
+
result.current.thread.append({
|
|
1401
|
+
role: "user",
|
|
1402
|
+
content: [{ type: "text", text: "go" }],
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1406
|
+
|
|
1407
|
+
act(() => {
|
|
1408
|
+
result.current.thread
|
|
1409
|
+
.getMessageByIndex(1)
|
|
1410
|
+
.getMessagePartByToolCallId("call_1")
|
|
1411
|
+
.respondToToolApproval({ optionId: "approve" });
|
|
1412
|
+
});
|
|
1413
|
+
expect(respond).not.toHaveBeenCalled();
|
|
1414
|
+
|
|
1415
|
+
await act(async () => {
|
|
1416
|
+
resolveFirstSend();
|
|
1417
|
+
});
|
|
1418
|
+
await waitFor(() => expect(respond).toHaveBeenCalledTimes(1));
|
|
1419
|
+
expect(respond).toHaveBeenCalledWith([
|
|
1420
|
+
{ requestId: "req_1", optionId: "approve" },
|
|
1421
|
+
]);
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
it("queues a send issued while a turn is in flight and preserves order", async () => {
|
|
1425
|
+
let resolveFirstSend!: () => void;
|
|
1426
|
+
const send = vi
|
|
1427
|
+
.fn()
|
|
1428
|
+
.mockImplementationOnce(
|
|
1429
|
+
() =>
|
|
1430
|
+
new Promise<void>((resolve) => {
|
|
1431
|
+
resolveFirstSend = resolve;
|
|
1432
|
+
}),
|
|
1433
|
+
)
|
|
1434
|
+
.mockResolvedValue(undefined);
|
|
1435
|
+
const agent = createAgent({ data: settledData, send });
|
|
1436
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1437
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1438
|
+
|
|
1439
|
+
await act(async () => {
|
|
1440
|
+
result.current.thread.append({
|
|
1441
|
+
role: "user",
|
|
1442
|
+
content: [{ type: "text", text: "first" }],
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1446
|
+
|
|
1447
|
+
await act(async () => {
|
|
1448
|
+
result.current.thread.append({
|
|
1449
|
+
role: "user",
|
|
1450
|
+
content: [{ type: "text", text: "second" }],
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1454
|
+
|
|
1455
|
+
await act(async () => {
|
|
1456
|
+
resolveFirstSend();
|
|
1457
|
+
});
|
|
1458
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
1459
|
+
expect(send).toHaveBeenNthCalledWith(1, "first", undefined);
|
|
1460
|
+
expect(send).toHaveBeenNthCalledWith(2, "second", undefined);
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
it("drops a queued send when the run is cancelled before it dispatches", async () => {
|
|
1464
|
+
let resolveFirstSend!: () => void;
|
|
1465
|
+
const send = vi
|
|
1466
|
+
.fn()
|
|
1467
|
+
.mockImplementationOnce(
|
|
1468
|
+
() =>
|
|
1469
|
+
new Promise<void>((resolve) => {
|
|
1470
|
+
resolveFirstSend = resolve;
|
|
1471
|
+
}),
|
|
1472
|
+
)
|
|
1473
|
+
.mockResolvedValue(undefined);
|
|
1474
|
+
const agent = createAgent({ data: settledData, send });
|
|
1475
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1476
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1477
|
+
|
|
1478
|
+
await act(async () => {
|
|
1479
|
+
result.current.thread.append({
|
|
1480
|
+
role: "user",
|
|
1481
|
+
content: [{ type: "text", text: "first" }],
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1485
|
+
|
|
1486
|
+
await act(async () => {
|
|
1487
|
+
result.current.thread.append({
|
|
1488
|
+
role: "user",
|
|
1489
|
+
content: [{ type: "text", text: "queued" }],
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
act(() => {
|
|
1493
|
+
result.current.thread.cancelRun();
|
|
1494
|
+
});
|
|
1495
|
+
expect(agent.stop).toHaveBeenCalledTimes(1);
|
|
1496
|
+
|
|
1497
|
+
await act(async () => {
|
|
1498
|
+
resolveFirstSend();
|
|
1499
|
+
});
|
|
1500
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
it("returns a cancelled queued send to the composer", async () => {
|
|
1504
|
+
let resolveFirstSend!: () => void;
|
|
1505
|
+
const send = vi
|
|
1506
|
+
.fn()
|
|
1507
|
+
.mockImplementationOnce(
|
|
1508
|
+
() =>
|
|
1509
|
+
new Promise<void>((resolve) => {
|
|
1510
|
+
resolveFirstSend = resolve;
|
|
1511
|
+
}),
|
|
1512
|
+
)
|
|
1513
|
+
.mockResolvedValue(undefined);
|
|
1514
|
+
const agent = createAgent({ data: settledData, send });
|
|
1515
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1516
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1517
|
+
|
|
1518
|
+
await act(async () => {
|
|
1519
|
+
result.current.thread.append({
|
|
1520
|
+
role: "user",
|
|
1521
|
+
content: [{ type: "text", text: "first" }],
|
|
1522
|
+
});
|
|
1523
|
+
});
|
|
1524
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1525
|
+
|
|
1526
|
+
await act(async () => {
|
|
1527
|
+
result.current.thread.composer.setText("queued");
|
|
1528
|
+
result.current.thread.composer.send();
|
|
1529
|
+
});
|
|
1530
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
1531
|
+
|
|
1532
|
+
act(() => {
|
|
1533
|
+
result.current.thread.cancelRun();
|
|
1534
|
+
});
|
|
1535
|
+
await act(async () => {
|
|
1536
|
+
resolveFirstSend();
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
await waitFor(() => {
|
|
1540
|
+
expect(result.current.thread.composer.getState().text).toBe("queued");
|
|
1541
|
+
});
|
|
1542
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1543
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
it("returns the queued send when cancel keeps the trailing message", async () => {
|
|
958
1547
|
let resolveFirstSend!: () => void;
|
|
959
1548
|
const send = vi
|
|
960
1549
|
.fn()
|
|
@@ -1007,16 +1596,14 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
1007
1596
|
|
|
1008
1597
|
it("keeps a cancelled tool approval discarded", async () => {
|
|
1009
1598
|
let resolveFirstSend!: () => void;
|
|
1010
|
-
const send = vi
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
.mockResolvedValue(undefined);
|
|
1019
|
-
const agent = createAgent({ data: approvalData, send });
|
|
1599
|
+
const send = vi.fn().mockImplementationOnce(
|
|
1600
|
+
() =>
|
|
1601
|
+
new Promise<void>((resolve) => {
|
|
1602
|
+
resolveFirstSend = resolve;
|
|
1603
|
+
}),
|
|
1604
|
+
);
|
|
1605
|
+
const respond = vi.fn().mockResolvedValue(undefined);
|
|
1606
|
+
const agent = createAgent({ data: approvalData, send, respond });
|
|
1020
1607
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1021
1608
|
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1022
1609
|
const before = getText(result.current);
|
|
@@ -1043,6 +1630,7 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
1043
1630
|
});
|
|
1044
1631
|
|
|
1045
1632
|
expect(send).toHaveBeenCalledTimes(1);
|
|
1633
|
+
expect(respond).not.toHaveBeenCalled();
|
|
1046
1634
|
expect(result.current.thread.composer.getState().text).toBe("");
|
|
1047
1635
|
expect(getText(result.current)).toEqual(before);
|
|
1048
1636
|
});
|
|
@@ -1142,10 +1730,292 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
1142
1730
|
});
|
|
1143
1731
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(3));
|
|
1144
1732
|
|
|
1145
|
-
expect(send.mock.calls.map(([
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1733
|
+
expect(send.mock.calls.map(([message]) => message)).toEqual([
|
|
1734
|
+
"first staged",
|
|
1735
|
+
"interleaved",
|
|
1736
|
+
"second staged",
|
|
1149
1737
|
]);
|
|
1150
1738
|
});
|
|
1151
1739
|
});
|
|
1740
|
+
|
|
1741
|
+
describe("useEveAgentRuntime cancel binding", () => {
|
|
1742
|
+
it("cancels through the eve 0.38+ cancel binding when the agent provides it", async () => {
|
|
1743
|
+
const cancel = vi.fn().mockResolvedValue({ status: "cancelled" });
|
|
1744
|
+
const agent = createAgent({ status: "streaming", cancel });
|
|
1745
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1746
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1747
|
+
|
|
1748
|
+
await act(async () => {
|
|
1749
|
+
result.current.thread.cancelRun();
|
|
1750
|
+
});
|
|
1751
|
+
|
|
1752
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
1753
|
+
expect(agent.stop).not.toHaveBeenCalled();
|
|
1754
|
+
});
|
|
1755
|
+
|
|
1756
|
+
it("falls back to the pre-0.38 stop binding when cancel is absent", async () => {
|
|
1757
|
+
const agent = createAgent({ status: "streaming" });
|
|
1758
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1759
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1760
|
+
|
|
1761
|
+
await act(async () => {
|
|
1762
|
+
result.current.thread.cancelRun();
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
expect(agent.stop).toHaveBeenCalledTimes(1);
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1768
|
+
it("drops a queued send when the durable cancel settles late", async () => {
|
|
1769
|
+
let resolveFirstSend!: () => void;
|
|
1770
|
+
const send = vi
|
|
1771
|
+
.fn()
|
|
1772
|
+
.mockImplementationOnce(
|
|
1773
|
+
() =>
|
|
1774
|
+
new Promise<void>((resolve) => {
|
|
1775
|
+
resolveFirstSend = resolve;
|
|
1776
|
+
}),
|
|
1777
|
+
)
|
|
1778
|
+
.mockResolvedValue(undefined);
|
|
1779
|
+
let resolveCancel!: () => void;
|
|
1780
|
+
const cancel = vi.fn(
|
|
1781
|
+
() =>
|
|
1782
|
+
new Promise<{ status: string }>((resolve) => {
|
|
1783
|
+
resolveCancel = () => resolve({ status: "cancelled" });
|
|
1784
|
+
}),
|
|
1785
|
+
);
|
|
1786
|
+
const agent = createAgent({ data: settledData, send, cancel });
|
|
1787
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1788
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1789
|
+
|
|
1790
|
+
await act(async () => {
|
|
1791
|
+
result.current.thread.append({
|
|
1792
|
+
role: "user",
|
|
1793
|
+
content: [{ type: "text", text: "first" }],
|
|
1794
|
+
});
|
|
1795
|
+
});
|
|
1796
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1797
|
+
|
|
1798
|
+
await act(async () => {
|
|
1799
|
+
result.current.thread.append({
|
|
1800
|
+
role: "user",
|
|
1801
|
+
content: [{ type: "text", text: "queued" }],
|
|
1802
|
+
});
|
|
1803
|
+
});
|
|
1804
|
+
act(() => {
|
|
1805
|
+
result.current.thread.cancelRun();
|
|
1806
|
+
});
|
|
1807
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
1808
|
+
|
|
1809
|
+
await act(async () => {
|
|
1810
|
+
resolveFirstSend();
|
|
1811
|
+
resolveCancel();
|
|
1812
|
+
});
|
|
1813
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1814
|
+
});
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
describe("useEveAgentRuntime thread refetch", () => {
|
|
1818
|
+
const session = { sessionId: "s1" };
|
|
1819
|
+
|
|
1820
|
+
it("replays the session through eve resume when the thread is refetched", async () => {
|
|
1821
|
+
const resume = vi.fn().mockResolvedValue(undefined);
|
|
1822
|
+
const agent = createAgent({ session, resume });
|
|
1823
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1824
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1825
|
+
expect(result.current.thread.getState().capabilities.refetchThread).toBe(
|
|
1826
|
+
true,
|
|
1827
|
+
);
|
|
1828
|
+
|
|
1829
|
+
await act(async () => {
|
|
1830
|
+
await result.current.threads.reloadMainThread();
|
|
1831
|
+
});
|
|
1832
|
+
|
|
1833
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
it("propagates a failed replay to the refetch caller", async () => {
|
|
1837
|
+
const resume = vi.fn().mockRejectedValue(new Error("replay failed"));
|
|
1838
|
+
const agent = createAgent({ session, resume });
|
|
1839
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1840
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1841
|
+
|
|
1842
|
+
await act(async () => {
|
|
1843
|
+
await expect(result.current.threads.reloadMainThread()).rejects.toThrow(
|
|
1844
|
+
"replay failed",
|
|
1845
|
+
);
|
|
1846
|
+
});
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
it("resolves without replaying when no session exists yet", async () => {
|
|
1850
|
+
const resume = vi.fn();
|
|
1851
|
+
const agent = createAgent({ session: undefined, resume });
|
|
1852
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1853
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1854
|
+
|
|
1855
|
+
await act(async () => {
|
|
1856
|
+
await result.current.threads.reloadMainThread();
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
expect(resume).not.toHaveBeenCalled();
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
it("waits for a turn dispatched before it to park, then replays", async () => {
|
|
1863
|
+
let resolveSend!: () => void;
|
|
1864
|
+
const send = vi.fn(
|
|
1865
|
+
() =>
|
|
1866
|
+
new Promise<void>((resolve) => {
|
|
1867
|
+
resolveSend = resolve;
|
|
1868
|
+
}),
|
|
1869
|
+
);
|
|
1870
|
+
const resume = vi.fn().mockResolvedValue(undefined);
|
|
1871
|
+
const agent = createAgent({ session, resume, send });
|
|
1872
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1873
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1874
|
+
|
|
1875
|
+
await act(async () => {
|
|
1876
|
+
result.current.thread.append({
|
|
1877
|
+
role: "user",
|
|
1878
|
+
content: [{ type: "text", text: "hi" }],
|
|
1879
|
+
});
|
|
1880
|
+
});
|
|
1881
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1882
|
+
|
|
1883
|
+
let settled = false;
|
|
1884
|
+
await act(async () => {
|
|
1885
|
+
void result.current.threads.reloadMainThread().then(() => {
|
|
1886
|
+
settled = true;
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
expect(resume).not.toHaveBeenCalled();
|
|
1890
|
+
expect(settled).toBe(false);
|
|
1891
|
+
|
|
1892
|
+
await act(async () => {
|
|
1893
|
+
resolveSend();
|
|
1894
|
+
});
|
|
1895
|
+
await waitFor(() => expect(settled).toBe(true));
|
|
1896
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
it("reads the session at dispatch time, after the first send created it", async () => {
|
|
1900
|
+
let resolveSend!: () => void;
|
|
1901
|
+
const send = vi.fn(
|
|
1902
|
+
() =>
|
|
1903
|
+
new Promise<void>((resolve) => {
|
|
1904
|
+
resolveSend = resolve;
|
|
1905
|
+
}),
|
|
1906
|
+
);
|
|
1907
|
+
const resume = vi.fn().mockResolvedValue(undefined);
|
|
1908
|
+
const agent = createAgent({ session: undefined, resume, send });
|
|
1909
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1910
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1911
|
+
|
|
1912
|
+
await act(async () => {
|
|
1913
|
+
result.current.thread.append({
|
|
1914
|
+
role: "user",
|
|
1915
|
+
content: [{ type: "text", text: "hi" }],
|
|
1916
|
+
});
|
|
1917
|
+
});
|
|
1918
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1919
|
+
|
|
1920
|
+
let settled = false;
|
|
1921
|
+
await act(async () => {
|
|
1922
|
+
void result.current.threads.reloadMainThread().then(() => {
|
|
1923
|
+
settled = true;
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
mockUseEveAgent.mockReturnValue({ ...agent, session } as never);
|
|
1928
|
+
rerender();
|
|
1929
|
+
await act(async () => {
|
|
1930
|
+
resolveSend();
|
|
1931
|
+
});
|
|
1932
|
+
await waitFor(() => expect(settled).toBe(true));
|
|
1933
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
it("still replays a queued refetch when the run it waited on is cancelled", async () => {
|
|
1937
|
+
let resolveSend!: () => void;
|
|
1938
|
+
const send = vi.fn(
|
|
1939
|
+
() =>
|
|
1940
|
+
new Promise<void>((resolve) => {
|
|
1941
|
+
resolveSend = resolve;
|
|
1942
|
+
}),
|
|
1943
|
+
);
|
|
1944
|
+
const resume = vi.fn().mockResolvedValue(undefined);
|
|
1945
|
+
const cancel = vi.fn().mockResolvedValue({ status: "cancelled" });
|
|
1946
|
+
const agent = createAgent({ session, resume, send, cancel });
|
|
1947
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1948
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1949
|
+
|
|
1950
|
+
await act(async () => {
|
|
1951
|
+
result.current.thread.append({
|
|
1952
|
+
role: "user",
|
|
1953
|
+
content: [{ type: "text", text: "hi" }],
|
|
1954
|
+
});
|
|
1955
|
+
});
|
|
1956
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1957
|
+
|
|
1958
|
+
let settled = false;
|
|
1959
|
+
await act(async () => {
|
|
1960
|
+
void result.current.threads.reloadMainThread().then(() => {
|
|
1961
|
+
settled = true;
|
|
1962
|
+
});
|
|
1963
|
+
result.current.thread.cancelRun();
|
|
1964
|
+
});
|
|
1965
|
+
await act(async () => {
|
|
1966
|
+
resolveSend();
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
await waitFor(() => expect(settled).toBe(true));
|
|
1970
|
+
expect(resume).toHaveBeenCalledTimes(1);
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
it("drops a queued refetch when the session is reset before it dispatches", async () => {
|
|
1974
|
+
let resolveSend!: () => void;
|
|
1975
|
+
const send = vi.fn(
|
|
1976
|
+
() =>
|
|
1977
|
+
new Promise<void>((resolve) => {
|
|
1978
|
+
resolveSend = resolve;
|
|
1979
|
+
}),
|
|
1980
|
+
);
|
|
1981
|
+
const resume = vi.fn();
|
|
1982
|
+
const agent = createAgent({ session, resume, send });
|
|
1983
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1984
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1985
|
+
|
|
1986
|
+
await act(async () => {
|
|
1987
|
+
result.current.thread.append({
|
|
1988
|
+
role: "user",
|
|
1989
|
+
content: [{ type: "text", text: "hi" }],
|
|
1990
|
+
});
|
|
1991
|
+
});
|
|
1992
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1993
|
+
|
|
1994
|
+
let settled = false;
|
|
1995
|
+
await act(async () => {
|
|
1996
|
+
void result.current.threads.reloadMainThread().then(() => {
|
|
1997
|
+
settled = true;
|
|
1998
|
+
});
|
|
1999
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
2000
|
+
});
|
|
2001
|
+
await act(async () => {
|
|
2002
|
+
resolveSend();
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
await waitFor(() => expect(settled).toBe(true));
|
|
2006
|
+
expect(resume).not.toHaveBeenCalled();
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
it("leaves the capability absent when the installed eve has no resume", async () => {
|
|
2010
|
+
const agent = createAgent({ session });
|
|
2011
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
2012
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
2013
|
+
|
|
2014
|
+
expect(result.current.thread.getState().capabilities.refetchThread).toBe(
|
|
2015
|
+
false,
|
|
2016
|
+
);
|
|
2017
|
+
await act(async () => {
|
|
2018
|
+
await result.current.threads.reloadMainThread();
|
|
2019
|
+
});
|
|
2020
|
+
});
|
|
2021
|
+
});
|