@vellumai/assistant 0.4.49 → 0.4.51

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 (353) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/integrations.md +2 -2
  4. package/docs/architecture/keychain-broker.md +6 -6
  5. package/docs/architecture/memory.md +180 -119
  6. package/knip.json +32 -0
  7. package/package.json +3 -2
  8. package/src/__tests__/agent-loop.test.ts +3 -1
  9. package/src/__tests__/anthropic-provider.test.ts +114 -23
  10. package/src/__tests__/approval-cascade.test.ts +1 -15
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  13. package/src/__tests__/btw-routes.test.ts +61 -5
  14. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  15. package/src/__tests__/checker.test.ts +13 -0
  16. package/src/__tests__/config-schema.test.ts +1 -68
  17. package/src/__tests__/config-watcher.test.ts +8 -0
  18. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  19. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  20. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  21. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  22. package/src/__tests__/credential-security-invariants.test.ts +8 -7
  23. package/src/__tests__/credential-vault-unit.test.ts +23 -18
  24. package/src/__tests__/credential-vault.test.ts +30 -18
  25. package/src/__tests__/credentials-cli.test.ts +257 -82
  26. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  27. package/src/__tests__/date-context.test.ts +93 -77
  28. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  29. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  30. package/src/__tests__/history-repair.test.ts +245 -0
  31. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  32. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  33. package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
  34. package/src/__tests__/integration-status.test.ts +31 -30
  35. package/src/__tests__/invite-redemption-service.test.ts +166 -13
  36. package/src/__tests__/invite-routes-http.test.ts +166 -5
  37. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  38. package/src/__tests__/list-messages-attachments.test.ts +193 -0
  39. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  40. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  41. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  42. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  43. package/src/__tests__/memory-regressions.test.ts +477 -2841
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  45. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  46. package/src/__tests__/mime-builder.test.ts +28 -0
  47. package/src/__tests__/native-web-search.test.ts +1 -0
  48. package/src/__tests__/oauth-cli.test.ts +824 -31
  49. package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
  50. package/src/__tests__/oauth-store.test.ts +363 -17
  51. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  52. package/src/__tests__/registry.test.ts +0 -1
  53. package/src/__tests__/relay-server.test.ts +55 -1
  54. package/src/__tests__/schedule-tools.test.ts +32 -0
  55. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  56. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  57. package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
  58. package/src/__tests__/secure-keys.test.ts +78 -18
  59. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  60. package/src/__tests__/server-history-render.test.ts +2 -2
  61. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  62. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  63. package/src/__tests__/session-agent-loop.test.ts +19 -15
  64. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  65. package/src/__tests__/session-error.test.ts +124 -2
  66. package/src/__tests__/session-history-web-search.test.ts +918 -0
  67. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  68. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  69. package/src/__tests__/session-queue.test.ts +37 -27
  70. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  71. package/src/__tests__/session-slash-known.test.ts +1 -15
  72. package/src/__tests__/session-slash-queue.test.ts +1 -15
  73. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  74. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  75. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  76. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  77. package/src/__tests__/skills-install-extract.test.ts +93 -0
  78. package/src/__tests__/skills.test.ts +2 -2
  79. package/src/__tests__/skillssh-registry.test.ts +451 -0
  80. package/src/__tests__/slack-channel-config.test.ts +10 -8
  81. package/src/__tests__/trust-store.test.ts +15 -0
  82. package/src/__tests__/twilio-config.test.ts +11 -10
  83. package/src/__tests__/twilio-provider.test.ts +9 -4
  84. package/src/__tests__/voice-invite-redemption.test.ts +85 -5
  85. package/src/agent/ax-tree-compaction.test.ts +51 -0
  86. package/src/agent/loop.ts +39 -12
  87. package/src/approvals/AGENTS.md +1 -1
  88. package/src/approvals/guardian-request-resolvers.ts +14 -2
  89. package/src/bundler/compiler-tools.ts +66 -2
  90. package/src/calls/call-domain.ts +134 -3
  91. package/src/calls/call-store.ts +6 -0
  92. package/src/calls/relay-server.ts +44 -6
  93. package/src/calls/relay-setup-router.ts +17 -1
  94. package/src/calls/twilio-config.ts +5 -4
  95. package/src/calls/twilio-provider.ts +14 -9
  96. package/src/calls/twilio-rest.ts +10 -7
  97. package/src/calls/types.ts +3 -1
  98. package/src/cli/commands/config.ts +14 -9
  99. package/src/cli/commands/contacts.ts +3 -0
  100. package/src/cli/commands/credentials.ts +170 -174
  101. package/src/cli/commands/doctor.ts +11 -8
  102. package/src/cli/commands/keys.ts +9 -9
  103. package/src/cli/commands/mcp.ts +46 -59
  104. package/src/cli/commands/memory.ts +16 -165
  105. package/src/cli/commands/oauth/apps.ts +68 -10
  106. package/src/cli/commands/oauth/connections.ts +475 -105
  107. package/src/cli/commands/oauth/index.ts +3 -3
  108. package/src/cli/commands/oauth/providers.ts +18 -4
  109. package/src/cli/commands/sessions.ts +5 -2
  110. package/src/cli/commands/skills.ts +173 -1
  111. package/src/cli/http-client.ts +0 -20
  112. package/src/cli/main-screen.tsx +2 -2
  113. package/src/cli/program.ts +5 -6
  114. package/src/cli.ts +20 -22
  115. package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
  116. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  117. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +35 -11
  119. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  120. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  121. package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
  135. package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
  136. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
  137. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
  138. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
  139. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
  140. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
  141. package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
  142. package/src/config/bundled-skills/messaging/SKILL.md +1 -1
  143. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  144. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  145. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
  146. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
  147. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
  148. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
  149. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
  150. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
  151. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
  152. package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
  153. package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
  154. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
  155. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
  156. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
  157. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
  158. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
  159. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
  160. package/src/config/bundled-tool-registry.ts +2 -5
  161. package/src/config/loader.ts +6 -42
  162. package/src/config/schema.ts +1 -12
  163. package/src/config/schemas/memory-lifecycle.ts +0 -9
  164. package/src/config/schemas/memory-processing.ts +0 -180
  165. package/src/config/schemas/memory-retrieval.ts +32 -104
  166. package/src/config/schemas/memory.ts +0 -10
  167. package/src/config/types.ts +0 -4
  168. package/src/contacts/contact-store.ts +39 -2
  169. package/src/contacts/contacts-write.ts +9 -0
  170. package/src/context/window-manager.ts +4 -1
  171. package/src/daemon/config-watcher.ts +55 -2
  172. package/src/daemon/daemon-control.ts +1 -1
  173. package/src/daemon/date-context.ts +114 -31
  174. package/src/daemon/handlers/config-ingress.ts +2 -2
  175. package/src/daemon/handlers/config-slack-channel.ts +59 -39
  176. package/src/daemon/handlers/config-telegram.ts +23 -14
  177. package/src/daemon/handlers/session-history.ts +1 -358
  178. package/src/daemon/handlers/sessions.ts +18 -13
  179. package/src/daemon/handlers/shared.ts +3 -17
  180. package/src/daemon/handlers/skills.ts +20 -1
  181. package/src/daemon/history-repair.ts +72 -8
  182. package/src/daemon/host-cu-proxy.ts +55 -26
  183. package/src/daemon/lifecycle.ts +39 -4
  184. package/src/daemon/mcp-reload-service.ts +2 -2
  185. package/src/daemon/message-types/computer-use.ts +1 -12
  186. package/src/daemon/message-types/memory.ts +4 -16
  187. package/src/daemon/message-types/messages.ts +1 -0
  188. package/src/daemon/message-types/sessions.ts +4 -42
  189. package/src/daemon/server.ts +6 -1
  190. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  191. package/src/daemon/session-agent-loop.ts +334 -48
  192. package/src/daemon/session-error.ts +89 -6
  193. package/src/daemon/session-history.ts +17 -7
  194. package/src/daemon/session-media-retry.ts +6 -2
  195. package/src/daemon/session-memory.ts +69 -149
  196. package/src/daemon/session-process.ts +10 -1
  197. package/src/daemon/session-runtime-assembly.ts +49 -19
  198. package/src/daemon/session-slash.ts +3 -5
  199. package/src/daemon/session-surfaces.ts +4 -1
  200. package/src/daemon/session-tool-setup.ts +7 -1
  201. package/src/daemon/session.ts +12 -2
  202. package/src/email/providers/index.ts +2 -2
  203. package/src/instrument.ts +61 -1
  204. package/src/media/avatar-router.ts +1 -1
  205. package/src/memory/admin.ts +2 -191
  206. package/src/memory/canonical-guardian-store.ts +38 -2
  207. package/src/memory/conversation-crud.ts +0 -33
  208. package/src/memory/conversation-queries.ts +25 -83
  209. package/src/memory/db-init.ts +32 -0
  210. package/src/memory/embedding-backend.ts +84 -8
  211. package/src/memory/embedding-types.ts +9 -1
  212. package/src/memory/indexer.ts +7 -46
  213. package/src/memory/invite-store.ts +19 -0
  214. package/src/memory/items-extractor.ts +274 -76
  215. package/src/memory/job-handlers/backfill.ts +2 -127
  216. package/src/memory/job-handlers/cleanup.ts +2 -16
  217. package/src/memory/job-handlers/extraction.ts +2 -138
  218. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  219. package/src/memory/job-handlers/summarization.ts +3 -148
  220. package/src/memory/job-utils.ts +21 -59
  221. package/src/memory/jobs-store.ts +1 -159
  222. package/src/memory/jobs-worker.ts +9 -52
  223. package/src/memory/migrations/104-core-indexes.ts +3 -3
  224. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  225. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  226. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  227. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  228. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  229. package/src/memory/migrations/154-drop-fts.ts +20 -0
  230. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  231. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  232. package/src/memory/migrations/157-invite-contact-id.ts +104 -0
  233. package/src/memory/migrations/index.ts +8 -0
  234. package/src/memory/migrations/registry.ts +6 -0
  235. package/src/memory/qdrant-client.ts +148 -51
  236. package/src/memory/raw-query.ts +1 -1
  237. package/src/memory/retriever.test.ts +294 -273
  238. package/src/memory/retriever.ts +421 -645
  239. package/src/memory/schema/calls.ts +2 -0
  240. package/src/memory/schema/contacts.ts +1 -0
  241. package/src/memory/schema/memory-core.ts +3 -48
  242. package/src/memory/schema/oauth.ts +2 -0
  243. package/src/memory/search/formatting.ts +263 -176
  244. package/src/memory/search/lexical.ts +1 -254
  245. package/src/memory/search/ranking.ts +0 -455
  246. package/src/memory/search/semantic.ts +100 -14
  247. package/src/memory/search/staleness.ts +47 -0
  248. package/src/memory/search/tier-classifier.ts +21 -0
  249. package/src/memory/search/types.ts +15 -77
  250. package/src/memory/task-memory-cleanup.ts +4 -6
  251. package/src/messaging/provider.ts +1 -1
  252. package/src/messaging/providers/gmail/adapter.ts +1 -1
  253. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  254. package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
  255. package/src/messaging/providers/whatsapp/adapter.ts +13 -9
  256. package/src/messaging/registry.ts +9 -5
  257. package/src/oauth/byo-connection.test.ts +40 -25
  258. package/src/oauth/connect-orchestrator.ts +4 -10
  259. package/src/oauth/connection-resolver.ts +20 -6
  260. package/src/oauth/manual-token-connection.ts +5 -5
  261. package/src/oauth/oauth-store.ts +183 -31
  262. package/src/oauth/platform-connection.test.ts +1 -1
  263. package/src/oauth/provider-behaviors.ts +503 -4
  264. package/src/oauth/seed-providers.ts +214 -8
  265. package/src/oauth/token-persistence.ts +31 -16
  266. package/src/permissions/defaults.ts +1 -0
  267. package/src/permissions/trust-store.ts +23 -1
  268. package/src/playbooks/playbook-compiler.ts +1 -1
  269. package/src/prompts/system-prompt.ts +18 -2
  270. package/src/providers/anthropic/client.ts +56 -126
  271. package/src/providers/types.ts +7 -1
  272. package/src/runtime/AGENTS.md +9 -0
  273. package/src/runtime/auth/route-policy.ts +6 -3
  274. package/src/runtime/channel-readiness-service.ts +48 -40
  275. package/src/runtime/guardian-reply-router.ts +24 -22
  276. package/src/runtime/http-server.ts +2 -2
  277. package/src/runtime/http-types.ts +2 -0
  278. package/src/runtime/invite-redemption-service.ts +72 -12
  279. package/src/runtime/invite-service.ts +43 -0
  280. package/src/runtime/middleware/twilio-validation.ts +1 -1
  281. package/src/runtime/pending-interactions.ts +2 -2
  282. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  283. package/src/runtime/routes/btw-routes.ts +10 -5
  284. package/src/runtime/routes/conversation-routes.ts +56 -11
  285. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  286. package/src/runtime/routes/integrations/slack/channel.ts +2 -2
  287. package/src/runtime/routes/integrations/telegram.ts +2 -2
  288. package/src/runtime/routes/integrations/twilio.ts +17 -17
  289. package/src/runtime/routes/invite-routes.ts +29 -4
  290. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  291. package/src/runtime/routes/memory-item-routes.ts +503 -0
  292. package/src/runtime/routes/secret-routes.ts +17 -0
  293. package/src/runtime/routes/session-management-routes.ts +3 -3
  294. package/src/runtime/routes/settings-routes.ts +3 -3
  295. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  296. package/src/runtime/routes/workspace-routes.ts +9 -4
  297. package/src/runtime/routes/workspace-utils.ts +8 -2
  298. package/src/schedule/integration-status.ts +26 -19
  299. package/src/security/keychain-broker-client.ts +17 -4
  300. package/src/security/oauth2.ts +6 -7
  301. package/src/security/secure-keys.ts +44 -19
  302. package/src/security/token-manager.ts +46 -39
  303. package/src/services/vercel-deploy.ts +0 -24
  304. package/src/signals/confirm.ts +78 -0
  305. package/src/signals/mcp-reload.ts +18 -0
  306. package/src/skills/catalog-install.ts +74 -18
  307. package/src/skills/skillssh-registry.ts +503 -0
  308. package/src/tools/assets/search.ts +5 -1
  309. package/src/tools/computer-use/definitions.ts +0 -10
  310. package/src/tools/computer-use/registry.ts +1 -1
  311. package/src/tools/credentials/vault.ts +22 -7
  312. package/src/tools/memory/definitions.ts +4 -13
  313. package/src/tools/memory/handlers.test.ts +83 -103
  314. package/src/tools/memory/handlers.ts +50 -85
  315. package/src/tools/network/script-proxy/session-manager.ts +8 -8
  316. package/src/tools/schedule/create.ts +10 -3
  317. package/src/tools/schedule/update.ts +8 -1
  318. package/src/tools/skills/load.ts +25 -2
  319. package/src/watcher/provider-types.ts +1 -1
  320. package/src/watcher/providers/github.ts +1 -1
  321. package/src/watcher/providers/gmail.ts +3 -3
  322. package/src/watcher/providers/google-calendar.ts +3 -3
  323. package/src/watcher/providers/linear.ts +1 -1
  324. package/src/__tests__/clarification-resolver.test.ts +0 -193
  325. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  326. package/src/__tests__/conflict-policy.test.ts +0 -269
  327. package/src/__tests__/conflict-store.test.ts +0 -372
  328. package/src/__tests__/contradiction-checker.test.ts +0 -361
  329. package/src/__tests__/entity-extractor.test.ts +0 -211
  330. package/src/__tests__/entity-search.test.ts +0 -1117
  331. package/src/__tests__/profile-compiler.test.ts +0 -392
  332. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  333. package/src/__tests__/session-profile-injection.test.ts +0 -557
  334. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  335. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  336. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  337. package/src/daemon/session-conflict-gate.ts +0 -167
  338. package/src/daemon/session-dynamic-profile.ts +0 -77
  339. package/src/memory/clarification-resolver.ts +0 -417
  340. package/src/memory/conflict-intent.ts +0 -205
  341. package/src/memory/conflict-policy.ts +0 -127
  342. package/src/memory/conflict-store.ts +0 -410
  343. package/src/memory/contradiction-checker.ts +0 -508
  344. package/src/memory/entity-extractor.ts +0 -535
  345. package/src/memory/format-recall.ts +0 -47
  346. package/src/memory/fts-reconciler.ts +0 -165
  347. package/src/memory/job-handlers/conflict.ts +0 -200
  348. package/src/memory/profile-compiler.ts +0 -195
  349. package/src/memory/recall-cache.ts +0 -117
  350. package/src/memory/search/entity.ts +0 -535
  351. package/src/memory/search/query-expansion.test.ts +0 -70
  352. package/src/memory/search/query-expansion.ts +0 -118
  353. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -23,7 +23,11 @@ mock.module("../util/logger.js", () => ({
23
23
  }),
24
24
  }));
