@openchambery/web 1.18.2-beta.15 → 1.18.2-beta.17

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.
Files changed (57) hide show
  1. package/dist/assets/{AssistantView-qRCdLcZN.js → AssistantView-DFHmpZ7G.js} +1 -1
  2. package/dist/assets/{ChatContainer-CRWQAE6E.js → ChatContainer-D7SxgV8T.js} +4 -4
  3. package/dist/assets/{DiagramView-B34t9o0C.js → DiagramView-DmHeDjDE.js} +1 -1
  4. package/dist/assets/{MarkdownRendererImpl-CvpH7Y_l.js → MarkdownRendererImpl-CR70vDmk.js} +1 -1
  5. package/dist/assets/{MobileDetailNavigation-CiZtnbwn.js → MobileDetailNavigation-CosmubCm.js} +1 -1
  6. package/dist/assets/{MobileShareBridge-DwysiJW7.js → MobileShareBridge-DzP5vJAv.js} +1 -1
  7. package/dist/assets/{MobileSurface-D1GUXnAt.js → MobileSurface-bpfoNOdp.js} +1 -1
  8. package/dist/assets/{MultiRunWindow-CDENQDyB.js → MultiRunWindow-Bj35xImj.js} +1 -1
  9. package/dist/assets/{OnboardingScreen-7mFH7iBm.js → OnboardingScreen-CYeRZznl.js} +1 -1
  10. package/dist/assets/{OverlayScrollbar-CmhitwHL.js → OverlayScrollbar-C0r95yEi.js} +1 -1
  11. package/dist/assets/{SettingsView-Dv566NkF.js → SettingsView-DlUCG6xP.js} +1 -1
  12. package/dist/assets/{SettingsWindow-CbVwm2T6.js → SettingsWindow-Bx6yUy4t.js} +1 -1
  13. package/dist/assets/{TerminalView-BfTCj_PO.js → TerminalView-A6lT_pyN.js} +1 -1
  14. package/dist/assets/{ToolOutputDialog-CdAO6ADz.js → ToolOutputDialog-74Ec3sow.js} +1 -1
  15. package/dist/assets/{appThemeRegistry-Cvo1QkKA.js → appThemeRegistry-C2czYOtx.js} +2 -2
  16. package/dist/assets/{clipboard-Cm4VHQ3n.js → clipboard-BnElHSnO.js} +1 -1
  17. package/dist/assets/{context-menu-C7P3at3V.js → context-menu-B87qIDjC.js} +1 -1
  18. package/dist/assets/{dialog-ruyemRYh.js → dialog-COrLBm9O.js} +1 -1
  19. package/dist/assets/{es-W_z2kSYf.js → es-DIVYdoxi.js} +1 -1
  20. package/dist/assets/{fr-MYuf2QNQ.js → fr-CNTlVypq.js} +1 -1
  21. package/dist/assets/{gitApi-GkEdeq0D.js → gitApi-CAYz6qHu.js} +1 -1
  22. package/dist/assets/{insertionBoundaries-CDxRoLe7.js → insertionBoundaries-iAOuvfny.js} +1 -1
  23. package/dist/assets/{ja-DBkl4NXl.js → ja-C_eW5ZM8.js} +1 -1
  24. package/dist/assets/{ko-DwSsHsxa.js → ko-CE3cOv7v.js} +1 -1
  25. package/dist/assets/{main-B-Pz3EI_.js → main-BQGhNyeU.js} +4 -4
  26. package/dist/assets/{main-CKV54Ayt.js → main-Cu7sZyJS.js} +2 -2
  27. package/dist/assets/{miniChat-D8VtCIh9.js → miniChat-CwrVreq-.js} +2 -2
  28. package/dist/assets/{mobile-B8jXmmPT.js → mobile-SQEAwCJ8.js} +2 -2
  29. package/dist/assets/{multirun-CQorhgZD.js → multirun-CJ3fED_0.js} +1 -1
  30. package/dist/assets/{pl-BGLpnSSE.js → pl-BZKZOE99.js} +1 -1
  31. package/dist/assets/{projectMeta-D-t3tPnX.js → projectMeta-BM3a0rwT.js} +1 -1
  32. package/dist/assets/{pt-BR-DuEh49lp.js → pt-BR-DHDgp8S3.js} +1 -1
  33. package/dist/assets/{radio-BIl2YIoa.js → radio-DfR3It8I.js} +1 -1
  34. package/dist/assets/{renderElectronMiniChatApp-Dn0BTQ2T.js → renderElectronMiniChatApp-b3VyI_V2.js} +2 -2
  35. package/dist/assets/renderMobileApp-DrU0se98.js +23 -0
  36. package/dist/assets/{runtimeEndpointReset-B1IMFiKd.js → runtimeEndpointReset-3gL_biDm.js} +1 -1
  37. package/dist/assets/{sortable-tabs-strip-QDIkcsUA.js → sortable-tabs-strip-D0fuWvzp.js} +1 -1
  38. package/dist/assets/src-DcPZ_rWS.css +2 -0
  39. package/dist/assets/{store-Cb8Q6EtD.js → store-CUWqzRt7.js} +2 -2
  40. package/dist/assets/{uk-CUoSz-Q8.js → uk-CfQK6cz1.js} +1 -1
  41. package/dist/assets/{useAppFontEffects-BwmqFEKp.js → useAppFontEffects-AWfqdYZm.js} +2 -2
  42. package/dist/assets/{useDesktopWindowControlsLayout-B5LGS8S5.js → useDesktopWindowControlsLayout-BgCS8KRJ.js} +1 -1
  43. package/dist/assets/{useEffectiveDirectory-BgIUpNz0.js → useEffectiveDirectory-ktSK1ADz.js} +1 -1
  44. package/dist/assets/{useMobileNavigationStore-DIRiAvR1.js → useMobileNavigationStore-Dy5a1iTA.js} +4 -4
  45. package/dist/assets/{useSessionAutoCleanup-Ypc623bG.js → useSessionAutoCleanup-BRYDBUlq.js} +1 -1
  46. package/dist/assets/{useWorkerHighlightedLines-BxaH8sAx.js → useWorkerHighlightedLines-RexsXGPu.js} +1 -1
  47. package/dist/assets/{zh-CN-CJP5FWCK.js → zh-CN-DvY2fKWp.js} +1 -1
  48. package/dist/assets/{zh-TW-B_2h1yuo.js → zh-TW-eWe0lm3-.js} +1 -1
  49. package/dist/index.html +2 -2
  50. package/dist/mini-chat.html +2 -2
  51. package/dist/mobile.html +2 -2
  52. package/package.json +1 -1
  53. package/server/lib/session-title/DOCUMENTATION.md +16 -12
  54. package/server/lib/session-title/runtime.js +7 -8
  55. package/server/lib/session-title/runtime.test.js +225 -0
  56. package/dist/assets/renderMobileApp-BE9NXDDC.js +0 -23
  57. package/dist/assets/src-C8jn0x69.css +0 -2
