@vellumai/assistant 0.5.1 → 0.5.2

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 (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,439 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ rmSync,
8
+ } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import { createAssistantMessage } from "../agent/message-types.js";
14
+ import type { Conversation } from "../daemon/conversation.js";
15
+ import { persistUserMessage } from "../daemon/conversation-messaging.js";
16
+ import {
17
+ addMessage,
18
+ getConversation,
19
+ provenanceFromTrustContext,
20
+ } from "../memory/conversation-crud.js";
21
+ import {
22
+ getConversationDirPath,
23
+ syncMessageToDisk,
24
+ } from "../memory/conversation-disk-view.js";
25
+ import {
26
+ getConversationByKey,
27
+ getOrCreateConversation as getOrCreateConversationMapping,
28
+ } from "../memory/conversation-key-store.js";
29
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
30
+ import { AssistantEventHub } from "../runtime/assistant-event-hub.js";
31
+ import type { AuthContext } from "../runtime/auth/types.js";
32
+ import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
33
+
34
+ const testDir = realpathSync(
35
+ mkdtempSync(join(tmpdir(), "conversation-routes-disk-view-test-")),
36
+ );
37
+ const workspaceDir = join(testDir, "workspace");
38
+ const conversationsDir = join(workspaceDir, "conversations");
39
+ mkdirSync(conversationsDir, { recursive: true });
40
+
41
+ mock.module("../util/platform.js", () => ({
42
+ getRootDir: () => testDir,
43
+ getDataDir: () => join(testDir, "data"),
44
+ getWorkspaceDir: () => workspaceDir,
45
+ getConversationsDir: () => conversationsDir,
46
+ isMacOS: () => process.platform === "darwin",
47
+ isLinux: () => process.platform === "linux",
48
+ isWindows: () => process.platform === "win32",
49
+ getPidPath: () => join(testDir, "test.pid"),
50
+ getDbPath: () => join(testDir, "test.db"),
51
+ getLogPath: () => join(testDir, "test.log"),
52
+ ensureDataDir: () => {},
53
+ }));
54
+
55
+ mock.module("../util/logger.js", () => ({
56
+ getLogger: () =>
57
+ new Proxy({} as Record<string, unknown>, {
58
+ get: () => () => {},
59
+ }),
60
+ }));
61
+
62
+ mock.module("../config/loader.js", () => ({
63
+ getConfig: () => ({
64
+ ui: {},
65
+ model: "test",
66
+ provider: "test",
67
+ memory: { enabled: false },
68
+ rateLimit: { maxRequestsPerMinute: 0 },
69
+ secretDetection: { enabled: false },
70
+ contextWindow: { maxInputTokens: 200000 },
71
+ services: {
72
+ inference: {
73
+ mode: "your-own",
74
+ provider: "anthropic",
75
+ model: "claude-opus-4-6",
76
+ },
77
+ "image-generation": {
78
+ mode: "your-own",
79
+ provider: "gemini",
80
+ model: "gemini-3.1-flash-image-preview",
81
+ },
82
+ "web-search": { mode: "your-own", provider: "inference-provider-native" },
83
+ },
84
+ }),
85
+ }));
86
+
87
+ initializeDb();
88
+
89
+ const conversationInstances = new Map<string, Conversation>();
90
+
91
+ const authContext: AuthContext = {
92
+ subject: "svc_gateway:self",
93
+ principalType: "svc_gateway",
94
+ assistantId: "self",
95
+ scopeProfile: "gateway_service_v1",
96
+ scopes: new Set([
97
+ "chat.read",
98
+ "chat.write",
99
+ "approval.read",
100
+ "approval.write",
101
+ "settings.read",
102
+ "settings.write",
103
+ "attachments.read",
104
+ "attachments.write",
105
+ "calls.read",
106
+ "calls.write",
107
+ "feature_flags.read",
108
+ "feature_flags.write",
109
+ ]),
110
+ policyEpoch: 1,
111
+ };
112
+
113
+ function resetTables(): void {
114
+ const db = getDb();
115
+ db.run("DELETE FROM messages");
116
+ db.run("DELETE FROM conversations");
117
+ db.run("DELETE FROM conversation_keys");
118
+ }
119
+
120
+ function resetConversationsDir(): void {
121
+ rmSync(conversationsDir, { recursive: true, force: true });
122
+ mkdirSync(conversationsDir, { recursive: true });
123
+ }
124
+
125
+ function createFakeConversation(conversationId: string): Conversation {
126
+ const conversation = {
127
+ conversationId,
128
+ processing: false,
129
+ currentRequestId: undefined as string | undefined,
130
+ abortController: null as AbortController | null,
131
+ trustContext: undefined as unknown,
132
+ turnChannelContext: null as
133
+ | {
134
+ userMessageChannel: string;
135
+ assistantMessageChannel: string;
136
+ }
137
+ | null,
138
+ turnInterfaceContext: null as
139
+ | {
140
+ userMessageInterface: string;
141
+ assistantMessageInterface: string;
142
+ }
143
+ | null,
144
+ messages: [] as Array<unknown>,
145
+ hostBashProxy: undefined as unknown,
146
+ hostFileProxy: undefined as unknown,
147
+ hostCuProxy: undefined as unknown,
148
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
149
+ memoryPolicy: {
150
+ scopeId: "default",
151
+ includeDefaultFallback: false,
152
+ strictSideEffects: false,
153
+ },
154
+ isProcessing(this: { processing: boolean }) {
155
+ return this.processing;
156
+ },
157
+ setChannelCapabilities: () => {},
158
+ setAssistantId: () => {},
159
+ setTrustContext(this: { trustContext: unknown }, ctx: unknown) {
160
+ this.trustContext = ctx;
161
+ },
162
+ setAuthContext: () => {},
163
+ setCommandIntent: () => {},
164
+ setTurnChannelContext(
165
+ this: {
166
+ turnChannelContext: {
167
+ userMessageChannel: string;
168
+ assistantMessageChannel: string;
169
+ } | null;
170
+ },
171
+ ctx: { userMessageChannel: string; assistantMessageChannel: string },
172
+ ) {
173
+ this.turnChannelContext = ctx;
174
+ },
175
+ getTurnChannelContext(this: {
176
+ turnChannelContext: {
177
+ userMessageChannel: string;
178
+ assistantMessageChannel: string;
179
+ } | null;
180
+ }) {
181
+ return this.turnChannelContext;
182
+ },
183
+ setTurnInterfaceContext(
184
+ this: {
185
+ turnInterfaceContext: {
186
+ userMessageInterface: string;
187
+ assistantMessageInterface: string;
188
+ } | null;
189
+ },
190
+ ctx: {
191
+ userMessageInterface: string;
192
+ assistantMessageInterface: string;
193
+ },
194
+ ) {
195
+ this.turnInterfaceContext = ctx;
196
+ },
197
+ getTurnInterfaceContext(this: {
198
+ turnInterfaceContext: {
199
+ userMessageInterface: string;
200
+ assistantMessageInterface: string;
201
+ } | null;
202
+ }) {
203
+ return this.turnInterfaceContext;
204
+ },
205
+ ensureActorScopedHistory: async () => {},
206
+ updateClient: () => {},
207
+ setHostBashProxy(this: { hostBashProxy: unknown }, proxy: unknown) {
208
+ this.hostBashProxy = proxy;
209
+ },
210
+ setHostFileProxy(this: { hostFileProxy: unknown }, proxy: unknown) {
211
+ this.hostFileProxy = proxy;
212
+ },
213
+ setHostCuProxy(this: { hostCuProxy: unknown }, proxy: unknown) {
214
+ this.hostCuProxy = proxy;
215
+ },
216
+ addPreactivatedSkillId: () => {},
217
+ hasAnyPendingConfirmation: () => false,
218
+ hasPendingConfirmation: () => false,
219
+ denyAllPendingConfirmations: () => {},
220
+ emitConfirmationStateChanged: () => {},
221
+ emitActivityState: () => {},
222
+ enqueueMessage: () => ({ queued: true, requestId: crypto.randomUUID() }),
223
+ getQueueDepth: () => 0,
224
+ handleConfirmationResponse: () => {},
225
+ handleSecretResponse: () => {},
226
+ getMessages(this: { messages: Array<unknown> }) {
227
+ return this.messages as never[];
228
+ },
229
+ persistUserMessage(
230
+ this: Conversation,
231
+ content: string,
232
+ attachments: Array<{
233
+ id: string;
234
+ filename: string;
235
+ mimeType: string;
236
+ data: string;
237
+ extractedText?: string;
238
+ filePath?: string;
239
+ }>,
240
+ requestId?: string,
241
+ metadata?: Record<string, unknown>,
242
+ displayContent?: string,
243
+ ): Promise<string> {
244
+ return persistUserMessage(
245
+ this as Parameters<typeof persistUserMessage>[0],
246
+ content,
247
+ attachments,
248
+ requestId,
249
+ metadata,
250
+ displayContent,
251
+ );
252
+ },
253
+ async runAgentLoop(
254
+ this: {
255
+ conversationId: string;
256
+ turnChannelContext: {
257
+ userMessageChannel: string;
258
+ assistantMessageChannel: string;
259
+ } | null;
260
+ turnInterfaceContext: {
261
+ userMessageInterface: string;
262
+ assistantMessageInterface: string;
263
+ } | null;
264
+ trustContext: unknown;
265
+ messages: Array<unknown>;
266
+ processing: boolean;
267
+ abortController: AbortController | null;
268
+ currentRequestId?: string;
269
+ },
270
+ _content: string,
271
+ _userMessageId: string,
272
+ onEvent: (msg: Record<string, unknown>) => void,
273
+ ): Promise<void> {
274
+ const assistantText = "Synthetic assistant reply";
275
+ const assistantMessage = createAssistantMessage(assistantText);
276
+ const assistantMetadata = {
277
+ ...provenanceFromTrustContext(this.trustContext as never),
278
+ ...(this.turnChannelContext
279
+ ? {
280
+ userMessageChannel: this.turnChannelContext.userMessageChannel,
281
+ assistantMessageChannel:
282
+ this.turnChannelContext.assistantMessageChannel,
283
+ }
284
+ : {}),
285
+ ...(this.turnInterfaceContext
286
+ ? {
287
+ userMessageInterface:
288
+ this.turnInterfaceContext.userMessageInterface,
289
+ assistantMessageInterface:
290
+ this.turnInterfaceContext.assistantMessageInterface,
291
+ }
292
+ : {}),
293
+ };
294
+
295
+ const persistedAssistant = await addMessage(
296
+ this.conversationId,
297
+ "assistant",
298
+ JSON.stringify(assistantMessage.content),
299
+ assistantMetadata,
300
+ );
301
+ this.messages.push(assistantMessage);
302
+
303
+ const conversationRow = getConversation(this.conversationId);
304
+ if (conversationRow) {
305
+ syncMessageToDisk(
306
+ this.conversationId,
307
+ persistedAssistant.id,
308
+ conversationRow.createdAt,
309
+ );
310
+ }
311
+
312
+ onEvent({
313
+ type: "assistant_text_delta",
314
+ text: assistantText,
315
+ conversationId: this.conversationId,
316
+ });
317
+ onEvent({
318
+ type: "message_complete",
319
+ conversationId: this.conversationId,
320
+ });
321
+
322
+ this.processing = false;
323
+ this.abortController = null;
324
+ this.currentRequestId = undefined;
325
+ },
326
+ };
327
+
328
+ return conversation as unknown as Conversation;
329
+ }
330
+
331
+ function getOrCreateFakeConversation(conversationId: string): Conversation {
332
+ const existing = conversationInstances.get(conversationId);
333
+ if (existing) return existing;
334
+ const created = createFakeConversation(conversationId);
335
+ conversationInstances.set(conversationId, created);
336
+ return created;
337
+ }
338
+
339
+ async function waitFor<T>(
340
+ getter: () => T | undefined,
341
+ timeoutMs = 3000,
342
+ ): Promise<T> {
343
+ const deadline = Date.now() + timeoutMs;
344
+ while (Date.now() < deadline) {
345
+ const value = getter();
346
+ if (value !== undefined) return value;
347
+ await Bun.sleep(20);
348
+ }
349
+ throw new Error("Timed out waiting for expected disk-view output");
350
+ }
351
+
352
+ beforeEach(() => {
353
+ resetTables();
354
+ resetConversationsDir();
355
+ conversationInstances.clear();
356
+ });
357
+
358
+ afterAll(() => {
359
+ resetDb();
360
+ try {
361
+ rmSync(testDir, { recursive: true, force: true });
362
+ } catch {
363
+ /* best effort */
364
+ }
365
+ });
366
+
367
+ describe("conversationKey send path disk-view regression", () => {
368
+ test("first send on a fresh conversationKey creates disk-view dir and writes user+assistant records", async () => {
369
+ const conversationKey = `fresh-conv-key-${crypto.randomUUID()}`;
370
+ const content = "Please persist this first turn.";
371
+
372
+ const response = await handleSendMessage(
373
+ new Request("http://localhost/v1/messages", {
374
+ method: "POST",
375
+ headers: { "Content-Type": "application/json" },
376
+ body: JSON.stringify({
377
+ conversationKey,
378
+ content,
379
+ sourceChannel: "vellum",
380
+ interface: "macos",
381
+ }),
382
+ }),
383
+ {
384
+ sendMessageDeps: {
385
+ getOrCreateConversation: async (conversationId: string) =>
386
+ getOrCreateFakeConversation(conversationId),
387
+ assistantEventHub: new AssistantEventHub(),
388
+ resolveAttachments: () => [],
389
+ },
390
+ },
391
+ authContext,
392
+ );
393
+
394
+ expect(response.status).toBe(202);
395
+ const body = (await response.json()) as {
396
+ accepted: boolean;
397
+ conversationId: string;
398
+ messageId: string;
399
+ };
400
+ expect(body.accepted).toBe(true);
401
+ expect(body.conversationId).toBeDefined();
402
+ expect(body.messageId).toBeDefined();
403
+
404
+ // Verify the real key store mapping is reused after the first send.
405
+ const mapping = getOrCreateConversationMapping(conversationKey);
406
+ expect(mapping.created).toBe(false);
407
+ expect(mapping.conversationId).toBe(body.conversationId);
408
+ expect(getConversationByKey(conversationKey)?.conversationId).toBe(
409
+ body.conversationId,
410
+ );
411
+
412
+ const conversationRow = getConversation(body.conversationId);
413
+ expect(conversationRow).not.toBeNull();
414
+ const conversationDir = getConversationDirPath(
415
+ body.conversationId,
416
+ conversationRow!.createdAt,
417
+ );
418
+ const metaPath = join(conversationDir, "meta.json");
419
+ const messagesPath = join(conversationDir, "messages.jsonl");
420
+
421
+ expect(existsSync(conversationDir)).toBe(true);
422
+ expect(existsSync(metaPath)).toBe(true);
423
+
424
+ const lines = await waitFor(() => {
425
+ if (!existsSync(messagesPath)) return undefined;
426
+ const raw = readFileSync(messagesPath, "utf-8").trim();
427
+ if (!raw) return undefined;
428
+ const parsed = raw
429
+ .split("\n")
430
+ .map((line) => JSON.parse(line) as { role: string; content?: string });
431
+ return parsed.length >= 2 ? parsed : undefined;
432
+ });
433
+
434
+ expect(lines[0]?.role).toBe("user");
435
+ expect(lines[0]?.content).toBe(content);
436
+ expect(lines[1]?.role).toBe("assistant");
437
+ expect(lines[1]?.content).toBe("Synthetic assistant reply");
438
+ });
439
+ });
@@ -216,7 +216,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
216
216
  expect(runAgentLoop).toHaveBeenCalledTimes(0);