25
25
 
26
- import { findContactChannel } from "../contacts/contact-store.js";
26
+ import {
27
+ findContactChannel,
28
+ getContact,
29
+ upsertContact,
30
+ } from "../contacts/contact-store.js";
27
31
  import { upsertContactChannel } from "../contacts/contacts-write.js";
28
32
  import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
29
33
  import {
@@ -54,12 +58,19 @@ function resetTables() {
54
58
  getSqlite().run("DELETE FROM contacts");
55
59
  }
56
60
 
61
+ /** Create a throwaway contact and return its ID, for use as the invite's contactId. */
62
+ function createTargetContact(displayName = "Target Contact"): string {
63
+ return upsertContact({ displayName, role: "contact" }).id;
64
+ }
65
+
57
66
  describe("invite-redemption-service", () => {
58
67
  beforeEach(resetTables);
59
68
 
60
69
  test("redeems a valid invite and returns typed outcome", () => {
70
+ const targetContactId = createTargetContact();
61
71
  const { rawToken, invite } = createInvite({
62
72
  sourceChannel: "telegram",
73
+ contactId: targetContactId,
63
74
  maxUses: 1,
64
75
  });
65
76
 
@@ -79,8 +90,10 @@ describe("invite-redemption-service", () => {
79
90
  });
80
91
 
81
92
  test("marks channel as verified via invite on redemption", () => {
93
+ const targetContactId = createTargetContact();
82
94
  const { rawToken } = createInvite({
83
95
  sourceChannel: "telegram",
96
+ contactId: targetContactId,
84
97
  maxUses: 1,
85
98
  });
86
99
 
@@ -104,9 +117,11 @@ describe("invite-redemption-service", () => {
104
117
  });
105
118
 
106
119
  test("marks channel as verified via invite on 6-digit code redemption", () => {
120
+ const targetContactId = createTargetContact();
107
121
  const inviteCode = "123456";
108
122
  createInvite({
109
123
  sourceChannel: "telegram",
124
+ contactId: targetContactId,
110
125
  maxUses: 1,
111
126
  inviteCodeHash: hashVoiceCode(inviteCode),
112
127
  });
@@ -141,9 +156,11 @@ describe("invite-redemption-service", () => {
141
156
  });
142
157
 
143
158
  test("returns expired for an expired invite", () => {
159
+ const targetContactId = createTargetContact();
144
160
  // Create an invite that expired 1 ms ago
145
161
  const { rawToken } = createInvite({
146
162
  sourceChannel: "telegram",
163
+ contactId: targetContactId,
147
164
  maxUses: 1,
148
165
  expiresInMs: -1,
149
166
  });
@@ -158,8 +175,10 @@ describe("invite-redemption-service", () => {
158
175
  });
159
176
 
160
177
  test("returns revoked for a revoked invite", () => {
178
+ const targetContactId = createTargetContact();
161
179
  const { rawToken, invite } = createInvite({
162
180
  sourceChannel: "telegram",
181
+ contactId: targetContactId,
163
182
  maxUses: 1,
164
183
  });
165
184
  revokeStoreFn(invite.id);
@@ -174,8 +193,10 @@ describe("invite-redemption-service", () => {
174
193
  });
175
194
 
176
195
  test("returns max_uses_reached when invite is fully consumed", () => {
196
+ const targetContactId = createTargetContact();
177
197
  const { rawToken } = createInvite({
178
198
  sourceChannel: "telegram",
199
+ contactId: targetContactId,
179
200
  maxUses: 1,
180
201
  });
181
202
 
@@ -198,8 +219,10 @@ describe("invite-redemption-service", () => {
198
219
  });
199
220
 
200
221
  test("returns channel_mismatch when redeeming on wrong channel", () => {
222
+ const targetContactId = createTargetContact();
201
223
  const { rawToken } = createInvite({
202
224
  sourceChannel: "telegram",
225
+ contactId: targetContactId,
203
226
  maxUses: 1,
204
227
  });
205
228
 
@@ -213,8 +236,10 @@ describe("invite-redemption-service", () => {
213
236
  });
214
237
 
215
238
  test("returns missing_identity when no externalUserId or externalChatId", () => {
239
+ const targetContactId = createTargetContact();
216
240
  const { rawToken } = createInvite({
217
241
  sourceChannel: "telegram",
242
+ contactId: targetContactId,
218
243
  maxUses: 1,
219
244
  });
220
245
 
@@ -227,16 +252,18 @@ describe("invite-redemption-service", () => {
227
252
  });
228
253
 
229
254
  test("returns already_member when user is already an active member", () => {
230
- const { rawToken } = createInvite({
255
+ // Pre-create an active member and find their contact
256
+ const member = upsertContactChannel({
231
257
  sourceChannel: "telegram",
232
- maxUses: 5,
258
+ externalUserId: "existing-user",
259
+ status: "active",
233
260
  });
234
261
 
235
- // Pre-create an active member
236
- upsertContactChannel({
262
+ // Create an invite targeting the same contact that owns the channel
263
+ const { rawToken } = createInvite({
237
264
  sourceChannel: "telegram",
238
- externalUserId: "existing-user",
239
- status: "active",
265
+ contactId: member!.contact.id,
266
+ maxUses: 5,
240
267
  });
241
268
 
242
269
  const outcome = redeemInvite({
@@ -257,30 +284,145 @@ describe("invite-redemption-service", () => {
257
284
  });
258
285
 
259
286
  test("returns invalid_token for a blocked member to avoid leaking membership status", () => {
287
+ // Pre-create a blocked member and find their contact
288
+ const member = upsertContactChannel({
289
+ sourceChannel: "telegram",
290
+ externalUserId: "blocked-user",
291
+ status: "blocked",
292
+ });
293
+
294
+ // Create an invite targeting the same contact that owns the channel
260
295
  const { rawToken } = createInvite({
261
296
  sourceChannel: "telegram",
297
+ contactId: member!.contact.id,
262
298
  maxUses: 5,
263
299
  });
264
300
 
265
- // Pre-create a blocked member — simulates a guardian-initiated block
266
- upsertContactChannel({
301
+ const outcome = redeemInvite({
302
+ rawToken,
267
303
  sourceChannel: "telegram",
268
304
  externalUserId: "blocked-user",
269
- status: "blocked",
270
305
  });
271
306
 
307
+ expect(outcome).toEqual({ ok: false, reason: "invalid_token" });
308
+ });
309
+
310
+ test("binds redeemer to the invite's target contact, not the guardian", () => {
311
+ // Pre-create a guardian contact with a revoked telegram channel
312
+ const guardianContact = upsertContact({
313
+ displayName: "Guardian",
314
+ role: "guardian",
315
+ channels: [
316
+ {
317
+ type: "telegram",
318
+ address: "guardian-tg-id",
319
+ externalUserId: "guardian-tg-id",
320
+ status: "revoked",
321
+ },
322
+ ],
323
+ });
324
+
325
+ // Create a separate target contact "Mom"
326
+ const momContact = upsertContact({
327
+ displayName: "Mom",
328
+ role: "contact",
329
+ });
330
+
331
+ // Create an invite targeting Mom's contact
332
+ const { rawToken } = createInvite({
333
+ sourceChannel: "telegram",
334
+ contactId: momContact.id,
335
+ maxUses: 5,
336
+ });
337
+
338
+ // Redeem using the guardian's Telegram identity
272
339
  const outcome = redeemInvite({
273
340
  rawToken,
274
341
  sourceChannel: "telegram",
275
- externalUserId: "blocked-user",
342
+ externalUserId: "guardian-tg-id",
276
343
  });
277
344
 
278
- expect(outcome).toEqual({ ok: false, reason: "invalid_token" });
345
+ // Should succeed redeemer's channel is bound to Mom
346
+ expect(outcome.ok).toBe(true);
347
+ expect((outcome as { type: string }).type).toBe("redeemed");
348
+
349
+ // Verify the redeemer's Telegram ID is now bound to Mom's contact
350
+ const result = findContactChannel({
351
+ channelType: "telegram",
352
+ externalUserId: "guardian-tg-id",
353
+ });
354
+ expect(result).not.toBeNull();
355
+ expect(result!.contact.id).toBe(momContact.id);
356
+ expect(result!.channel.status).toBe("active");
357
+
358
+ // Verify the original guardian contact was NOT modified
359
+ const guardian = getContact(guardianContact.id);
360
+ expect(guardian).not.toBeNull();
361
+ expect(guardian!.role).toBe("guardian");
362
+ });
363
+
364
+ test("binds redeemer to the invite's target contact via 6-digit code, not the guardian", () => {
365
+ // Pre-create a guardian contact with a revoked telegram channel
366
+ const guardianContact = upsertContact({
367
+ displayName: "Guardian",
368
+ role: "guardian",
369
+ channels: [
370
+ {
371
+ type: "telegram",
372
+ address: "guardian-code-id",
373
+ externalUserId: "guardian-code-id",
374
+ status: "revoked",
375
+ },
376
+ ],
377
+ });
378
+
379
+ // Create a separate target contact "Mom"
380
+ const momContact = upsertContact({
381
+ displayName: "Mom",
382
+ role: "contact",
383
+ });
384
+
385
+ // Create an invite targeting Mom's contact with a 6-digit code
386
+ const code = "123456";
387
+ const inviteCodeHash = hashVoiceCode(code);
388
+ createInvite({
389
+ sourceChannel: "telegram",
390
+ contactId: momContact.id,
391
+ maxUses: 5,
392
+ inviteCodeHash,
393
+ });
394
+
395
+ // Redeem using the guardian's Telegram identity
396
+ const outcome = redeemInviteByCode({
397
+ code,
398
+ sourceChannel: "telegram",
399
+ externalUserId: "guardian-code-id",
400
+ });
401
+
402
+ // Should succeed — redeemer's channel is bound to Mom
403
+ expect(outcome.ok).toBe(true);
404
+ expect((outcome as { type: string }).type).toBe("redeemed");
405
+
406
+ // Verify the redeemer's Telegram ID is now bound to Mom's contact
407
+ const result = findContactChannel({
408
+ channelType: "telegram",
409
+ externalUserId: "guardian-code-id",
410
+ });
411
+ expect(result).not.toBeNull();
412
+ expect(result!.contact.id).toBe(momContact.id);
413
+ expect(result!.channel.status).toBe("active");
414
+
415
+ // Verify the original guardian contact was NOT modified
416
+ const guardian = getContact(guardianContact.id);
417
+ expect(guardian).not.toBeNull();
418
+ expect(guardian!.role).toBe("guardian");
279
419
  });
280
420
 
281
421
  test("does not return already_member for a revoked member", () => {
422
+ const targetContactId = createTargetContact();
282
423
  const { rawToken } = createInvite({
283
424
  sourceChannel: "telegram",
425
+ contactId: targetContactId,
284
426
  maxUses: 5,
285
427
  });
286
428
 
@@ -306,8 +448,10 @@ describe("invite-redemption-service", () => {
306
448
  });
307
449
 
308
450
  test("raw token is not present in the outcome object", () => {
451
+ const targetContactId = createTargetContact();
309
452
  const { rawToken } = createInvite({
310
453
  sourceChannel: "telegram",
454
+ contactId: targetContactId,
311
455
  maxUses: 1,
312
456
  });
313
457
 
@@ -323,7 +467,12 @@ describe("invite-redemption-service", () => {
323
467
  });
324
468
 
325
469
  test("channel enforcement blocks cross-channel redemption (voice invite via slack)", () => {
326
- const { rawToken } = createInvite({ sourceChannel: "phone", maxUses: 1 });
470
+ const targetContactId = createTargetContact();
471
+ const { rawToken } = createInvite({
472
+ sourceChannel: "phone",
473
+ contactId: targetContactId,
474
+ maxUses: 1,
475
+ });
327
476
 
328
477
  const outcome = redeemInvite({
329
478
  rawToken,
@@ -353,9 +502,11 @@ describe("invite-redemption-service", () => {
353
502
  });
354
503
 
355
504
  test("returns expired for an active member with an expired invite token", () => {
505
+ const targetContactId = createTargetContact();
356
506
  // Create an expired invite
357
507
  const { rawToken } = createInvite({
358
508
  sourceChannel: "telegram",
509
+ contactId: targetContactId,
359
510
  maxUses: 5,
360
511
  expiresInMs: -1,
361
512
  });
@@ -378,9 +529,11 @@ describe("invite-redemption-service", () => {
378
529
  });
379
530
 
380
531
  test("returns channel_mismatch for an active member with a valid token for a different channel", () => {
532
+ const targetContactId = createTargetContact();
381
533
  // Create an invite for voice
382
534
  const { rawToken } = createInvite({
383
535
  sourceChannel: "phone",
536
+ contactId: targetContactId,
384
537
  maxUses: 5,
385
538
  });
386
539
 
@@ -41,16 +41,34 @@ mock.module("../telegram/bot-username.js", () => ({
41
41
  getTelegramBotUsername: () => mockTelegramBotUsername,
42
42
  }));
43
43
 
44
+ // Mock startInviteCall from call-domain — test env lacks Twilio credentials.
45
+ let mockStartInviteCallResult:
46
+ | { ok: true; callSid: string }
47
+ | { ok: false; error: string; status?: number } = {
48
+ ok: true,
49
+ callSid: "CA_test_sid_123",
50
+ };
51
+ mock.module("../calls/call-domain.js", () => ({
52
+ startInviteCall: async () => mockStartInviteCallResult,
53
+ }));
54
+
55
+ import { upsertContact } from "../contacts/contact-store.js";
44
56
  import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
45
57
  import {
46
58
  handleCreateInvite,
47
59
  handleListInvites,
48
60
  handleRedeemInvite,
49
61
  handleRevokeInvite,
62
+ handleTriggerInviteCall,
50
63
  } from "../runtime/routes/invite-routes.js";
51
64
 
52
65
  initializeDb();
53
66
 
67
+ /** Create a throwaway contact and return its ID, for use as the invite's contactId. */
68
+ function createTargetContact(displayName = "Test Contact"): string {
69
+ return upsertContact({ displayName, role: "contact" }).id;
70
+ }
71
+
54
72
  afterAll(() => {
55
73
  resetDb();
56
74
  try {
@@ -79,6 +97,7 @@ describe("ingress invite HTTP routes", () => {
79
97
  headers: { "Content-Type": "application/json" },
80
98
  body: JSON.stringify({
81
99
  sourceChannel: "telegram",
100
+ contactId: createTargetContact(),
82
101
  note: "Test invite",
83
102
  maxUses: 5,
84
103
  }),
@@ -108,6 +127,7 @@ describe("ingress invite HTTP routes", () => {
108
127
  headers: { "Content-Type": "application/json" },
109
128
  body: JSON.stringify({
110
129
  sourceChannel: "telegram",
130
+ contactId: createTargetContact(),
111
131
  note: "Share link test",
112
132
  }),
113
133
  });
@@ -151,14 +171,20 @@ describe("ingress invite HTTP routes", () => {
151
171
  new Request("http://localhost/v1/contacts/invites", {
152
172
  method: "POST",
153
173
  headers: { "Content-Type": "application/json" },
154
- body: JSON.stringify({ sourceChannel: "telegram" }),
174
+ body: JSON.stringify({
175
+ sourceChannel: "telegram",
176
+ contactId: createTargetContact(),
177
+ }),
155
178
  }),
156
179
  );
157
180
  await handleCreateInvite(
158
181
  new Request("http://localhost/v1/contacts/invites", {
159
182
  method: "POST",
160
183
  headers: { "Content-Type": "application/json" },
161
- body: JSON.stringify({ sourceChannel: "telegram" }),
184
+ body: JSON.stringify({
185
+ sourceChannel: "telegram",
186
+ contactId: createTargetContact(),
187
+ }),
162
188
  }),
163
189
  );
164
190
 
@@ -177,7 +203,10 @@ describe("ingress invite HTTP routes", () => {
177
203
  new Request("http://localhost/v1/contacts/invites", {
178
204
  method: "POST",
179
205
  headers: { "Content-Type": "application/json" },
180
- body: JSON.stringify({ sourceChannel: "telegram" }),
206
+ body: JSON.stringify({
207
+ sourceChannel: "telegram",
208
+ contactId: createTargetContact(),
209
+ }),
181
210
  }),
182
211
  );
183
212
  const created = (await createRes.json()) as { invite: { id: string } };
@@ -202,7 +231,11 @@ describe("ingress invite HTTP routes", () => {
202
231
  new Request("http://localhost/v1/contacts/invites", {
203
232
  method: "POST",
204
233
  headers: { "Content-Type": "application/json" },
205
- body: JSON.stringify({ sourceChannel: "telegram", maxUses: 1 }),
234
+ body: JSON.stringify({
235
+ sourceChannel: "telegram",
236
+ contactId: createTargetContact(),
237
+ maxUses: 1,
238
+ }),
206
239
  }),
207
240
  );
208
241
  const created = (await createRes.json()) as { invite: { token: string } };
@@ -270,7 +303,10 @@ describe("ingress service shared logic", () => {
270
303
  new Request("http://localhost/v1/contacts/invites", {
271
304
  method: "POST",
272
305
  headers: { "Content-Type": "application/json" },
273
- body: JSON.stringify({ sourceChannel: "telegram" }),
306
+ body: JSON.stringify({
307
+ sourceChannel: "telegram",
308
+ contactId: createTargetContact(),
309
+ }),
274
310
  }),
275
311
  );
276
312
  const created = (await createRes.json()) as {
@@ -300,6 +336,7 @@ describe("voice invite HTTP routes", () => {
300
336
  headers: { "Content-Type": "application/json" },
301
337
  body: JSON.stringify({
302
338
  sourceChannel: "phone",
339
+ contactId: createTargetContact(),
303
340
  expectedExternalUserId: "+15551234567",
304
341
  friendName: "Alice",
305
342
  guardianName: "Bob",
@@ -336,6 +373,7 @@ describe("voice invite HTTP routes", () => {
336
373
  headers: { "Content-Type": "application/json" },
337
374
  body: JSON.stringify({
338
375
  sourceChannel: "phone",
376
+ contactId: createTargetContact(),
339
377
  friendName: "Alice",
340
378
  guardianName: "Bob",
341
379
  }),
@@ -355,6 +393,7 @@ describe("voice invite HTTP routes", () => {
355
393
  headers: { "Content-Type": "application/json" },
356
394
  body: JSON.stringify({
357
395
  sourceChannel: "phone",
396
+ contactId: createTargetContact(),
358
397
  expectedExternalUserId: "not-a-phone-number",
359
398
  friendName: "Alice",
360
399
  guardianName: "Bob",
@@ -375,6 +414,7 @@ describe("voice invite HTTP routes", () => {
375
414
  headers: { "Content-Type": "application/json" },
376
415
  body: JSON.stringify({
377
416
  sourceChannel: "phone",
417
+ contactId: createTargetContact(),
378
418
  expectedExternalUserId: "+15551234567",
379
419
  guardianName: "Bob",
380
420
  }),
@@ -394,6 +434,7 @@ describe("voice invite HTTP routes", () => {
394
434
  headers: { "Content-Type": "application/json" },
395
435
  body: JSON.stringify({
396
436
  sourceChannel: "phone",
437
+ contactId: createTargetContact(),
397
438
  expectedExternalUserId: "+15551234567",
398
439
  friendName: "Alice",
399
440
  }),
@@ -413,6 +454,7 @@ describe("voice invite HTTP routes", () => {
413
454
  headers: { "Content-Type": "application/json" },
414
455
  body: JSON.stringify({
415
456
  sourceChannel: "phone",
457
+ contactId: createTargetContact(),
416
458
  expectedExternalUserId: "+15551234567",
417
459
  friendName: "Alice",
418
460
  guardianName: "Bob",
@@ -436,6 +478,7 @@ describe("voice invite HTTP routes", () => {
436
478
  headers: { "Content-Type": "application/json" },
437
479
  body: JSON.stringify({
438
480
  sourceChannel: "phone",
481
+ contactId: createTargetContact(),
439
482
  expectedExternalUserId: "+15551234567",
440
483
  friendName: "Alice",
441
484
  guardianName: "Bob",
@@ -460,6 +503,7 @@ describe("voice invite HTTP routes", () => {
460
503
  headers: { "Content-Type": "application/json" },
461
504
  body: JSON.stringify({
462
505
  sourceChannel: "phone",
506
+ contactId: createTargetContact(),
463
507
  expectedExternalUserId: "+15551234567",
464
508
  friendName: "Alice",
465
509
  guardianName: "Bob",
@@ -517,6 +561,7 @@ describe("voice invite HTTP routes", () => {
517
561
  headers: { "Content-Type": "application/json" },
518
562
  body: JSON.stringify({
519
563
  sourceChannel: "phone",
564
+ contactId: createTargetContact(),
520
565
  expectedExternalUserId: "+15551234567",
521
566
  friendName: "Alice",
522
567
  guardianName: "Bob",
@@ -540,4 +585,120 @@ describe("voice invite HTTP routes", () => {
540
585
  expect(res.status).toBe(400);
541
586
  expect(body.ok).toBe(false);
542
587
  });
588
+
589
+ test("voice invite creation returns guardianInstruction with friend name", async () => {
590
+ const req = new Request("http://localhost/v1/contacts/invites", {
591
+ method: "POST",
592
+ headers: { "Content-Type": "application/json" },
593
+ body: JSON.stringify({
594
+ sourceChannel: "phone",
595
+ contactId: createTargetContact(),
596
+ expectedExternalUserId: "+15551234567",
597
+ friendName: "Alice",
598
+ guardianName: "Bob",
599
+ }),
600
+ });
601
+
602
+ const res = await handleCreateInvite(req);
603
+ const body = (await res.json()) as Record<string, unknown>;
604
+
605
+ expect(res.status).toBe(201);
606
+ expect(body.ok).toBe(true);
607
+ const invite = body.invite as Record<string, unknown>;
608
+ expect(invite.guardianInstruction).toBe(
609
+ "Alice will need this code when they answer. Share it with them first.",
610
+ );
611
+ });
612
+ });
613
+
614
+ // ---------------------------------------------------------------------------
615
+ // Trigger invite call endpoint
616
+ // ---------------------------------------------------------------------------
617
+
618
+ describe("POST /v1/contacts/invites/:id/call", () => {
619
+ beforeEach(() => {
620
+ resetTables();
621
+ mockStartInviteCallResult = { ok: true, callSid: "CA_test_sid_123" };
622
+ });
623
+
624
+ test("triggers a call for an active phone invite", async () => {
625
+ const createRes = await handleCreateInvite(
626
+ new Request("http://localhost/v1/contacts/invites", {
627
+ method: "POST",
628
+ headers: { "Content-Type": "application/json" },
629
+ body: JSON.stringify({
630
+ sourceChannel: "phone",
631
+ contactId: createTargetContact(),
632
+ expectedExternalUserId: "+15551234567",
633
+ friendName: "Alice",
634
+ guardianName: "Bob",
635
+ }),
636
+ }),
637
+ );
638
+ const created = (await createRes.json()) as { invite: { id: string } };
639
+
640
+ const res = await handleTriggerInviteCall(created.invite.id);
641
+ const body = (await res.json()) as Record<string, unknown>;
642
+
643
+ expect(res.status).toBe(200);
644
+ expect(body.ok).toBe(true);
645
+ expect(body.callSid).toBe("CA_test_sid_123");
646
+ });
647
+
648
+ test("returns 400 for non-existent invite", async () => {
649
+ const res = await handleTriggerInviteCall("nonexistent-id");
650
+ const body = (await res.json()) as Record<string, unknown>;
651
+
652
+ expect(res.status).toBe(400);
653
+ expect(body.ok).toBe(false);
654
+ expect(body.error).toBe("Invite not found");
655
+ });
656
+
657
+ test("returns 400 for a revoked (non-active) invite", async () => {
658
+ const createRes = await handleCreateInvite(
659
+ new Request("http://localhost/v1/contacts/invites", {
660
+ method: "POST",
661
+ headers: { "Content-Type": "application/json" },
662
+ body: JSON.stringify({
663
+ sourceChannel: "phone",
664
+ contactId: createTargetContact(),
665
+ expectedExternalUserId: "+15551234567",
666
+ friendName: "Alice",
667
+ guardianName: "Bob",
668
+ }),
669
+ }),
670
+ );
671
+ const created = (await createRes.json()) as { invite: { id: string } };
672
+
673
+ // Revoke the invite
674
+ handleRevokeInvite(created.invite.id);
675
+
676
+ const res = await handleTriggerInviteCall(created.invite.id);
677
+ const body = (await res.json()) as Record<string, unknown>;
678
+
679
+ expect(res.status).toBe(400);
680
+ expect(body.ok).toBe(false);
681
+ expect(body.error).toBe("Invite is not active");
682
+ });
683
+
684
+ test("returns 400 for a non-phone invite", async () => {
685
+ const createRes = await handleCreateInvite(
686
+ new Request("http://localhost/v1/contacts/invites", {
687
+ method: "POST",
688
+ headers: { "Content-Type": "application/json" },
689
+ body: JSON.stringify({
690
+ sourceChannel: "telegram",
691
+ contactId: createTargetContact(),
692
+ }),
693
+ }),
694
+ );
695
+ const created = (await createRes.json()) as { invite: { id: string } };
696
+
697
+ const res = await handleTriggerInviteCall(created.invite.id);
698
+ const body = (await res.json()) as Record<string, unknown>;
699
+
700
+ expect(res.status).toBe(400);
701
+ expect(body.ok).toBe(false);
702
+ expect(body.error).toBe("Only phone invites support call triggering");
703
+ });
543
704
  });
@@ -264,7 +264,7 @@ describe("keychain-broker-client", () => {
264
264
 
265
265
  const client = createBrokerClient();
266
266
  const result = await client.set("my-key", "new-value");
267
- expect(result).toBe(true);
267
+ expect(result).toEqual({ status: "ok" });
268
268
  });
269
269
 
270
270
  test("del returns true on success", async () => {
@@ -434,11 +434,11 @@ describe("keychain-broker-client", () => {
434
434
  expect(result).toBeNull();
435
435
  });
436
436
 
437
- test("set returns false when socket file does not exist", async () => {
437
+ test("set returns unreachable when socket file does not exist", async () => {
438
438
  writeFileSync(TOKEN_PATH, TEST_TOKEN);
439
439
  const client = createBrokerClient();
440
440
  const result = await client.set("test-key", "value");
441
- expect(result).toBe(false);
441
+ expect(result).toEqual({ status: "unreachable" });
442
442
  });
443
443
 
444
444
  test("del returns false when socket file does not exist", async () => {
@@ -470,7 +470,7 @@ describe("keychain-broker-client", () => {
470
470
  }
471
471
  const client = createBrokerClient();
472
472
  expect(await client.get("key")).toBeNull();
473
- expect(await client.set("key", "val")).toBe(false);
473
+ expect(await client.set("key", "val")).toEqual({ status: "unreachable" });
474
474
  expect(await client.del("key")).toBe(false);
475
475
  expect(await client.list()).toEqual([]);
476
476
  expect(await client.ping()).toBeNull();