package/dist/index.html CHANGED
@@ -552,7 +552,7 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-CKV54Ayt.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-Cu7sZyJS.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
@@ -568,7 +568,7 @@
568
568
  <link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
569
569
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
570
570
  <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
571
- <link rel="stylesheet" crossorigin href="/assets/src-C8jn0x69.css">
571
+ <link rel="stylesheet" crossorigin href="/assets/src-DcPZ_rWS.css">
572
572
  </head>
573
573
  <body class="h-full bg-background text-foreground">
574
574
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -74,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-D8VtCIh9.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-CwrVreq-.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
@@ -90,7 +90,7 @@
90
90
  <link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
91
91
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
92
92
  <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
93
- <link rel="stylesheet" crossorigin href="/assets/src-C8jn0x69.css">
93
+ <link rel="stylesheet" crossorigin href="/assets/src-DcPZ_rWS.css">
94
94
  </head>
95
95
  <body class="h-full bg-background text-foreground">
96
96
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,7 +63,7 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-B8jXmmPT.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-SQEAwCJ8.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/vendor-base-ui-CnHfUmDN.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/vendor-zustand-B4dlGEM2.js">
@@ -79,7 +79,7 @@
79
79
  <link rel="modulepreload" crossorigin href="/assets/gitApiHttp-BxGAFLTf.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-DpiAJlil.js">
81
81
  <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BXCt7ZsQ.js">
82
- <link rel="stylesheet" crossorigin href="/assets/src-C8jn0x69.css">
82
+ <link rel="stylesheet" crossorigin href="/assets/src-DcPZ_rWS.css">
83
83
  </head>
84
84
  <body class="h-full bg-background text-foreground">
85
85
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "1.18.2-beta.15",
3
+ "version": "1.18.2-beta.17",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -5,10 +5,11 @@ conversation's **main subject** (overall feature / goal) with the small model
5
5
  (`lib/small-model`), then PATCHes `title` plus `metadata.openchamber.titleRefresh`.
6
6
 
7
7
  OpenCode only auto-titles once from the first user message