217
217
  });
218
218
 
219
- test("passes undefined pendingRequestIds when no canonical hints are found", async () => {
219
+ test("passes empty pendingRequestIds array when no canonical hints are found", async () => {
220
220
  listPendingByDestinationMock.mockReturnValue([]);
221
221
  listCanonicalMock.mockReturnValue([]);
222
222
  routeGuardianReplyMock.mockResolvedValue({
@@ -279,7 +279,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
279
279
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
280
280
  const routerCall = (routeGuardianReplyMock as any).mock
281
281
  .calls[0][0] as Record<string, unknown>;
282
- expect(routerCall.pendingRequestIds).toBeUndefined();
282
+ expect(routerCall.pendingRequestIds).toEqual([]);
283
283
  expect(persistUserMessage).toHaveBeenCalledTimes(1);
284
284
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
285
285
  });
@@ -2,7 +2,7 @@
2
2
  * Tests for slash command interception in the POST /v1/messages handler.
3
3
  *
4
4
  * Validates that:
5
- * - Built-in slash commands (/status, /model, /commands) are intercepted and
5
+ * - Built-in slash commands (/status, /models, /commands) are intercepted and
6
6
  * do NOT trigger the agent loop.
7
7
  * - Regular messages pass through to the agent loop unchanged.
8
8
  */
@@ -122,12 +122,7 @@ mock.module("../daemon/conversation-process.js", () => ({
122
122
  configuredProviders: ["anthropic", "ollama"],
123
123
  }),
124
124
  isModelSlashCommand: (content: string) => {
125
- const trimmed = content.trim();
126
- return (
127
- trimmed === "/model" ||
128
- trimmed === "/models" ||
129
- trimmed.startsWith("/model ")
130
- );
125
+ return content.trim() === "/models";
131
126
  },
132
127
  }));
133
128
 
@@ -1065,9 +1065,24 @@ describe("buildTurnContextBlock (channel-only)", () => {
1065
1065
  },
1066
1066
  undefined,
1067
1067
  );
1068
- expect(block).toBe(
1069
- "<turn_context>\n" + "channel: telegram\n" + "</turn_context>",
1068
+ expect(block).toContain("<turn_context>");
1069
+ expect(block).toContain("channel: telegram");
1070
+ expect(block).toContain("response_discretion:");
1071
+ expect(block).toContain("</turn_context>");
1072
+ });
1073
+
1074
+ test("omits response_discretion for vellum channel", () => {
1075
+ const block = buildTurnContextBlock(
1076
+ {
1077
+ turnContext: {
1078
+ userMessageChannel: "vellum",
1079
+ assistantMessageChannel: "vellum",
1080
+ },
1081
+ conversationOriginChannel: "vellum",
1082
+ },
1083
+ undefined,
1070
1084
  );
1085
+ expect(block).not.toContain("response_discretion:");
1071
1086
  });
1072
1087
 
1073
1088
  test('uses "unknown" when conversationOriginChannel is null', () => {
@@ -1779,14 +1779,9 @@ describe("bundled skill: claude-code", () => {
1779
1779
 
1780
1780
  const APP_BUILDER_TOOL_NAMES = [
1781
1781
  "app_create",
1782
- "app_list",
1783
- "app_query",
1784
- "app_update",
1785
1782
  "app_delete",
1786
- "app_file_list",
1787
- "app_file_read",
1788
- "app_file_edit",
1789
- "app_file_write",
1783
+ "app_generate_icon",
1784
+ "app_refresh",
1790
1785
  ] as const;
1791
1786
 
1792
1787
  describe("bundled skill: app-builder", () => {
@@ -1803,7 +1798,7 @@ describe("bundled skill: app-builder", () => {
1803
1798
  sessionState = new Map<string, string>();
1804
1799
  });
1805
1800
 
1806
- test("app-builder skill activation registers all 9 canonical non-proxy tools in allowedToolNames", () => {
1801
+ test("app-builder skill activation registers all 4 canonical non-proxy tools in allowedToolNames", () => {
1807
1802
  mockCatalog = [
1808
1803
  makeSkill("app-builder", "/path/to/bundled-skills/app-builder"),
1809
1804
  ];
@@ -1862,7 +1857,7 @@ describe("bundled skill: app-builder", () => {
1862
1857
 
1863
1858
  const tools = mockRegisteredTools.get("app-builder");
1864
1859
  expect(tools).toBeDefined();
1865
- expect(tools!.length).toBe(9);
1860
+ expect(tools!.length).toBe(4);
1866
1861
 
1867
1862
  // All tools should have skill origin metadata
1868
1863
  for (const tool of tools!) {
@@ -0,0 +1,149 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ resolveSlash,
5
+ type SlashContext,
6
+ } from "../daemon/conversation-slash.js";
7
+
8
+ function makeSlashContext(
9
+ overrides: Partial<SlashContext> = {},
10
+ ): SlashContext {
11
+ return {
12
+ messageCount: 4,
13
+ inputTokens: 1024,
14
+ outputTokens: 256,
15
+ maxInputTokens: 200000,
16
+ model: "claude-opus-4-6",
17
+ provider: "anthropic",
18
+ estimatedCost: 0.03,
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ async function resolveCommandsLines(context?: SlashContext): Promise<string[]> {
24
+ const result = await resolveSlash("/commands", context);
25
+ expect(result.kind).toBe("unknown");
26
+ if (result.kind !== "unknown") {
27
+ throw new Error("Expected /commands to resolve to kind=unknown");
28
+ }
29
+ return result.message.split("\n");
30
+ }
31
+
32
+ describe("resolveSlash /commands interface-aware help", () => {
33
+ test("renders desktop command help for macOS", async () => {
34
+ const lines = await resolveCommandsLines(
35
+ makeSlashContext({ userMessageInterface: "macos" }),
36
+ );
37
+ expect(lines).toEqual([
38
+ "/commands — List all available commands",
39
+ "/models — List all available models",
40
+ "/status — Show conversation status and context usage",
41
+ "/btw — Ask a side question while the assistant is working",
42
+ "/fork — Fork the current conversation into a new branch",
43
+ "/pair — Generate pairing info for connecting a mobile device",
44
+ ]);
45
+ expect(lines).not.toContain(
46
+ "/model — Switch the active model",
47
+ );
48
+ });
49
+
50
+ test("renders iOS command help with /fork but without /pair", async () => {
51
+ const lines = await resolveCommandsLines(
52
+ makeSlashContext({ userMessageInterface: "ios" }),
53
+ );
54
+ expect(lines).toEqual([
55
+ "/commands — List all available commands",
56
+ "/models — List all available models",
57
+ "/status — Show conversation status and context usage",
58
+ "/btw — Ask a side question while the assistant is working",
59
+ "/fork — Fork the current conversation into a new branch",
60
+ ]);
61
+ });
62
+
63
+ test("renders explicit cli command help without /pair", async () => {
64
+ const lines = await resolveCommandsLines(
65
+ makeSlashContext({ userMessageInterface: "cli" }),
66
+ );
67
+ expect(lines).toEqual([
68
+ "/commands — List all available commands",
69
+ "/models — List all available models",
70
+ "/status — Show conversation status and context usage",
71
+ "/btw — Ask a side question while the assistant is working",
72
+ ]);
73
+ });
74
+
75
+ test("keeps legacy fallback help when no interface is provided", async () => {
76
+ const lines = await resolveCommandsLines(makeSlashContext());
77
+ expect(lines).toEqual([
78
+ "/commands — List all available commands",
79
+ "/models — List all available models",
80
+ "/pair — Generate pairing info for connecting a mobile device",
81
+ "/status — Show conversation status and context usage",
82
+ ]);
83
+ });
84
+
85
+ test("keeps context-free fallback without /status", async () => {
86
+ const lines = await resolveCommandsLines();
87
+ expect(lines).toEqual([
88
+ "/commands — List all available commands",
89
+ "/models — List all available models",
90
+ "/pair — Generate pairing info for connecting a mobile device",
91
+ ]);
92
+ });
93
+ });
94
+
95
+ describe("resolveSlash command contract", () => {
96
+ test("keeps unsupported slash forms as passthrough", async () => {
97
+ const slashForms = [
98
+ "/commands foo",
99
+ "/models foo",
100
+ "/status foo",
101
+ "/pair foo",
102
+ "/btw",
103
+ ];
104
+
105
+ for (const input of slashForms) {
106
+ const result = await resolveSlash(
107
+ input,
108
+ makeSlashContext({ userMessageInterface: "macos" }),
109
+ );
110
+ expect(result).toEqual({ kind: "passthrough", content: input });
111
+ }
112
+ });
113
+
114
+ test("rejects /pair on iOS interfaces", async () => {
115
+ const result = await resolveSlash(
116
+ "/pair",
117
+ makeSlashContext({ userMessageInterface: "ios" }),
118
+ );
119
+ expect(result.kind).toBe("unknown");
120
+ if (result.kind !== "unknown") {
121
+ throw new Error("Expected /pair on iOS to resolve to kind=unknown");
122
+ }
123
+ expect(result.message).toContain("only available in the macOS desktop app");
124
+ });
125
+
126
+ test("keeps /pair rejected for explicit non-macOS interfaces", async () => {
127
+ const result = await resolveSlash(
128
+ "/pair",
129
+ makeSlashContext({ userMessageInterface: "cli" }),
130
+ );
131
+ expect(result.kind).toBe("unknown");
132
+ if (result.kind !== "unknown") {
133
+ throw new Error("Expected /pair on cli to resolve to kind=unknown");
134
+ }
135
+ expect(result.message).toContain("only available in the macOS desktop app");
136
+ });
137
+
138
+ test("keeps /pair handling enabled on macOS interfaces", async () => {
139
+ const result = await resolveSlash(
140
+ "/pair",
141
+ makeSlashContext({ userMessageInterface: "macos" }),
142
+ );
143
+ expect(result.kind).toBe("unknown");
144
+ if (result.kind !== "unknown") {
145
+ throw new Error("Expected /pair on macOS to resolve to kind=unknown");
146
+ }
147
+ expect(result.message).toContain("Pairing is not available");
148
+ });
149
+ });