@vellumai/assistant 0.11.5 → 0.11.6-staging.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.
Files changed (233) hide show
  1. package/AGENTS.md +5 -1
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  4. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  5. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
  7. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
  9. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
  10. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
  11. package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
  12. package/openapi.yaml +421 -15
  13. package/package.json +1 -1
  14. package/scripts/sync-web-search-catalog.ts +6 -0
  15. package/src/__tests__/app-pin-store.test.ts +149 -0
  16. package/src/__tests__/channel-availability-routes.test.ts +23 -1
  17. package/src/__tests__/channel-readiness-discord.test.ts +231 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +126 -0
  19. package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
  20. package/src/__tests__/channel-reply-delivery.test.ts +4 -4
  21. package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
  22. package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
  23. package/src/__tests__/conversation-error.test.ts +17 -0
  24. package/src/__tests__/conversation-seed-composer.test.ts +8 -0
  25. package/src/__tests__/conversation-slash-commands.test.ts +8 -0
  26. package/src/__tests__/disk-pressure-policy.test.ts +6 -0
  27. package/src/__tests__/gemini-provider.test.ts +138 -0
  28. package/src/__tests__/history-repair.test.ts +105 -3
  29. package/src/__tests__/identity-routes.test.ts +1 -0
  30. package/src/__tests__/llm-catalog-parity.test.ts +45 -0
  31. package/src/__tests__/migration-import-from-path.test.ts +349 -0
  32. package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
  33. package/src/__tests__/oauth-commands-routes.test.ts +89 -0
  34. package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
  35. package/src/__tests__/openai-provider.test.ts +18 -0
  36. package/src/__tests__/openai-responses-provider.test.ts +18 -0
  37. package/src/__tests__/platform-callback-registration.test.ts +184 -0
  38. package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
  39. package/src/__tests__/pricing.test.ts +2 -2
  40. package/src/__tests__/public-ingress-urls.test.ts +36 -0
  41. package/src/__tests__/resolve-trust-class.test.ts +0 -48
  42. package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
  43. package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
  44. package/src/__tests__/settings-routes.test.ts +85 -3
  45. package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
  46. package/src/agent/history-repair/history-repair.ts +45 -14
  47. package/src/agent/loop.ts +4 -1
  48. package/src/api/constants/profile-config-validation.ts +60 -0
  49. package/src/api/events/tool-result.ts +6 -1
  50. package/src/api/events/watch-retro-completed.ts +52 -0
  51. package/src/api/index.ts +11 -0
  52. package/src/apps/app-pin-reconciler.ts +92 -0
  53. package/src/apps/app-pin-store.ts +125 -0
  54. package/src/channels/gateway-channel-socket-health.ts +32 -0
  55. package/src/channels/gateway-discord-admission.ts +32 -0
  56. package/src/channels/types.ts +20 -0
  57. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  58. package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
  59. package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
  60. package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
  61. package/src/cli/commands/channels/index.ts +45 -31
  62. package/src/cli/commands/inference-profiles.ts +56 -3
  63. package/src/cli/commands/inference-providers.ts +28 -2
  64. package/src/cli/commands/oauth/index.help.ts +7 -1
  65. package/src/cli/commands/oauth/request.test.ts +290 -0
  66. package/src/cli/commands/oauth/request.ts +57 -41
  67. package/src/cli/lib/bundled-marketplace.json +14 -1
  68. package/src/cli/lib/open-browser.test.ts +67 -0
  69. package/src/cli/lib/open-browser.ts +24 -5
  70. package/src/config/__tests__/profile-materialization.test.ts +26 -0
  71. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
  72. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  73. package/src/config/feature-flag-registry.json +17 -1
  74. package/src/config/profile-materialization.ts +29 -0
  75. package/src/config/sanitize-for-transfer.ts +16 -0
  76. package/src/config/schemas/llm.ts +7 -0
  77. package/src/config/schemas/services.ts +6 -0
  78. package/src/context/outbound-sanitize.ts +6 -0
  79. package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
  80. package/src/daemon/conversation-error.ts +24 -2
  81. package/src/daemon/conversation-slash.ts +6 -15
  82. package/src/daemon/daemon-control.ts +1 -0
  83. package/src/daemon/disk-pressure-policy.ts +7 -1
  84. package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
  85. package/src/daemon/handlers/config-ingress.ts +115 -5
  86. package/src/daemon/lifecycle.ts +26 -0
  87. package/src/daemon/message-types/web-activity.ts +3 -2
  88. package/src/daemon/trust-context.ts +0 -37
  89. package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
  90. package/src/inbound/platform-callback-registration.ts +28 -2
  91. package/src/inbound/public-ingress-urls.ts +12 -0
  92. package/src/inbound/tunnel-probe.ts +261 -0
  93. package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
  94. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
  95. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
  96. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
  97. package/src/live-voice/live-voice-connection.ts +46 -8
  98. package/src/live-voice/live-voice-manager.ts +25 -0
  99. package/src/live-voice/live-voice-session-manager.ts +318 -2
  100. package/src/live-voice/live-voice-session.ts +52 -2
  101. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
  102. package/src/messaging/providers/channel-transport.ts +64 -47
  103. package/src/messaging/providers/discord/send.test.ts +46 -1
  104. package/src/messaging/providers/discord/send.ts +51 -0
  105. package/src/messaging/providers/discord/transport.ts +26 -3
  106. package/src/messaging/providers/index.ts +22 -47
  107. package/src/messaging/providers/slack/send.test.ts +83 -26
  108. package/src/messaging/providers/slack/send.ts +120 -51
  109. package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
  110. package/src/messaging/providers/slack/stream-tasks.ts +39 -0
  111. package/src/messaging/providers/slack/transport.ts +24 -22
  112. package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
  113. package/src/messaging/providers/telegram-bot/send.ts +43 -0
  114. package/src/messaging/providers/telegram-bot/transport.ts +25 -8
  115. package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
  116. package/src/notifications/adapters/telegram.ts +48 -1
  117. package/src/notifications/assistant-reply-producer.ts +7 -7
  118. package/src/notifications/conversation-seed-composer.ts +7 -2
  119. package/src/oauth/byo-connection.test.ts +63 -0
  120. package/src/oauth/byo-connection.ts +16 -15
  121. package/src/oauth/connection.test.ts +111 -0
  122. package/src/oauth/connection.ts +142 -1
  123. package/src/oauth/platform-connection.test.ts +34 -0
  124. package/src/oauth/platform-connection.ts +28 -5
  125. package/src/oauth/seed-providers.ts +15 -1
  126. package/src/permissions/types.ts +3 -1
  127. package/src/persistence/conversation-crud.ts +46 -0
  128. package/src/persistence/conversation-types.ts +11 -9
  129. package/src/persistence/db-async-query.ts +2 -1
  130. package/src/persistence/db-maintenance.ts +15 -0
  131. package/src/persistence/embeddings/qdrant-manager.ts +1 -0
  132. package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
  133. package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
  134. package/src/persistence/migrations/369-create-app-pins.ts +37 -0
  135. package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
  136. package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
  137. package/src/persistence/schema/index.ts +1 -0
  138. package/src/persistence/schema/infrastructure.ts +17 -0
  139. package/src/persistence/schema/watch.ts +29 -0
  140. package/src/persistence/steps.ts +6 -0
  141. package/src/plugins/mtime-cache.ts +11 -0
  142. package/src/providers/__tests__/retry-network-error.test.ts +84 -0
  143. package/src/providers/connection-resolution.ts +23 -1
  144. package/src/providers/content-blocks.ts +9 -0
  145. package/src/providers/fetch-provider-catalog.ts +19 -0
  146. package/src/providers/gemini/client.ts +13 -5
  147. package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
  148. package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
  149. package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
  150. package/src/providers/inference/adapter-factory.ts +0 -9
  151. package/src/providers/inference/credential-rotation.ts +61 -0
  152. package/src/providers/inference/endpoint-probe.ts +115 -0
  153. package/src/providers/inference/profile-probe.ts +256 -0
  154. package/src/providers/model-catalog.ts +170 -125
  155. package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
  156. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
  157. package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
  158. package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
  159. package/src/providers/openai/api-error-normalization.ts +16 -2
  160. package/src/providers/openai/chat-completions-provider.ts +75 -29
  161. package/src/providers/openai/responses-provider.ts +5 -2
  162. package/src/providers/openrouter/client.ts +0 -1
  163. package/src/providers/provider-send-message.ts +11 -0
  164. package/src/providers/retry.ts +6 -0
  165. package/src/providers/search-provider-catalog.ts +20 -0
  166. package/src/providers/vercel-ai-gateway/client.ts +0 -1
  167. package/src/runtime/AGENTS.md +1 -0
  168. package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
  169. package/src/runtime/__tests__/host-observe.test.ts +302 -0
  170. package/src/runtime/channel-readiness-service.ts +214 -14
  171. package/src/runtime/channel-readiness-types.ts +49 -2
  172. package/src/runtime/channel-reply-delivery.ts +2 -2
  173. package/src/runtime/desktop-presence.ts +24 -21
  174. package/src/runtime/host-observe.ts +246 -0
  175. package/src/runtime/http-server.ts +181 -1
  176. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
  177. package/src/runtime/migrations/staged-import-path.ts +116 -0
  178. package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
  179. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
  180. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
  181. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
  182. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
  183. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
  184. package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
  185. package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
  186. package/src/runtime/routes/app-management-routes.ts +140 -29
  187. package/src/runtime/routes/channel-availability-routes.ts +1 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +14 -2
  189. package/src/runtime/routes/conversation-query-routes.ts +10 -0
  190. package/src/runtime/routes/guardian-approval-interception.ts +24 -33
  191. package/src/runtime/routes/host-cu-routes.ts +18 -0
  192. package/src/runtime/routes/identity-routes.ts +2 -0
  193. package/src/runtime/routes/inbound-message-handler.ts +10 -7
  194. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
  195. package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
  196. package/src/runtime/routes/index.ts +2 -0
  197. package/src/runtime/routes/inference-profiles-routes.ts +232 -31
  198. package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
  199. package/src/runtime/routes/ingress-status-routes.ts +180 -0
  200. package/src/runtime/routes/live-voice-routes.test.ts +40 -1
  201. package/src/runtime/routes/live-voice-routes.ts +34 -0
  202. package/src/runtime/routes/migration-routes.ts +218 -10
  203. package/src/runtime/routes/oauth-commands-routes.ts +23 -16
  204. package/src/runtime/routes/plugins-routes.ts +12 -28
  205. package/src/runtime/routes/question-routes.ts +6 -0
  206. package/src/runtime/routes/secret-routes.ts +7 -27
  207. package/src/runtime/routes/settings-routes.ts +9 -6
  208. package/src/runtime/routes/watch-routes.ts +807 -0
  209. package/src/runtime/slack-reply-session.test.ts +230 -121
  210. package/src/runtime/slack-reply-session.ts +113 -81
  211. package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
  212. package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
  213. package/src/security/__tests__/untrusted-content.test.ts +42 -0
  214. package/src/security/untrusted-content.ts +28 -9
  215. package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
  216. package/src/telemetry/live-voice-funnel.ts +75 -8
  217. package/src/tools/credentials/store.ts +18 -6
  218. package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
  219. package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
  220. package/src/tools/network/__tests__/web-search.test.ts +97 -2
  221. package/src/tools/network/firecrawl-compat.ts +90 -0
  222. package/src/tools/network/web-fetch.ts +142 -62
  223. package/src/tools/network/web-search.ts +141 -55
  224. package/src/tools/types.ts +2 -1
  225. package/src/util/oauth-request-body.test.ts +74 -0
  226. package/src/util/oauth-request-body.ts +60 -0
  227. package/src/util/worker-process.ts +1 -0
  228. package/src/watch/__tests__/watch-retro.test.ts +665 -0
  229. package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
  230. package/src/watch/__tests__/watch-timeline.test.ts +670 -0
  231. package/src/watch/watch-retro.ts +480 -0
  232. package/src/watch/watch-session-manager.ts +575 -0
  233. package/src/watch/watch-timeline.ts +848 -0
