@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.
@@ -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. upsertMessage clears it when the
289
- // tool's `${executionId}:${toolCallId}` key is inflight/resolved.
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
- // Stale re-emit flips awaiting back to true on the wire.
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"));