8
- (`SessionPrompt.ensureTitle`). Long sessions often need a refresh; this module
9
- keeps the sidebar title aligned with the durable work being done not the last
10
- wrap-up utterance like "commit and push"throttled to at most once per 5
11
- minutes per session.
8
+ (`SessionPrompt.ensureTitle`). This module auto-refreshes sparsely (first idle
9
+ of a new session, first newly-sent reply on a fork) and on explicit smart-title
10
+ requests, naming the durable work being done not the last wrap-up utterance
11
+ like "commit and push". Background auto refreshes still respect a 5-minute
12
+ throttle (`TITLE_THROTTLE_MS`) when a refresh is armed.
12
13
 
13
14
  ## Flow
14
15
 
@@ -16,14 +17,17 @@ minutes per session.
16
17
  fan-out (`index.js` → `globalMessageStreamHub.subscribeEvent`), same
17
18
  pattern as session-assist. Purely event-driven — dormant sessions never
18
19
  generate anything.
19
- 2. A newly observed root `session.created` generates its first title immediately
20
- on the first `session.status: idle`. A fork title (`(fork #n)`) waits for its
21
- first newly-created user message; the matching assistant completion triggers
22
- an immediate title refresh that bypasses inherited title metadata and throttle.
23
- Later idle transitions arm a 15-second quiet timer; any `busy`/`retry` status
24
- or a fresh user `message.updated` clears it.
25
- A sidebar smart-title request sets `titleRefresh.requestedAt`; its
26
- `session.updated` event arms the same flow immediately.
20
+ 2. Auto title refresh is intentionally sparse (title stability first). A newly
21
+ observed root `session.created` generates its first title immediately on the
22
+ first `session.status: idle`. A fork title (`(fork #n)`) waits for its first
23
+ newly-created user message; the matching assistant completion triggers an
24
+ immediate title refresh that bypasses inherited title metadata and throttle.
25
+ Ordinary later idle transitions do **not** arm another refresh. Any
26
+ `busy`/`retry` status or a fresh user `message.updated` still clears an
27
+ already-armed timer (so initial/fork timers cancel if the user keeps going).
28
+ A sidebar smart-title request sets `titleRefresh.requestedAt`; its
29
+ `session.updated` event arms the same flow immediately (forced / manual
30
+ refresh is unaffected by the background gate).
27
31
  3. On fire:
28
32
  - Skip when Settings → Chat → session title refresh is off.
29
33
  - Skip system-owned sessions whose metadata carries a non-empty
@@ -1,13 +1,14 @@
1
- // Session title refresh: after a session goes idle, regenerate the sidebar
2
- // title from the conversation's MAIN SUBJECT (overall feature / goal), not
3
- // just the last wrap-up utterance. OpenCode only titles once from the first
4
- // user message; long sessions need refresh, but continuing the same work
1
+ // Session title refresh: regenerate the sidebar title from the conversation's
2
+ // MAIN SUBJECT (overall feature / goal), not just the last wrap-up utterance.
3
+ // OpenCode only titles once from the first user message. Auto refresh is sparse
4
+ // for title stability: first idle of a new session, first newly-sent reply on a
5
+ // fork, plus explicit smart-title / forced refresh. Continuing the same work
5
6
  // should keep naming the thing being done — not "commit and push". This
6
7
  // watcher throttles to at most one refresh per session every TITLE_THROTTLE_MS
7
8
  // and never overwrites a user-renamed title.
8
9
  //
9
- // Purely event-driven: only sessions that transition busy→idle while the
10
- // server is running ever generate anything. No backfill, no session scans.
10
+ // Purely event-driven: only sessions that arm a refresh while the server is
11
+ // running ever generate anything. No backfill, no session scans.
11
12
 
12
13
  import fs from 'fs';
13
14
  import os from 'os';
