@runtypelabs/persona 3.29.0 → 3.29.1
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/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CxvHw0X6.d.cts → types-B_Qazlm4.d.cts} +6 -0
- package/dist/animations/{types-CxvHw0X6.d.ts → types-B_Qazlm4.d.ts} +6 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +39 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.global.js +47 -47
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +39 -39
- package/dist/index.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +6 -0
- package/dist/smart-dom-reader.d.ts +6 -0
- package/dist/theme-editor.cjs +30 -30
- package/dist/theme-editor.d.cts +6 -0
- package/dist/theme-editor.d.ts +6 -0
- package/dist/theme-editor.js +30 -30
- package/package.json +1 -1
- package/src/client.test.ts +40 -0
- package/src/client.ts +18 -4
- package/src/generated/runtype-openapi-contract.ts +12 -0
- package/src/session.ts +190 -29
- package/src/session.webmcp.test.ts +326 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { AgentWidgetSession } from "./session";
|
|
4
4
|
import type {
|
|
@@ -80,6 +80,10 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
80
80
|
vi.clearAllMocks();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
});
|
|
86
|
+
|
|
83
87
|
it("posts result to /resume on the happy path", async () => {
|
|
84
88
|
const { session, executeSpy, resumeSpy } = makeSession({
|
|
85
89
|
executeReturn: {
|
|
@@ -97,6 +101,33 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
97
101
|
);
|
|
98
102
|
});
|
|
99
103
|
|
|
104
|
+
it("records the elapsed time of the resolved browser WebMCP promise", async () => {
|
|
105
|
+
vi.useFakeTimers();
|
|
106
|
+
vi.setSystemTime(1_000);
|
|
107
|
+
const { session } = makeSession({
|
|
108
|
+
executeImpl: async () => {
|
|
109
|
+
vi.advanceTimersByTime(1_250);
|
|
110
|
+
return { content: [{ type: "text", text: "loaded jade" }] };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await session.resolveWebMcpToolCall(
|
|
115
|
+
awaitingMessage("tool-1", "webmcp:get_product_by_url"),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const stored = (
|
|
119
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
120
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
121
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
122
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
123
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
124
|
+
expect(stored?.toolCall?.completedAt).toBe(2_250);
|
|
125
|
+
expect(stored?.toolCall?.durationMs).toBe(1_250);
|
|
126
|
+
expect(stored?.toolCall?.result).toEqual({
|
|
127
|
+
content: [{ type: "text", text: "loaded jade" }],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
100
131
|
it("still resumes (with isError) when the bridge is not operational", async () => {
|
|
101
132
|
// BugBot finding #1: previously, handleEvent skipped resolveWebMcpToolCall
|
|
102
133
|
// entirely when `isWebMcpOperational()` was false — leaving the dispatch
|
|
@@ -152,6 +183,13 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
152
183
|
|
|
153
184
|
const msg = awaitingMessage("tool-1", "webmcp:search");
|
|
154
185
|
await session.resolveWebMcpToolCall(msg);
|
|
186
|
+
const afterFailedResume = (
|
|
187
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
188
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
189
|
+
expect(afterFailedResume?.toolCall?.status).toBe("running");
|
|
190
|
+
expect(afterFailedResume?.toolCall?.result).toBeUndefined();
|
|
191
|
+
expect(afterFailedResume?.toolCall?.durationMs).toBeUndefined();
|
|
192
|
+
|
|
155
193
|
await session.resolveWebMcpToolCall(msg); // retry — must be allowed
|
|
156
194
|
await session.resolveWebMcpToolCall(msg); // post-success — must be blocked
|
|
157
195
|
|
|
@@ -159,6 +197,29 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
159
197
|
expect(resumeSpy).toHaveBeenCalledTimes(2);
|
|
160
198
|
});
|
|
161
199
|
|
|
200
|
+
it("marks the bubble complete with an error when browser execution throws before /resume", async () => {
|
|
201
|
+
const { session, resumeSpy } = makeSession({
|
|
202
|
+
executeImpl: async () => {
|
|
203
|
+
throw new Error("page tool exploded");
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await session.resolveWebMcpToolCall(
|
|
208
|
+
awaitingMessage("tool-1", "webmcp:search"),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const stored = (
|
|
212
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
213
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
214
|
+
expect(resumeSpy).not.toHaveBeenCalled();
|
|
215
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
216
|
+
expect(stored?.toolCall?.durationMs).toBeGreaterThanOrEqual(0);
|
|
217
|
+
expect(stored?.toolCall?.result).toMatchObject({
|
|
218
|
+
isError: true,
|
|
219
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
162
223
|
it("threads an AbortSignal into resumeFlow", async () => {
|
|
163
224
|
// BugBot finding #6: cancel() needs to propagate into /resume.
|
|
164
225
|
const { session, resumeSpy } = makeSession();
|
|
@@ -189,6 +250,14 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
189
250
|
release();
|
|
190
251
|
await inflight;
|
|
191
252
|
expect(resumeSpy).not.toHaveBeenCalled();
|
|
253
|
+
const stored = (
|
|
254
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
255
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
256
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
257
|
+
expect(stored?.toolCall?.result).toMatchObject({
|
|
258
|
+
isError: true,
|
|
259
|
+
content: [{ type: "text", text: "Aborted by cancel()" }],
|
|
260
|
+
});
|
|
192
261
|
});
|
|
193
262
|
|
|
194
263
|
it("does NOT abort the shared session abortController", async () => {
|
|
@@ -282,11 +351,12 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
282
351
|
expect(executeSpy).not.toHaveBeenCalled();
|
|
283
352
|
});
|
|
284
353
|
|
|
285
|
-
it("a stale step_await re-emit does not resurrect awaitingLocalTool once resolved", () => {
|
|
354
|
+
it("a stale step_await re-emit does not resurrect awaitingLocalTool or reset completed tool state once resolved", () => {
|
|
286
355
|
// BugBot: a duplicate step_await (awaitingLocalTool:true) for an
|
|
287
356
|
// already-resolved webmcp tool must not flip the message back to awaiting
|
|
288
|
-
// and show a stuck local-tool wait
|
|
289
|
-
// tool's `${executionId}:${toolCallId}`
|
|
357
|
+
// or running and show a stuck local-tool wait / lose the measured duration.
|
|
358
|
+
// upsertMessage clears it when the tool's `${executionId}:${toolCallId}`
|
|
359
|
+
// key is inflight/resolved.
|
|
290
360
|
const session = makeSession().session;
|
|
291
361
|
const s = session as unknown as {
|
|
292
362
|
webMcpResolvedKeys: Set<string>;
|
|
@@ -298,11 +368,182 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
298
368
|
s.upsertMessage({
|
|
299
369
|
...awaitingMessage("tool-1", "webmcp:search"),
|
|
300
370
|
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
371
|
+
streaming: false,
|
|
372
|
+
toolCall: {
|
|
373
|
+
id: "tool-1",
|
|
374
|
+
name: "webmcp:search",
|
|
375
|
+
status: "complete",
|
|
376
|
+
args: { q: "shoes" },
|
|
377
|
+
result: { content: [{ type: "text", text: "ok" }] },
|
|
378
|
+
startedAt: 1_000,
|
|
379
|
+
completedAt: 2_200,
|
|
380
|
+
durationMs: 1_200,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
// Stale re-emit flips awaiting back to true and carries the running state
|
|
384
|
+
// emitted by client.ts for WebMCP step_await events.
|
|
385
|
+
s.upsertMessage({
|
|
386
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
387
|
+
streaming: false,
|
|
388
|
+
toolCall: {
|
|
389
|
+
id: "tool-1",
|
|
390
|
+
name: "webmcp:search",
|
|
391
|
+
status: "running",
|
|
392
|
+
args: { q: "shoes" },
|
|
393
|
+
startedAt: 3_000,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const stored = s.messages.find((m) => m.toolCall?.id === "tool-1");
|
|
397
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
398
|
+
expect(stored?.streaming).toBe(false);
|
|
399
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
400
|
+
expect(stored?.toolCall?.result).toEqual({
|
|
401
|
+
content: [{ type: "text", text: "ok" }],
|
|
402
|
+
});
|
|
403
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
404
|
+
expect(stored?.toolCall?.completedAt).toBe(2_200);
|
|
405
|
+
expect(stored?.toolCall?.durationMs).toBe(1_200);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("a stale step_await re-emit does not reset in-flight WebMCP tool timing", () => {
|
|
409
|
+
const session = makeSession().session;
|
|
410
|
+
const s = session as unknown as {
|
|
411
|
+
webMcpInflightKeys: Set<string>;
|
|
412
|
+
upsertMessage: (m: AgentWidgetMessage) => void;
|
|
413
|
+
messages: AgentWidgetMessage[];
|
|
414
|
+
};
|
|
415
|
+
s.webMcpInflightKeys.add("exec-1:tool-1");
|
|
416
|
+
s.upsertMessage({
|
|
417
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
418
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
419
|
+
streaming: true,
|
|
420
|
+
toolCall: {
|
|
421
|
+
id: "tool-1",
|
|
422
|
+
name: "webmcp:search",
|
|
423
|
+
status: "running",
|
|
424
|
+
args: { q: "shoes" },
|
|
425
|
+
startedAt: 1_000,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
s.upsertMessage({
|
|
430
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
431
|
+
streaming: false,
|
|
432
|
+
toolCall: {
|
|
433
|
+
id: "tool-1",
|
|
434
|
+
name: "webmcp:search",
|
|
435
|
+
status: "running",
|
|
436
|
+
args: { q: "shoes" },
|
|
437
|
+
startedAt: 3_000,
|
|
438
|
+
},
|
|
301
439
|
});
|
|
302
|
-
|
|
303
|
-
s.upsertMessage(awaitingMessage("tool-1", "webmcp:search"));
|
|
440
|
+
|
|
304
441
|
const stored = s.messages.find((m) => m.toolCall?.id === "tool-1");
|
|
305
442
|
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
443
|
+
expect(stored?.streaming).toBe(true);
|
|
444
|
+
expect(stored?.toolCall?.status).toBe("running");
|
|
445
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
446
|
+
expect(stored?.toolCall?.durationMs).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("a stale step_await re-emit does not reset completed WebMCP local-error state", () => {
|
|
450
|
+
const session = makeSession().session;
|
|
451
|
+
const s = session as unknown as {
|
|
452
|
+
upsertMessage: (m: AgentWidgetMessage) => void;
|
|
453
|
+
messages: AgentWidgetMessage[];
|
|
454
|
+
};
|
|
455
|
+
s.upsertMessage({
|
|
456
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
457
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
458
|
+
streaming: false,
|
|
459
|
+
toolCall: {
|
|
460
|
+
id: "tool-1",
|
|
461
|
+
name: "webmcp:search",
|
|
462
|
+
status: "complete",
|
|
463
|
+
args: { q: "shoes" },
|
|
464
|
+
result: {
|
|
465
|
+
isError: true,
|
|
466
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
467
|
+
},
|
|
468
|
+
startedAt: 1_000,
|
|
469
|
+
completedAt: 1_500,
|
|
470
|
+
durationMs: 500,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
s.upsertMessage({
|
|
475
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
476
|
+
streaming: false,
|
|
477
|
+
toolCall: {
|
|
478
|
+
id: "tool-1",
|
|
479
|
+
name: "webmcp:search",
|
|
480
|
+
status: "running",
|
|
481
|
+
args: { q: "shoes" },
|
|
482
|
+
startedAt: 3_000,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const stored = s.messages.find((m) => m.toolCall?.id === "tool-1");
|
|
487
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
488
|
+
expect(stored?.streaming).toBe(false);
|
|
489
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
490
|
+
expect(stored?.toolCall?.result).toEqual({
|
|
491
|
+
isError: true,
|
|
492
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
493
|
+
});
|
|
494
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
495
|
+
expect(stored?.toolCall?.completedAt).toBe(1_500);
|
|
496
|
+
expect(stored?.toolCall?.durationMs).toBe(500);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("a new execution reusing a completed tool id is not treated as stale", () => {
|
|
500
|
+
const session = makeSession().session;
|
|
501
|
+
const s = session as unknown as {
|
|
502
|
+
upsertMessage: (m: AgentWidgetMessage) => void;
|
|
503
|
+
messages: AgentWidgetMessage[];
|
|
504
|
+
};
|
|
505
|
+
s.upsertMessage({
|
|
506
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
507
|
+
id: "tool-tool-1",
|
|
508
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
509
|
+
streaming: false,
|
|
510
|
+
toolCall: {
|
|
511
|
+
id: "tool-1",
|
|
512
|
+
name: "webmcp:search",
|
|
513
|
+
status: "complete",
|
|
514
|
+
args: { q: "shoes" },
|
|
515
|
+
result: {
|
|
516
|
+
isError: true,
|
|
517
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
518
|
+
},
|
|
519
|
+
startedAt: 1_000,
|
|
520
|
+
completedAt: 1_500,
|
|
521
|
+
durationMs: 500,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
s.upsertMessage({
|
|
526
|
+
...awaitingMessage("tool-1", "webmcp:search", "exec-2"),
|
|
527
|
+
id: "tool-tool-1",
|
|
528
|
+
streaming: false,
|
|
529
|
+
toolCall: {
|
|
530
|
+
id: "tool-1",
|
|
531
|
+
name: "webmcp:search",
|
|
532
|
+
status: "running",
|
|
533
|
+
args: { q: "boots" },
|
|
534
|
+
startedAt: 3_000,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const stored = s.messages.find((m) => m.id === "tool-tool-1");
|
|
539
|
+
expect(stored?.agentMetadata?.executionId).toBe("exec-2");
|
|
540
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(true);
|
|
541
|
+
expect(stored?.toolCall?.status).toBe("running");
|
|
542
|
+
expect(stored?.toolCall?.args).toEqual({ q: "boots" });
|
|
543
|
+
expect(stored?.toolCall?.startedAt).toBe(3_000);
|
|
544
|
+
expect(stored?.toolCall?.result).toBeUndefined();
|
|
545
|
+
expect(stored?.toolCall?.completedAt).toBeUndefined();
|
|
546
|
+
expect(stored?.toolCall?.durationMs).toBeUndefined();
|
|
306
547
|
});
|
|
307
548
|
|
|
308
549
|
it("an error event does not clear streaming while a webmcp resolve is in flight", () => {
|
|
@@ -576,6 +817,10 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
576
817
|
vi.clearAllMocks();
|
|
577
818
|
});
|
|
578
819
|
|
|
820
|
+
afterEach(() => {
|
|
821
|
+
vi.useRealTimers();
|
|
822
|
+
});
|
|
823
|
+
|
|
579
824
|
// A `step_await(local_tool_required)` message as client.ts emits it for a
|
|
580
825
|
// PARALLEL local-tool call: the per-call `toolCallId` is both the toolCall.id
|
|
581
826
|
// AND `agentMetadata.webMcpToolCallId`. Two of these for one executionId share
|
|
@@ -705,6 +950,81 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
705
950
|
expect(Object.keys(toolOutputs).sort()).toEqual(["toolu_A", "toolu_B"]);
|
|
706
951
|
});
|
|
707
952
|
|
|
953
|
+
it("records elapsed time for each resolved tool in a batched WebMCP resume", async () => {
|
|
954
|
+
vi.useFakeTimers();
|
|
955
|
+
vi.setSystemTime(1_000);
|
|
956
|
+
|
|
957
|
+
let releaseA: (r: WebMcpToolResult) => void = () => undefined;
|
|
958
|
+
let releaseB: (r: WebMcpToolResult) => void = () => undefined;
|
|
959
|
+
const pA = new Promise<WebMcpToolResult>((r) => (releaseA = r));
|
|
960
|
+
const pB = new Promise<WebMcpToolResult>((r) => (releaseB = r));
|
|
961
|
+
const { session, resumeSpy } = makeSession();
|
|
962
|
+
const client = (session as unknown as { client: Record<string, unknown> })
|
|
963
|
+
.client;
|
|
964
|
+
(client.executeWebMcpToolCall as ReturnType<typeof vi.fn>).mockImplementation(
|
|
965
|
+
(_name: string, args: { sku: string }) =>
|
|
966
|
+
args.sku === "SHOE-001" ? pA : pB,
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
970
|
+
feed(session, parallelAwait("toolu_B", "SHOE-007"));
|
|
971
|
+
endStream(session);
|
|
972
|
+
await flushMicrotasks();
|
|
973
|
+
|
|
974
|
+
vi.setSystemTime(1_800);
|
|
975
|
+
releaseA({ content: [{ type: "text", text: "a" }] });
|
|
976
|
+
await flushMicrotasks();
|
|
977
|
+
|
|
978
|
+
let messages = (session as unknown as { messages: AgentWidgetMessage[] })
|
|
979
|
+
.messages;
|
|
980
|
+
const toolA = messages.find((m) => m.toolCall?.id === "toolu_A");
|
|
981
|
+
const toolBWhileRunning = messages.find((m) => m.toolCall?.id === "toolu_B");
|
|
982
|
+
expect(toolA?.toolCall?.status).toBe("running");
|
|
983
|
+
expect(toolA?.toolCall?.durationMs).toBeUndefined();
|
|
984
|
+
expect(toolBWhileRunning?.toolCall?.status).toBe("running");
|
|
985
|
+
expect(resumeSpy).not.toHaveBeenCalled();
|
|
986
|
+
|
|
987
|
+
vi.setSystemTime(2_600);
|
|
988
|
+
releaseB({ content: [{ type: "text", text: "b" }] });
|
|
989
|
+
await flushMicrotasks();
|
|
990
|
+
|
|
991
|
+
messages = (session as unknown as { messages: AgentWidgetMessage[] })
|
|
992
|
+
.messages;
|
|
993
|
+
const completedToolA = messages.find((m) => m.toolCall?.id === "toolu_A");
|
|
994
|
+
const toolB = messages.find((m) => m.toolCall?.id === "toolu_B");
|
|
995
|
+
expect(completedToolA?.toolCall?.status).toBe("complete");
|
|
996
|
+
expect(completedToolA?.toolCall?.durationMs).toBe(800);
|
|
997
|
+
expect(toolB?.toolCall?.status).toBe("complete");
|
|
998
|
+
expect(toolB?.toolCall?.durationMs).toBe(1_600);
|
|
999
|
+
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("does not mark batched WebMCP bubbles complete when the shared /resume fails", async () => {
|
|
1003
|
+
const { session, client, resumeSpy } = makeSession();
|
|
1004
|
+
(client.resumeFlow as ReturnType<typeof vi.fn>).mockImplementationOnce(
|
|
1005
|
+
async () => {
|
|
1006
|
+
throw new Error("resume failed");
|
|
1007
|
+
},
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
1011
|
+
feed(session, parallelAwait("toolu_B", "SHOE-007"));
|
|
1012
|
+
endStream(session);
|
|
1013
|
+
await flushMicrotasks();
|
|
1014
|
+
|
|
1015
|
+
const messages = (session as unknown as { messages: AgentWidgetMessage[] })
|
|
1016
|
+
.messages;
|
|
1017
|
+
const toolA = messages.find((m) => m.toolCall?.id === "toolu_A");
|
|
1018
|
+
const toolB = messages.find((m) => m.toolCall?.id === "toolu_B");
|
|
1019
|
+
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
|
1020
|
+
expect(toolA?.toolCall?.status).toBe("running");
|
|
1021
|
+
expect(toolA?.toolCall?.result).toBeUndefined();
|
|
1022
|
+
expect(toolA?.toolCall?.durationMs).toBeUndefined();
|
|
1023
|
+
expect(toolB?.toolCall?.status).toBe("running");
|
|
1024
|
+
expect(toolB?.toolCall?.result).toBeUndefined();
|
|
1025
|
+
expect(toolB?.toolCall?.durationMs).toBeUndefined();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
708
1028
|
it("dedupes a duplicate parallel await within the same batch", async () => {
|
|
709
1029
|
const { session, executeSpy, resumeSpy } = makeSession();
|
|
710
1030
|
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|