@@ -319,9 +319,9 @@ mock.module("../../../cli/lib/toggle-plugin.js", () => ({
319
319
  enablePlugin: enablePluginSpy,
320
320
  }));
321
321
 
322
- // Spy on the plugin-schedule reconcile the disable handler pokes. The handler
323
- // imports it lazily at call time, so this mock intercepts the dynamic import
324
- // and keeps the reconciler's persistence graph out of the test.
322
+ // Spy on the plugin-schedule reconcile so a toggle cannot silently go back
323
+ // to poking it from the route. Enable and disable both await the source
324
+ // reconcile, which converges schedules itself.
325
325
  const reconcilePluginSchedulesSpy = mock(() => Promise.resolve());
326
326
 
327
327
  mock.module("../../../schedule/plugin-schedule-reconciler.js", () => ({
@@ -2597,27 +2597,10 @@ async function invokeEnable(
2597
2597
  return (await enableHandler(args)) as { ok: boolean };
2598
2598
  }
2599
2599
 
2600
- function invokeDisable(args: RouteHandlerArgs = {}): { ok: boolean } {
2601
- return disableHandler(args) as { ok: boolean };
2602
- }
2603
-
2604
- /**
2605
- * Wait for a toggle handler's fire-and-forget reconcile poke to land. The poke
2606
- * runs off the microtask queue (the disable path also resolves a lazy
2607
- * `import()` first), so the assertion cannot run on the handler's synchronous
2608
- * return.
2609
- */
2610
- async function waitForReconcile(
2611
- spy: { mock: { calls: unknown[] } },
2612
- label: string,
2613
- ): Promise<void> {
2614
- for (let i = 0; i < 50; i++) {
2615
- if (spy.mock.calls.length > 0) {
2616
- return;
2617
- }
2618
- await new Promise((resolve) => setTimeout(resolve, 2));
2619
- }
2620
- throw new Error(`${label} was never triggered`);
2600
+ async function invokeDisable(
2601
+ args: RouteHandlerArgs = {},
2602
+ ): Promise<{ ok: boolean }> {
2603
+ return (await disableHandler(args)) as { ok: boolean };
2621
2604
  }
2622
2605
 
2623
2606
  /** Assert the spy received exactly one sync_changed carrying `plugins:list`. */
@@ -2745,42 +2728,38 @@ describe("POST /v1/plugins/:name/disable", () => {
2745
2728
  reconcilePluginSourcesNowSpy.mockClear();
2746
2729
  });
2747
2730
 
2748
- test("reconciles plugin-declared schedules so the plugin's rows disarm now", async () => {
2731
+ test("awaits the source reconcile so shutdown runs before the route returns", async () => {
2749
2732
  disablePluginSpy.mockImplementation((name) =>
2750
2733
  toggleResult(name, "disable"),
2751
2734
  );
2752
2735
 
2753
- invokeDisable({ pathParams: { name: "simple-memory" } });
2736
+ await invokeDisable({ pathParams: { name: "simple-memory" } });
2754
2737
 
2755
- // Without this poke the rows stay armed until the reconciler's next
2756
- // backstop sweep. Disable stays on the schedule poke: the sentinel filters
2757
- // the plugin out at read time, and a source reconcile here would run its
2758
- // shutdown hooks.
2759
- await waitForReconcile(
2760
- reconcilePluginSchedulesSpy,
2761
- "plugin schedule reconcile",
2762
- );
2763
- expect(reconcilePluginSourcesNowSpy).not.toHaveBeenCalled();
2738
+ // The sentinel hides tools at read time, but `init` already opened
2739
+ // in-process work (a live subscribe, a poll worker). The source
2740
+ // reconcile is what runs `shutdown`. It converges schedules itself, so
2741
+ // the route does not poke them separately.
2742
+ expect(reconcilePluginSourcesNowSpy).toHaveBeenCalledTimes(1);
2743
+ expect(reconcilePluginSchedulesSpy).not.toHaveBeenCalled();
2764
2744
  });
2765
2745
 
2766
- test("a failed toggle does not poke the schedule reconcile", async () => {
2746
+ test("a failed toggle does not run the source reconcile", async () => {
2767
2747
  disablePluginSpy.mockImplementation((name) => {
2768
2748
  throw new PluginDirectoryNotFoundError(name);
2769
2749
  });
2770
2750
 
2771
- expect(() => invokeDisable({ pathParams: { name: "ghost" } })).toThrow(
2772
- NotFoundError,
2773
- );
2774
- await new Promise((resolve) => setTimeout(resolve, 10));
2775
- expect(reconcilePluginSchedulesSpy).not.toHaveBeenCalled();
2751
+ await expect(
2752
+ invokeDisable({ pathParams: { name: "ghost" } }),
2753
+ ).rejects.toThrow(NotFoundError);
2754
+ expect(reconcilePluginSourcesNowSpy).not.toHaveBeenCalled();
2776
2755
  });
2777
2756
 
2778
- test("disables the plugin and broadcasts sync_changed(plugins:list)", () => {
2757
+ test("disables the plugin and broadcasts sync_changed(plugins:list)", async () => {
2779
2758
  disablePluginSpy.mockImplementation((name) =>
2780
2759
  toggleResult(name, "disable"),
2781
2760
  );
2782
2761
 
2783
- const result = invokeDisable({ pathParams: { name: "simple-memory" } });
2762
+ const result = await invokeDisable({ pathParams: { name: "simple-memory" } });
2784
2763
 
2785
2764
  expect(result).toEqual({ ok: true });
2786
2765
  expect(disablePluginSpy.mock.calls[0]?.[0]).toBe("simple-memory");
@@ -2789,12 +2768,12 @@ describe("POST /v1/plugins/:name/disable", () => {
2789
2768
  expectPluginsListBroadcast();
2790
2769
  });
2791
2770
 
2792
- test("threads x-vellum-client-id into the published event's originClientId", () => {
2771
+ test("threads x-vellum-client-id into the published event's originClientId", async () => {
2793
2772
  disablePluginSpy.mockImplementation((name) =>
2794
2773
  toggleResult(name, "disable"),
2795
2774
  );
2796
2775
 
2797
- invokeDisable({
2776
+ await invokeDisable({
2798
2777
  pathParams: { name: "simple-memory" },
2799
2778
  headers: { "x-vellum-client-id": "client-xyz" },
2800
2779
  });
@@ -2806,35 +2785,35 @@ describe("POST /v1/plugins/:name/disable", () => {
2806
2785
  });
2807
2786
  });
2808
2787
 
2809
- test("PluginAlreadyInStateException → ConflictError (409), no broadcast", () => {
2788
+ test("PluginAlreadyInStateException → ConflictError (409), no broadcast", async () => {
2810
2789
  disablePluginSpy.mockImplementation((name) => {
2811
2790
  throw new PluginAlreadyInStateException(name, "disable");
2812
2791
  });
2813
2792
 
2814
- expect(() =>
2793
+ await expect(
2815
2794
  invokeDisable({ pathParams: { name: "simple-memory" } }),
2816
- ).toThrow(ConflictError);
2795
+ ).rejects.toThrow(ConflictError);
2817
2796
  expect(broadcastMessageSpy).not.toHaveBeenCalled();
2818
2797
  });
2819
2798
 
2820
- test("PluginDirectoryNotFoundError → NotFoundError (404)", () => {
2799
+ test("PluginDirectoryNotFoundError → NotFoundError (404)", async () => {
2821
2800
  disablePluginSpy.mockImplementation((name) => {
2822
2801
  throw new PluginDirectoryNotFoundError(name);
2823
2802
  });
2824
2803
 
2825
- expect(() => invokeDisable({ pathParams: { name: "ghost" } })).toThrow(
2826
- NotFoundError,
2827
- );
2804
+ await expect(
2805
+ invokeDisable({ pathParams: { name: "ghost" } }),
2806
+ ).rejects.toThrow(NotFoundError);
2828
2807
  });
2829
2808
 
2830
- test("InvalidPluginNameError → BadRequestError (400)", () => {
2809
+ test("InvalidPluginNameError → BadRequestError (400)", async () => {
2831
2810
  disablePluginSpy.mockImplementation(() => {
2832
2811
  throw new ToggleInvalidPluginNameError("../escape");
2833
2812
  });
2834
2813
 
2835
- expect(() => invokeDisable({ pathParams: { name: "../escape" } })).toThrow(
2836
- BadRequestError,
2837
- );
2814
+ await expect(
2815
+ invokeDisable({ pathParams: { name: "../escape" } }),
2816
+ ).rejects.toThrow(BadRequestError);
2838
2817
  });
2839
2818
  });
2840
2819
 
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A watch session's principal lookup against a stale guardian-delivery cache.
3
+ *
4
+ * The cache keeps a successful read that found no binding, and a gateway-side
5
+ * binding write does not invalidate it, so the read a session start depends on
6
+ * has to force a refresh or a first run fails for the cache's whole TTL. The
7
+ * guardian reader is mocked here rather than in the main watch-routes suite so
8
+ * the substitution stays inside this file.
9
+ */
10
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ import type { GuardianDelivery } from "@vellumai/gateway-client";
13
+
14
+ let cachedResult: GuardianDelivery[] | null = null;
15
+ let freshResult: GuardianDelivery[] | null = null;
16
+ let cachedCalls = 0;
17
+ let freshCalls = 0;
18
+
19
+ // Only the two read entry points are substituted; the rest of the reader's
20
+ // surface stays real, because other modules in this graph import from it.
21
+ const realReader =
22
+ await import("../../../contacts/guardian-delivery-reader.js");
23
+ mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
24
+ ...realReader,
25
+ getGuardianDelivery: () => {
26
+ cachedCalls++;
27
+ return Promise.resolve(cachedResult);
28
+ },
29
+ getGuardianDeliveryFresh: () => {
30
+ freshCalls++;
31
+ return Promise.resolve(freshResult);
32
+ },
33
+ peekCachedGuardianDelivery: () => undefined,
34
+ }));
35
+
36
+ const { initializeDb } = await import("../../../persistence/db-init.js");
37
+ const { WatchSessionManager } =
38
+ await import("../../../watch/watch-session-manager.js");
39
+ const { WatchStreamSession, resolveWatchActorPrincipalId } =
40
+ await import("../watch-routes.js");
41
+
42
+ await initializeDb();
43
+
44
+ afterAll(() => {
45
+ mock.restore();
46
+ });
47
+
48
+ /** The shape `guardianForChannel` matches on for the vellum binding. */
49
+ function guardian(principalId: string): GuardianDelivery {
50
+ return {
51
+ principalId,
52
+ channelType: "vellum",
53
+ status: "active",
54
+ } as unknown as GuardianDelivery;
55
+ }
56
+
57
+ /** A socket that keeps the frame types the session sent it. */
58
+ function newSocket() {
59
+ const types: string[] = [];
60
+ return {
61
+ types,
62
+ send: (data: string) => {
63
+ types.push((JSON.parse(data) as { type: string }).type);
64
+ },
65
+ close: () => {},
66
+ };
67
+ }
68
+
69
+ /** A transcriber that opens without reaching any provider. */
70
+ function newTranscriber() {
71
+ return {
72
+ providerId: "deepgram" as const,
73
+ boundaryId: "daemon-streaming" as const,
74
+ start: async () => {},
75
+ sendAudio: () => {},
76
+ stop: () => {},
77
+ };
78
+ }
79
+
80
+ describe("watch principal lookup against a stale guardian cache", () => {
81
+ beforeEach(() => {
82
+ cachedCalls = 0;
83
+ freshCalls = 0;
84
+ cachedResult = [];
85
+ freshResult = [];
86
+ });
87
+
88
+ test("reads past an empty cached answer to the binding that now exists", async () => {
89
+ cachedResult = [];
90
+ freshResult = [guardian("guardian-just-bound")];
91
+
92
+ expect(await resolveWatchActorPrincipalId()).toBe("guardian-just-bound");
93
+ expect(freshCalls).toBe(1);
94
+ expect(cachedCalls).toBe(0);
95
+ });
96
+
97
+ test("a session started after the binding appears reaches ready", async () => {
98
+ // The cache still holds the empty answer from before the guardian was
99
+ // bound, which is the state a first run starts in.
100
+ cachedResult = [];
101
+ freshResult = [guardian("guardian-just-bound")];
102
+
103
+ const manager = new WatchSessionManager({
104
+ observe: async () => ({ ok: true, axTree: "Window: Editor" }),
105
+ });
106
+ const ws = newSocket();
107
+ const session = new WatchStreamSession(ws, {
108
+ mimeType: "audio/webm",
109
+ manager,
110
+ resolveTranscriber: async () => newTranscriber(),
111
+ });
112
+
113
+ await session.start();
114
+
115
+ // The handshake rather than the exact sequence: the session reads the
116
+ // screen as it opens, so its capture acknowledgement lands in here too.
117
+ expect(ws.types[0]).toBe("ready");
118
+ expect(ws.types).not.toContain("error");
119
+ expect(manager.isActive()).toBe(true);
120
+
121
+ session.destroy();
122
+ });
123
+
124
+ test("no binding at all still fails closed", async () => {
125
+ cachedResult = [];
126
+ freshResult = [];
127
+
128
+ const ws = newSocket();
129
+ const session = new WatchStreamSession(ws, {
130
+ mimeType: "audio/webm",
131
+ manager: new WatchSessionManager(),
132
+ resolveTranscriber: async () => newTranscriber(),
133
+ });
134
+
135
+ await session.start();
136
+
137
+ expect(ws.types).toEqual(["error", "closed"]);
138
+ });
139
+ });