@@ -742,8 +743,6 @@ export const createSessionTitleRuntime = ({
742
743
  if (newSessions.delete(status.sessionId)) {
743
744
  initialRefreshes.add(status.sessionId);
744
745
  armTimer(status.sessionId, status.directory || directoryHint, 0);
745
- } else {
746
- armTimer(status.sessionId, status.directory || directoryHint, quietMs);
747
746
  }
748
747
  } else {
749
748
  clearTimer(status.sessionId);
@@ -403,4 +403,229 @@ describe('session-title helpers', () => {
403
403
  globalThis.fetch = originalFetch;
404
404
  }
405
405
  });
406
+
407
+ it('refreshes a new session on first idle, then ignores later idle transitions', async () => {
408
+ const originalFetch = globalThis.fetch;
409
+ const session = {
410
+ id: 'ses-new',
411
+ title: 'New session - 2026-07-10T12:00:00.000Z',
412
+ metadata: {},
413
+ };
414
+ const messages = [
415
+ { info: { id: 'u1', role: 'user' }, parts: [{ type: 'text', text: 'Add rate limiting' }] },
416
+ { info: { id: 'a1', role: 'assistant' }, parts: [{ type: 'text', text: 'Implemented rate limit' }] },
417
+ ];
418
+ let generationCalls = 0;
419
+ let patchedTitle = session.title;
420
+
421
+ globalThis.fetch = async (input, init = {}) => {
422
+ const url = String(input);
423
+ if (url.includes('/message')) {
424
+ return new Response(JSON.stringify(messages), { status: 200 });
425
+ }
426
+ if (init.method === 'PATCH') {
427
+ const body = typeof init.body === 'string' ? JSON.parse(init.body) : init.body;
428
+ if (typeof body?.title === 'string') {
429
+ patchedTitle = body.title;
430
+ session.title = body.title;
431
+ session.metadata = body.metadata || session.metadata;
432
+ } else if (body?.metadata) {
433
+ session.metadata = body.metadata;
434
+ }
435
+ return new Response(JSON.stringify(session), { status: 200 });
436
+ }
437
+ return new Response(JSON.stringify({ ...session, title: patchedTitle }), { status: 200 });
438
+ };
439
+
440
+ try {
441
+ const runtime = createSessionTitleRuntime({
442
+ buildOpenCodeUrl: (pathname) => `http://opencode${pathname}`,
443
+ getOpenCodeAuthHeaders: () => ({}),
444
+ getSmallModelService: async () => ({
445
+ generateSmallModelText: async () => {
446
+ generationCalls += 1;
447
+ return { text: 'Rate limiting', providerID: 'test', modelID: 'test' };
448
+ },
449
+ }),
450
+ now: () => 1_000,
451
+ isTitleRefreshEnabled: () => true,
452
+ });
453
+
454
+ runtime.processPayload({
455
+ type: 'session.created',
456
+ properties: {
457
+ directory: '/repo',
458
+ info: { id: 'ses-new', title: session.title, time: { created: 100 } },
459
+ },
460
+ });
461
+ runtime.processPayload({
462
+ type: 'session.status',
463
+ properties: { sessionID: 'ses-new', directory: '/repo', status: { type: 'idle' } },
464
+ });
465
+ await new Promise((resolve) => setTimeout(resolve, 25));
466
+ expect(generationCalls).toBe(1);
467
+
468
+ // Later conversation progress + idle must not auto-refresh again.
469
+ messages.push(
470
+ { info: { id: 'u2', role: 'user' }, parts: [{ type: 'text', text: 'Also add retries' }] },
471
+ { info: { id: 'a2', role: 'assistant' }, parts: [{ type: 'text', text: 'Retries added' }] },
472
+ );
473
+ runtime.processPayload({
474
+ type: 'message.updated',
475
+ properties: { directory: '/repo', info: { sessionID: 'ses-new', role: 'user', time: { created: 200 } } },
476
+ });
477
+ runtime.processPayload({
478
+ type: 'session.status',
479
+ properties: { sessionID: 'ses-new', directory: '/repo', status: { type: 'busy' } },
480
+ });
481
+ runtime.processPayload({
482
+ type: 'session.status',
483
+ properties: { sessionID: 'ses-new', directory: '/repo', status: { type: 'idle' } },
484
+ });
485
+ await new Promise((resolve) => setTimeout(resolve, 25));
486
+ expect(generationCalls).toBe(1);
487
+
488
+ runtime.stop();
489
+ } finally {
490
+ globalThis.fetch = originalFetch;
491
+ }
492
+ });
493
+
494
+ it('still runs an explicit smart-title forced refresh after initial title', async () => {
495
+ const originalFetch = globalThis.fetch;
496
+ const session = {
497
+ id: 'ses-forced',
498
+ title: 'Rate limiting',
499
+ metadata: {
500
+ openchamber: {
501
+ titleRefresh: {
502
+ lastAutoTitle: 'Rate limiting',
503
+ generatedAt: 500,
504
+ forMessageID: 'a1',
505
+ },
506
+ },
507
+ },
508
+ };
509
+ const messages = [
510
+ { info: { id: 'u1', role: 'user' }, parts: [{ type: 'text', text: 'Add rate limiting' }] },
511
+ { info: { id: 'a1', role: 'assistant' }, parts: [{ type: 'text', text: 'Implemented rate limit' }] },
512
+ { info: { id: 'u2', role: 'user' }, parts: [{ type: 'text', text: 'Switch to token bucket' }] },
513
+ { info: { id: 'a2', role: 'assistant' }, parts: [{ type: 'text', text: 'Token bucket ready' }] },
514
+ ];
515
+ let generationCalls = 0;
516
+
517
+ globalThis.fetch = async (input, init = {}) => {
518
+ const url = String(input);
519
+ if (url.includes('/message')) {
520
+ return new Response(JSON.stringify(messages), { status: 200 });
521
+ }
522
+ if (init.method === 'PATCH') {
523
+ return new Response(JSON.stringify(session), { status: 200 });
524
+ }
525
+ return new Response(JSON.stringify(session), { status: 200 });
526
+ };
527
+
528
+ try {
529
+ const runtime = createSessionTitleRuntime({
530
+ buildOpenCodeUrl: (pathname) => `http://opencode${pathname}`,
531
+ getOpenCodeAuthHeaders: () => ({}),
532
+ getSmallModelService: async () => ({
533
+ generateSmallModelText: async () => {
534
+ generationCalls += 1;
535
+ return { text: 'Token bucket rate limiting', providerID: 'test', modelID: 'test' };
536
+ },
537
+ }),
538
+ now: () => 1_000,
539
+ isTitleRefreshEnabled: () => true,
540
+ });
541
+
542
+ runtime.processPayload({
543
+ type: 'session.status',
544
+ properties: { sessionID: 'ses-forced', directory: '/repo', status: { type: 'idle' } },
545
+ });
546
+ await new Promise((resolve) => setTimeout(resolve, 25));
547
+ expect(generationCalls).toBe(0);
548
+
549
+ runtime.processPayload({
550
+ type: 'session.updated',
551
+ properties: {
552
+ directory: '/repo',
553
+ info: {
554
+ id: 'ses-forced',
555
+ title: session.title,
556
+ metadata: {
557
+ openchamber: {
558
+ titleRefresh: {
559
+ ...session.metadata.openchamber.titleRefresh,
560
+ requestedAt: 1_000,
561
+ },
562
+ },
563
+ },
564
+ },
565
+ },
566
+ });
567
+ await new Promise((resolve) => setTimeout(resolve, 25));
568
+ expect(generationCalls).toBe(1);
569
+
570
+ runtime.stop();
571
+ } finally {
572
+ globalThis.fetch = originalFetch;
573
+ }
574
+ });
575
+
576
+ it('skips automatic initial refresh when session title refresh is disabled', async () => {
577
+ const originalFetch = globalThis.fetch;
578
+ const session = {
579
+ id: 'ses-disabled',
580
+ title: 'New session - 2026-07-10T12:00:00.000Z',
581
+ metadata: {},
582
+ };
583
+ let generationCalls = 0;
584
+
585
+ globalThis.fetch = async (input, init = {}) => {
586
+ if (String(input).includes('/message')) {
587
+ return new Response(JSON.stringify([
588
+ { info: { id: 'u1', role: 'user' }, parts: [{ type: 'text', text: 'hello' }] },
589
+ { info: { id: 'a1', role: 'assistant' }, parts: [{ type: 'text', text: 'world' }] },
590
+ ]), { status: 200 });
591
+ }
592
+ if (init.method === 'PATCH') {
593
+ return new Response(JSON.stringify(session), { status: 200 });
594
+ }
595
+ return new Response(JSON.stringify(session), { status: 200 });
596
+ };
597
+
598
+ try {
599
+ const runtime = createSessionTitleRuntime({
600
+ buildOpenCodeUrl: (pathname) => `http://opencode${pathname}`,
601
+ getOpenCodeAuthHeaders: () => ({}),
602
+ getSmallModelService: async () => ({
603
+ generateSmallModelText: async () => {
604
+ generationCalls += 1;
605
+ return { text: 'Should not run', providerID: 'test', modelID: 'test' };
606
+ },
607
+ }),
608
+ now: () => 1_000,
609
+ isTitleRefreshEnabled: () => false,
610
+ });
611
+
612
+ runtime.processPayload({
613
+ type: 'session.created',
614
+ properties: {
615
+ directory: '/repo',
616
+ info: { id: 'ses-disabled', title: session.title, time: { created: 100 } },
617
+ },
618
+ });
619
+ runtime.processPayload({
620
+ type: 'session.status',
621
+ properties: { sessionID: 'ses-disabled', directory: '/repo', status: { type: 'idle' } },
622
+ });
623
+ await new Promise((resolve) => setTimeout(resolve, 25));
624
+ expect(generationCalls).toBe(0);
625
+
626
+ runtime.stop();
627
+ } finally {
628
+ globalThis.fetch = originalFetch;
629
+ }
630
+ });
406
631
  });