vellum 0.2.1 → 0.2.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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * Tests for RelayConnection — the WebSocket handler for Twilio
3
+ * ConversationRelay protocol.
4
+ *
5
+ * Tests:
6
+ * - Setup message handling (callSid association, event recording, orchestrator creation)
7
+ * - Prompt message handling (final vs partial, routing to orchestrator)
8
+ * - Interrupt handling (abort propagation)
9
+ * - Error handling (event recording)
10
+ * - DTMF handling (event recording)
11
+ * - sendTextToken / endSession (outbound WebSocket messages)
12
+ * - Conversation history tracking
13
+ * - destroy cleanup
14
+ * - Malformed message resilience
15
+ */
16
+ import { describe, test, expect, beforeEach, afterAll, mock, type Mock } from 'bun:test';
17
+ import { mkdtempSync, rmSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ import { EventEmitter } from 'node:events';
21
+
22
+ const testDir = mkdtempSync(join(tmpdir(), 'relay-server-test-'));
23
+
24
+ // ── Platform + logger mocks (must come before any source imports) ────
25
+
26
+ mock.module('../util/platform.js', () => ({
27
+ getDataDir: () => testDir,
28
+ isMacOS: () => process.platform === 'darwin',
29
+ isLinux: () => process.platform === 'linux',
30
+ isWindows: () => process.platform === 'win32',
31
+ getSocketPath: () => join(testDir, 'test.sock'),
32
+ getPidPath: () => join(testDir, 'test.pid'),
33
+ getDbPath: () => join(testDir, 'test.db'),
34
+ getLogPath: () => join(testDir, 'test.log'),
35
+ ensureDataDir: () => {},
36
+ }));
37
+
38
+ mock.module('../util/logger.js', () => ({
39
+ getLogger: () =>
40
+ new Proxy({} as Record<string, unknown>, {
41
+ get: () => () => {},
42
+ }),
43
+ }));
44
+
45
+ // ── Config mock ─────────────────────────────────────────────────────
46
+
47
+ mock.module('../config/loader.js', () => ({
48
+ getConfig: () => ({
49
+ apiKeys: { anthropic: 'test-key' },
50
+ calls: {
51
+ enabled: true,
52
+ provider: 'twilio',
53
+ maxDurationSeconds: 3600,
54
+ userConsultTimeoutSeconds: 120,
55
+ disclosure: { enabled: false, text: '' },
56
+ safety: { denyCategories: [] },
57
+ },
58
+ }),
59
+ }));
60
+
61
+ // ── Anthropic SDK mock ──────────────────────────────────────────────
62
+
63
+ function createMockStream(tokens: string[]) {
64
+ const emitter = new EventEmitter();
65
+ const fullText = tokens.join('');
66
+
67
+ const stream = {
68
+ on: (event: string, handler: (...args: unknown[]) => void) => {
69
+ emitter.on(event, handler);
70
+ return stream;
71
+ },
72
+ finalMessage: () => {
73
+ for (const token of tokens) {
74
+ emitter.emit('text', token);
75
+ }
76
+ return Promise.resolve({
77
+ content: [{ type: 'text', text: fullText }],
78
+ });
79
+ },
80
+ };
81
+
82
+ return stream;
83
+ }
84
+
85
+ let mockStreamFn: Mock<(...args: unknown[]) => unknown>;
86
+
87
+ mock.module('@anthropic-ai/sdk', () => {
88
+ mockStreamFn = mock((..._args: unknown[]) => createMockStream(['Hello']));
89
+ return {
90
+ default: class MockAnthropic {
91
+ messages = {
92
+ stream: (...args: unknown[]) => mockStreamFn(...args),
93
+ };
94
+ },
95
+ };
96
+ });
97
+
98
+ // ── Import source modules after all mocks ────────────────────────────
99
+
100
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
101
+ import { conversations } from '../memory/schema.js';
102
+ import {
103
+ createCallSession,
104
+ getCallSession,
105
+ getCallEvents,
106
+ } from '../calls/call-store.js';
107
+ import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
108
+ import type { RelayWebSocketData } from '../calls/relay-server.js';
109
+
110
+ initializeDb();
111
+
112
+ afterAll(() => {
113
+ resetDb();
114
+ try {
115
+ rmSync(testDir, { recursive: true });
116
+ } catch {
117
+ /* best effort */
118
+ }
119
+ });
120
+
121
+ // ── Mock WebSocket factory ──────────────────────────────────────────
122
+
123
+ interface MockWs {
124
+ sentMessages: string[];
125
+ readyState: number;
126
+ }
127
+
128
+ function createMockWs(callSessionId: string): { ws: MockWs; relay: RelayConnection } {
129
+ const sentMessages: string[] = [];
130
+ const ws = {
131
+ sentMessages,
132
+ readyState: 1, // WebSocket.OPEN
133
+ send(data: string) {
134
+ sentMessages.push(data);
135
+ },
136
+ data: { callSessionId } as RelayWebSocketData,
137
+ };
138
+
139
+ const relay = new RelayConnection(ws as unknown as import('bun').ServerWebSocket<RelayWebSocketData>, callSessionId);
140
+ return { ws, relay };
141
+ }
142
+
143
+ // ── Helpers ─────────────────────────────────────────────────────────
144
+
145
+ let ensuredConvIds = new Set<string>();
146
+ function ensureConversation(id: string): void {
147
+ if (ensuredConvIds.has(id)) return;
148
+ const db = getDb();
149
+ const now = Date.now();
150
+ db.insert(conversations).values({
151
+ id,
152
+ title: `Test conversation ${id}`,
153
+ createdAt: now,
154
+ updatedAt: now,
155
+ }).run();
156
+ ensuredConvIds.add(id);
157
+ }
158
+
159
+ function resetTables() {
160
+ const db = getDb();
161
+ db.run('DELETE FROM call_pending_questions');
162
+ db.run('DELETE FROM call_events');
163
+ db.run('DELETE FROM call_sessions');
164
+ db.run('DELETE FROM conversations');
165
+ ensuredConvIds = new Set();
166
+ }
167
+
168
+ describe('relay-server', () => {
169
+ beforeEach(() => {
170
+ resetTables();
171
+ activeRelayConnections.clear();
172
+ mockStreamFn.mockImplementation(() => createMockStream(['Hello']));
173
+ });
174
+
175
+ // ── Setup message handling ──────────────────────────────────────
176
+
177
+ test('handleMessage: setup message associates callSid and records event', async () => {
178
+ ensureConversation('conv-relay-1');
179
+ const session = createCallSession({
180
+ conversationId: 'conv-relay-1',
181
+ provider: 'twilio',
182
+ fromNumber: '+15551111111',
183
+ toNumber: '+15552222222',
184
+ });
185
+
186
+ const { relay } = createMockWs(session.id);
187
+
188
+ await relay.handleMessage(JSON.stringify({
189
+ type: 'setup',
190
+ callSid: 'CA_relay_setup_123',
191
+ from: '+15551111111',
192
+ to: '+15552222222',
193
+ }));
194
+
195
+ // Verify callSid was stored on the session
196
+ const updated = getCallSession(session.id);
197
+ expect(updated).not.toBeNull();
198
+ expect(updated!.providerCallSid).toBe('CA_relay_setup_123');
199
+
200
+ // Verify event was recorded
201
+ const events = getCallEvents(session.id);
202
+ const connectedEvents = events.filter(e => e.eventType === 'call_connected');
203
+ expect(connectedEvents.length).toBe(1);
204
+
205
+ // Verify orchestrator was created
206
+ expect(relay.getOrchestrator()).not.toBeNull();
207
+
208
+ relay.destroy();
209
+ });
210
+
211
+ test('handleMessage: setup message with custom parameters', async () => {
212
+ ensureConversation('conv-relay-custom');
213
+ const session = createCallSession({
214
+ conversationId: 'conv-relay-custom',
215
+ provider: 'twilio',
216
+ fromNumber: '+15551111111',
217
+ toNumber: '+15552222222',
218
+ task: 'Book appointment',
219
+ });
220
+
221
+ const { relay } = createMockWs(session.id);
222
+
223
+ await relay.handleMessage(JSON.stringify({
224
+ type: 'setup',
225
+ callSid: 'CA_relay_custom_123',
226
+ from: '+15551111111',
227
+ to: '+15552222222',
228
+ customParameters: { taskId: 'task-1', priority: 'high' },
229
+ }));
230
+
231
+ // Verify event recorded with custom parameters
232
+ const events = getCallEvents(session.id);
233
+ expect(events.length).toBe(1);
234
+ const payload = JSON.parse(events[0].payloadJson);
235
+ expect(payload.customParameters).toEqual({ taskId: 'task-1', priority: 'high' });
236
+
237
+ relay.destroy();
238
+ });
239
+
240
+ // ── Prompt message handling ─────────────────────────────────────
241
+
242
+ test('handleMessage: final prompt routes to orchestrator and records event', async () => {
243
+ ensureConversation('conv-relay-prompt');
244
+ const session = createCallSession({
245
+ conversationId: 'conv-relay-prompt',
246
+ provider: 'twilio',
247
+ fromNumber: '+15551111111',
248
+ toNumber: '+15552222222',
249
+ });
250
+
251
+ const { ws, relay } = createMockWs(session.id);
252
+
253
+ // First, setup to create orchestrator
254
+ await relay.handleMessage(JSON.stringify({
255
+ type: 'setup',
256
+ callSid: 'CA_prompt_123',
257
+ from: '+15551111111',
258
+ to: '+15552222222',
259
+ }));
260
+
261
+ // Now send a final prompt
262
+ await relay.handleMessage(JSON.stringify({
263
+ type: 'prompt',
264
+ voicePrompt: 'Hello, I need to make a reservation',
265
+ lang: 'en-US',
266
+ last: true,
267
+ }));
268
+
269
+ // Verify event was recorded
270
+ const events = getCallEvents(session.id);
271
+ const spokeEvents = events.filter(e => e.eventType === 'caller_spoke');
272
+ expect(spokeEvents.length).toBe(1);
273
+ const payload = JSON.parse(spokeEvents[0].payloadJson);
274
+ expect(payload.transcript).toBe('Hello, I need to make a reservation');
275
+
276
+ // Verify conversation history was updated
277
+ const history = relay.getConversationHistory();
278
+ expect(history.length).toBe(1);
279
+ expect(history[0].role).toBe('caller');
280
+ expect(history[0].text).toBe('Hello, I need to make a reservation');
281
+
282
+ // Verify tokens were sent through the WebSocket
283
+ expect(ws.sentMessages.length).toBeGreaterThan(0);
284
+
285
+ relay.destroy();
286
+ });
287
+
288
+ test('handleMessage: partial prompt (last=false) does not route to orchestrator', async () => {
289
+ ensureConversation('conv-relay-partial');
290
+ const session = createCallSession({
291
+ conversationId: 'conv-relay-partial',
292
+ provider: 'twilio',
293
+ fromNumber: '+15551111111',
294
+ toNumber: '+15552222222',
295
+ });
296
+
297
+ const { ws, relay } = createMockWs(session.id);
298
+
299
+ // Setup
300
+ await relay.handleMessage(JSON.stringify({
301
+ type: 'setup',
302
+ callSid: 'CA_partial_123',
303
+ from: '+15551111111',
304
+ to: '+15552222222',
305
+ }));
306
+
307
+ const messagesBeforePrompt = ws.sentMessages.length;
308
+
309
+ // Send a partial prompt (last=false)
310
+ await relay.handleMessage(JSON.stringify({
311
+ type: 'prompt',
312
+ voicePrompt: 'Hello, I need...',
313
+ lang: 'en-US',
314
+ last: false,
315
+ }));
316
+
317
+ // Should not have generated any new text tokens (no LLM call for partials)
318
+ // Only the setup-related messages should exist
319
+ expect(ws.sentMessages.length).toBe(messagesBeforePrompt);
320
+
321
+ // Conversation history should not have been updated for partials
322
+ const history = relay.getConversationHistory();
323
+ expect(history.length).toBe(0);
324
+
325
+ relay.destroy();
326
+ });
327
+
328
+ test('handleMessage: prompt without orchestrator sends fallback', async () => {
329
+ ensureConversation('conv-relay-no-orch');
330
+ const session = createCallSession({
331
+ conversationId: 'conv-relay-no-orch',
332
+ provider: 'twilio',
333
+ fromNumber: '+15551111111',
334
+ toNumber: '+15552222222',
335
+ });
336
+
337
+ const { ws, relay } = createMockWs(session.id);
338
+ // Note: no setup message, so no orchestrator
339
+
340
+ await relay.handleMessage(JSON.stringify({
341
+ type: 'prompt',
342
+ voicePrompt: 'Hello',
343
+ lang: 'en-US',
344
+ last: true,
345
+ }));
346
+
347
+ // Should have sent a fallback message
348
+ const textMessages = ws.sentMessages
349
+ .map(m => JSON.parse(m))
350
+ .filter((m: { type: string }) => m.type === 'text');
351
+ expect(textMessages.length).toBe(1);
352
+ expect(textMessages[0].token).toContain('still setting up');
353
+ expect(textMessages[0].last).toBe(true);
354
+
355
+ relay.destroy();
356
+ });
357
+
358
+ // ── Interrupt handling ──────────────────────────────────────────
359
+
360
+ test('handleMessage: interrupt message is handled without error', async () => {
361
+ ensureConversation('conv-relay-int');
362
+ const session = createCallSession({
363
+ conversationId: 'conv-relay-int',
364
+ provider: 'twilio',
365
+ fromNumber: '+15551111111',
366
+ toNumber: '+15552222222',
367
+ });
368
+
369
+ const { relay } = createMockWs(session.id);
370
+
371
+ // Setup
372
+ await relay.handleMessage(JSON.stringify({
373
+ type: 'setup',
374
+ callSid: 'CA_int_123',
375
+ from: '+15551111111',
376
+ to: '+15552222222',
377
+ }));
378
+
379
+ // Interrupt should not throw
380
+ await relay.handleMessage(JSON.stringify({
381
+ type: 'interrupt',
382
+ utteranceUntilInterrupt: 'Hello, I was saying...',
383
+ }));
384
+
385
+ relay.destroy();
386
+ });
387
+
388
+ // ── DTMF handling ───────────────────────────────────────────────
389
+
390
+ test('handleMessage: dtmf digit records event', async () => {
391
+ ensureConversation('conv-relay-dtmf');
392
+ const session = createCallSession({
393
+ conversationId: 'conv-relay-dtmf',
394
+ provider: 'twilio',
395
+ fromNumber: '+15551111111',
396
+ toNumber: '+15552222222',
397
+ });
398
+
399
+ const { relay } = createMockWs(session.id);
400
+
401
+ await relay.handleMessage(JSON.stringify({
402
+ type: 'dtmf',
403
+ digit: '5',
404
+ }));
405
+
406
+ const events = getCallEvents(session.id);
407
+ const dtmfEvents = events.filter(e => e.eventType === 'caller_spoke');
408
+ expect(dtmfEvents.length).toBe(1);
409
+ const payload = JSON.parse(dtmfEvents[0].payloadJson);
410
+ expect(payload.dtmfDigit).toBe('5');
411
+
412
+ relay.destroy();
413
+ });
414
+
415
+ // ── Error handling ──────────────────────────────────────────────
416
+
417
+ test('handleMessage: error message records call_failed event', async () => {
418
+ ensureConversation('conv-relay-err');
419
+ const session = createCallSession({
420
+ conversationId: 'conv-relay-err',
421
+ provider: 'twilio',
422
+ fromNumber: '+15551111111',
423
+ toNumber: '+15552222222',
424
+ });
425
+
426
+ const { relay } = createMockWs(session.id);
427
+
428
+ await relay.handleMessage(JSON.stringify({
429
+ type: 'error',
430
+ description: 'Audio stream disconnected',
431
+ }));
432
+
433
+ const events = getCallEvents(session.id);
434
+ const failEvents = events.filter(e => e.eventType === 'call_failed');
435
+ expect(failEvents.length).toBe(1);
436
+ const payload = JSON.parse(failEvents[0].payloadJson);
437
+ expect(payload.error).toBe('Audio stream disconnected');
438
+
439
+ relay.destroy();
440
+ });
441
+
442
+ // ── Malformed message resilience ────────────────────────────────
443
+
444
+ test('handleMessage: malformed JSON does not throw', async () => {
445
+ ensureConversation('conv-relay-malformed');
446
+ const session = createCallSession({
447
+ conversationId: 'conv-relay-malformed',
448
+ provider: 'twilio',
449
+ fromNumber: '+15551111111',
450
+ toNumber: '+15552222222',
451
+ });
452
+
453
+ const { relay } = createMockWs(session.id);
454
+
455
+ // Should not throw
456
+ await relay.handleMessage('not-valid-json{{{');
457
+
458
+ relay.destroy();
459
+ });
460
+
461
+ test('handleMessage: unknown message type does not throw', async () => {
462
+ ensureConversation('conv-relay-unknown');
463
+ const session = createCallSession({
464
+ conversationId: 'conv-relay-unknown',
465
+ provider: 'twilio',
466
+ fromNumber: '+15551111111',
467
+ toNumber: '+15552222222',
468
+ });
469
+
470
+ const { relay } = createMockWs(session.id);
471
+
472
+ // Should not throw
473
+ await relay.handleMessage(JSON.stringify({
474
+ type: 'some_future_type',
475
+ data: 'whatever',
476
+ }));
477
+
478
+ relay.destroy();
479
+ });
480
+
481
+ // ── sendTextToken / endSession ──────────────────────────────────
482
+
483
+ test('sendTextToken: sends correctly formatted text message', () => {
484
+ ensureConversation('conv-relay-send');
485
+ const session = createCallSession({
486
+ conversationId: 'conv-relay-send',
487
+ provider: 'twilio',
488
+ fromNumber: '+15551111111',
489
+ toNumber: '+15552222222',
490
+ });
491
+
492
+ const { ws, relay } = createMockWs(session.id);
493
+
494
+ relay.sendTextToken('Hello there', false);
495
+ relay.sendTextToken('', true);
496
+
497
+ expect(ws.sentMessages.length).toBe(2);
498
+
499
+ const msg1 = JSON.parse(ws.sentMessages[0]);
500
+ expect(msg1.type).toBe('text');
501
+ expect(msg1.token).toBe('Hello there');
502
+ expect(msg1.last).toBe(false);
503
+
504
+ const msg2 = JSON.parse(ws.sentMessages[1]);
505
+ expect(msg2.type).toBe('text');
506
+ expect(msg2.token).toBe('');
507
+ expect(msg2.last).toBe(true);
508
+
509
+ relay.destroy();
510
+ });
511
+
512
+ test('endSession: sends end message without reason', () => {
513
+ ensureConversation('conv-relay-end');
514
+ const session = createCallSession({
515
+ conversationId: 'conv-relay-end',
516
+ provider: 'twilio',
517
+ fromNumber: '+15551111111',
518
+ toNumber: '+15552222222',
519
+ });
520
+
521
+ const { ws, relay } = createMockWs(session.id);
522
+
523
+ relay.endSession();
524
+
525
+ expect(ws.sentMessages.length).toBe(1);
526
+ const msg = JSON.parse(ws.sentMessages[0]);
527
+ expect(msg.type).toBe('end');
528
+ expect(msg.handoffData).toBeUndefined();
529
+
530
+ relay.destroy();
531
+ });
532
+
533
+ test('endSession: sends end message with reason as handoffData', () => {
534
+ ensureConversation('conv-relay-end-reason');
535
+ const session = createCallSession({
536
+ conversationId: 'conv-relay-end-reason',
537
+ provider: 'twilio',
538
+ fromNumber: '+15551111111',
539
+ toNumber: '+15552222222',
540
+ });
541
+
542
+ const { ws, relay } = createMockWs(session.id);
543
+
544
+ relay.endSession('Call completed');
545
+
546
+ expect(ws.sentMessages.length).toBe(1);
547
+ const msg = JSON.parse(ws.sentMessages[0]);
548
+ expect(msg.type).toBe('end');
549
+ const handoff = JSON.parse(msg.handoffData);
550
+ expect(handoff.reason).toBe('Call completed');
551
+
552
+ relay.destroy();
553
+ });
554
+
555
+ // ── Conversation history ────────────────────────────────────────
556
+
557
+ test('getConversationHistory: returns role and text without timestamps', () => {
558
+ ensureConversation('conv-relay-hist');
559
+ const session = createCallSession({
560
+ conversationId: 'conv-relay-hist',
561
+ provider: 'twilio',
562
+ fromNumber: '+15551111111',
563
+ toNumber: '+15552222222',
564
+ });
565
+
566
+ const { relay } = createMockWs(session.id);
567
+
568
+ // Empty initially
569
+ expect(relay.getConversationHistory()).toEqual([]);
570
+
571
+ relay.destroy();
572
+ });
573
+
574
+ // ── Accessors ───────────────────────────────────────────────────
575
+
576
+ test('getCallSessionId: returns the call session ID', () => {
577
+ ensureConversation('conv-relay-id');
578
+ const session = createCallSession({
579
+ conversationId: 'conv-relay-id',
580
+ provider: 'twilio',
581
+ fromNumber: '+15551111111',
582
+ toNumber: '+15552222222',
583
+ });
584
+
585
+ const { relay } = createMockWs(session.id);
586
+ expect(relay.getCallSessionId()).toBe(session.id);
587
+
588
+ relay.destroy();
589
+ });
590
+
591
+ // ── destroy ─────────────────────────────────────────────────────
592
+
593
+ test('destroy: cleans up orchestrator', async () => {
594
+ ensureConversation('conv-relay-destroy');
595
+ const session = createCallSession({
596
+ conversationId: 'conv-relay-destroy',
597
+ provider: 'twilio',
598
+ fromNumber: '+15551111111',
599
+ toNumber: '+15552222222',
600
+ });
601
+
602
+ const { relay } = createMockWs(session.id);
603
+
604
+ // Setup creates orchestrator
605
+ await relay.handleMessage(JSON.stringify({
606
+ type: 'setup',
607
+ callSid: 'CA_destroy_123',
608
+ from: '+15551111111',
609
+ to: '+15552222222',
610
+ }));
611
+
612
+ expect(relay.getOrchestrator()).not.toBeNull();
613
+
614
+ relay.destroy();
615
+
616
+ expect(relay.getOrchestrator()).toBeNull();
617
+ });
618
+
619
+ test('destroy: can be called multiple times without error', () => {
620
+ ensureConversation('conv-relay-destroy2');
621
+ const session = createCallSession({
622
+ conversationId: 'conv-relay-destroy2',
623
+ provider: 'twilio',
624
+ fromNumber: '+15551111111',
625
+ toNumber: '+15552222222',
626
+ });
627
+
628
+ const { relay } = createMockWs(session.id);
629
+
630
+ relay.destroy();
631
+ expect(() => relay.destroy()).not.toThrow();
632
+ });
633
+ });
@@ -1,6 +1,5 @@
1
- import { describe, test, expect, beforeEach } from 'bun:test';
2
- import { initializeDb } from '../memory/db.js';
3
- import { getDb } from '../memory/db.js';
1
+ import { afterAll, describe, test, expect, beforeEach } from 'bun:test';
2
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
4
3
  import { reminders } from '../memory/schema.js';
5
4
  import {
6
5
  insertReminder,
@@ -19,6 +18,10 @@ function clearReminders() {
19
18
  getDb().delete(reminders).run();
20
19
  }
21
20
 
21
+ afterAll(() => {
22
+ resetDb();
23
+ });
24
+
22
25
  describe('reminder-store', () => {
23
26
  beforeEach(() => {
24
27
  clearReminders();