@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
@@ -10,7 +10,7 @@ describe("oauth provider behaviors", () => {
10
10
  const service = resolveService("gmail");
11
11
  const behavior = getProviderBehavior(service);
12
12
 
13
- expect(service).toBe("integration:gmail");
13
+ expect(service).toBe("integration:google");
14
14
  expect(behavior).toBeDefined();
15
15
  expect(behavior?.injectionTemplates).toBeDefined();
16
16
  expect(behavior?.injectionTemplates).toHaveLength(3);
@@ -34,9 +34,14 @@ mock.module("../security/secure-keys.js", () => ({
34
34
  deleteSecureKeyAsync: mockDeleteSecureKeyAsync,
35
35
  setSecureKeyAsync: mockSetSecureKeyAsync,
36
36
  getSecureKey: (account: string) => secureKeyValues.get(account),
37
+ getSecureKeyAsync: (account: string) =>
38
+ Promise.resolve(secureKeyValues.get(account)),
37
39
  }));
38
40
 
39
- import { initializeDb, resetDb, resetTestTables } from "../memory/db.js";
41
+ import { eq } from "drizzle-orm";
42
+
43
+ import { getDb, initializeDb, resetDb, resetTestTables } from "../memory/db.js";
44
+ import { oauthProviders } from "../memory/schema/oauth.js";
40
45
  import {
41
46
  createConnection,
42
47
  deleteApp,
@@ -46,8 +51,10 @@ import {
46
51
  getAppByProviderAndClientId,
47
52
  getConnection,
48
53
  getConnectionByProvider,
54
+ getConnectionByProviderAndAccount,
49
55
  getProvider,
50
56
  isProviderConnected,
57
+ listActiveConnectionsByProvider,
51
58
  listConnections,
52
59
  registerProvider,
53
60
  seedProviders,
@@ -137,7 +144,7 @@ describe("provider operations", () => {
137
144
  });
138
145
  });
139
146
 
140
- test("updates existing provider rows with corrected seed data", () => {
147
+ test("updates implementation fields while preserving user-customizable fields on re-seed", () => {
141
148
  seedProviders([
142
149
  {
143
150
  providerKey: "github",
@@ -168,17 +175,134 @@ describe("provider operations", () => {
168
175
 
169
176
  const row = getProvider("github");
170
177
  expect(row).toBeDefined();
171
- // Seed data should overwrite the existing row
178
+ // Implementation fields should be overwritten by the re-seed
172
179
  expect(row!.authUrl).toBe("https://github.com/login/oauth/authorize-v2");
173
180
  expect(row!.tokenUrl).toBe(
174
181
  "https://github.com/login/oauth/access_token-v2",
175
182
  );
176
- expect(row!.baseUrl).toBe("https://api.github.com/v2");
177
- expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user"]);
178
- expect(JSON.parse(row!.scopePolicy)).toEqual({ required: ["repo"] });
183
+ // User-customizable fields (baseUrl, defaultScopes, scopePolicy) are
184
+ // preserved from the original insert — not overwritten on re-seed.
185
+ expect(row!.baseUrl).toBe("https://api.github.com");
186
+ expect(JSON.parse(row!.defaultScopes)).toEqual(["repo"]);
187
+ expect(JSON.parse(row!.scopePolicy)).toEqual({});
179
188
  // createdAt should be preserved from the original insert
180
189
  expect(row!.createdAt).toBe(originalCreatedAt);
181
190
  });
191
+
192
+ test("persists pingUrl when provided", () => {
193
+ seedProviders([
194
+ {
195
+ providerKey: "github",
196
+ authUrl: "https://github.com/authorize",
197
+ tokenUrl: "https://github.com/token",
198
+ defaultScopes: ["repo"],
199
+ scopePolicy: {},
200
+ pingUrl: "https://api.github.com/user",
201
+ },
202
+ ]);
203
+ const row = getProvider("github");
204
+ expect(row!.pingUrl).toBe("https://api.github.com/user");
205
+ });
206
+
207
+ test("pingUrl defaults to null when omitted", () => {
208
+ seedProviders([
209
+ {
210
+ providerKey: "github",
211
+ authUrl: "https://github.com/authorize",
212
+ tokenUrl: "https://github.com/token",
213
+ defaultScopes: ["repo"],
214
+ scopePolicy: {},
215
+ },
216
+ ]);
217
+ const row = getProvider("github");
218
+ expect(row!.pingUrl).toBeNull();
219
+ });
220
+
221
+ test("preserves user-customizable fields while overwriting implementation fields on re-seed", () => {
222
+ // Initial seed with all fields
223
+ seedProviders([
224
+ {
225
+ providerKey: "github",
226
+ authUrl: "https://github.com/authorize",
227
+ tokenUrl: "https://github.com/token",
228
+ tokenEndpointAuthMethod: "client_secret_post",
229
+ defaultScopes: ["repo"],
230
+ scopePolicy: { required: ["repo"] },
231
+ userinfoUrl: "https://api.github.com/user",
232
+ baseUrl: "https://api.github.com",
233
+ extraParams: { prompt: "consent" },
234
+ callbackTransport: "loopback",
235
+ loopbackPort: 8765,
236
+ pingUrl: "https://api.github.com/user",
237
+ },
238
+ ]);
239
+
240
+ // Manually update user-customizable fields to simulate user edits
241
+ const db = getDb();
242
+ db.update(oauthProviders)
243
+ .set({
244
+ defaultScopes: JSON.stringify(["repo", "user", "gist"]),
245
+ scopePolicy: JSON.stringify({
246
+ required: ["repo"],
247
+ allowAdditionalScopes: true,
248
+ }),
249
+ userinfoUrl: "https://api.github.com/user/custom",
250
+ baseUrl: "https://custom.github.com/api",
251
+ })
252
+ .where(eq(oauthProviders.providerKey, "github"))
253
+ .run();
254
+
255
+ // Verify the manual updates took effect
256
+ const beforeReseed = getProvider("github");
257
+ expect(JSON.parse(beforeReseed!.defaultScopes)).toEqual([
258
+ "repo",
259
+ "user",
260
+ "gist",
261
+ ]);
262
+ expect(beforeReseed!.userinfoUrl).toBe(
263
+ "https://api.github.com/user/custom",
264
+ );
265
+ expect(beforeReseed!.baseUrl).toBe("https://custom.github.com/api");
266
+
267
+ // Re-seed with updated implementation fields
268
+ seedProviders([
269
+ {
270
+ providerKey: "github",
271
+ authUrl: "https://github.com/authorize-v2",
272
+ tokenUrl: "https://github.com/token-v2",
273
+ tokenEndpointAuthMethod: "client_secret_basic",
274
+ defaultScopes: ["repo-only"],
275
+ scopePolicy: {},
276
+ userinfoUrl: "https://api.github.com/user-v2",
277
+ baseUrl: "https://api.github.com/v2",
278
+ extraParams: { prompt: "login" },
279
+ callbackTransport: "gateway",
280
+ loopbackPort: 9999,
281
+ pingUrl: "https://api.github.com/user-v2",
282
+ },
283
+ ]);
284
+
285
+ const row = getProvider("github");
286
+ expect(row).toBeDefined();
287
+
288
+ // User-customizable fields should retain their manual values
289
+ expect(JSON.parse(row!.defaultScopes)).toEqual(["repo", "user", "gist"]);
290
+ expect(JSON.parse(row!.scopePolicy)).toEqual({
291
+ required: ["repo"],
292
+ allowAdditionalScopes: true,
293
+ });
294
+ expect(row!.userinfoUrl).toBe("https://api.github.com/user/custom");
295
+ expect(row!.baseUrl).toBe("https://custom.github.com/api");
296
+
297
+ // Implementation fields should be overwritten from the seed data
298
+ expect(row!.authUrl).toBe("https://github.com/authorize-v2");
299
+ expect(row!.tokenUrl).toBe("https://github.com/token-v2");
300
+ expect(row!.tokenEndpointAuthMethod).toBe("client_secret_basic");
301
+ expect(JSON.parse(row!.extraParams!)).toEqual({ prompt: "login" });
302
+ expect(row!.callbackTransport).toBe("gateway");
303
+ expect(row!.loopbackPort).toBe(9999);
304
+ expect(row!.pingUrl).toBe("https://api.github.com/user-v2");
305
+ });
182
306
  });
183
307
 
184
308
  describe("getProvider", () => {
@@ -279,13 +403,18 @@ describe("app operations", () => {
279
403
 
280
404
  test("stores clientSecret in secure storage on new app creation", async () => {
281
405
  seedTestProvider("github");
282
- const app = await upsertApp("github", "client-abc", "my-secret");
406
+ const app = await upsertApp("github", "client-abc", {
407
+ clientSecretValue: "my-secret",
408
+ });
283
409
 
284
410
  expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
285
411
  expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
286
412
  `oauth_app/${app.id}/client_secret`,
287
413
  "my-secret",
288
414
  );
415
+ expect(app.clientSecretCredentialPath).toBe(
416
+ `oauth_app/${app.id}/client_secret`,
417
+ );
289
418
  });
290
419
 
291
420
  test("stores clientSecret in secure storage when upserting an existing app", async () => {
@@ -293,11 +422,13 @@ describe("app operations", () => {
293
422
  const first = await upsertApp("github", "client-abc");
294
423
  mockSetSecureKeyAsync.mockClear();
295
424
 
296
- await upsertApp("github", "client-abc", "updated-secret");
425
+ await upsertApp("github", "client-abc", {
426
+ clientSecretValue: "updated-secret",
427
+ });
297
428
 
298
429
  expect(mockSetSecureKeyAsync).toHaveBeenCalledTimes(1);
299
430
  expect(mockSetSecureKeyAsync).toHaveBeenCalledWith(
300
- `oauth_app/${first.id}/client_secret`,
431
+ first.clientSecretCredentialPath,
301
432
  "updated-secret",
302
433
  );
303
434
  });
@@ -307,9 +438,70 @@ describe("app operations", () => {
307
438
  mockSetSecureKeyAsync.mockResolvedValueOnce(false);
308
439
 
309
440
  await expect(
310
- upsertApp("github", "client-abc", "bad-secret"),
441
+ upsertApp("github", "client-abc", { clientSecretValue: "bad-secret" }),
311
442
  ).rejects.toThrow("Failed to store client_secret in secure storage");
312
443
  });
444
+
445
+ test("accepts clientSecretCredentialPath and verifies existence", async () => {
446
+ seedTestProvider("github");
447
+ secureKeyValues.set("custom/path", "stored-secret");
448
+
449
+ const app = await upsertApp("github", "client-abc", {
450
+ clientSecretCredentialPath: "custom/path",
451
+ });
452
+
453
+ expect(app.clientSecretCredentialPath).toBe("custom/path");
454
+ // Should not have called setSecureKeyAsync since we only provided a path
455
+ expect(mockSetSecureKeyAsync).not.toHaveBeenCalled();
456
+ });
457
+
458
+ test("throws when clientSecretCredentialPath points to nonexistent secret", async () => {
459
+ seedTestProvider("github");
460
+
461
+ await expect(
462
+ upsertApp("github", "client-abc", {
463
+ clientSecretCredentialPath: "nonexistent/path",
464
+ }),
465
+ ).rejects.toThrow("No secret found at credential path: nonexistent/path");
466
+ });
467
+
468
+ test("throws when both clientSecretValue and clientSecretCredentialPath are provided", async () => {
469
+ seedTestProvider("github");
470
+
471
+ await expect(
472
+ upsertApp("github", "client-abc", {
473
+ clientSecretValue: "my-secret",
474
+ clientSecretCredentialPath: "custom/path",
475
+ }),
476
+ ).rejects.toThrow(
477
+ "Cannot provide both clientSecretValue and clientSecretCredentialPath",
478
+ );
479
+ });
480
+
481
+ test("records default clientSecretCredentialPath when neither value nor path is provided", async () => {
482
+ seedTestProvider("github");
483
+ const app = await upsertApp("github", "client-abc");
484
+
485
+ expect(app.clientSecretCredentialPath).toBe(
486
+ `oauth_app/${app.id}/client_secret`,
487
+ );
488
+ });
489
+
490
+ test("updates clientSecretCredentialPath on existing row when path is provided", async () => {
491
+ seedTestProvider("github");
492
+ const first = await upsertApp("github", "client-abc");
493
+ expect(first.clientSecretCredentialPath).toBe(
494
+ `oauth_app/${first.id}/client_secret`,
495
+ );
496
+
497
+ secureKeyValues.set("new/custom/path", "stored-secret");
498
+ const updated = await upsertApp("github", "client-abc", {
499
+ clientSecretCredentialPath: "new/custom/path",
500
+ });
501
+
502
+ expect(updated.id).toBe(first.id);
503
+ expect(updated.clientSecretCredentialPath).toBe("new/custom/path");
504
+ });
313
505
  });
314
506
 
315
507
  describe("getApp", () => {
@@ -353,14 +545,29 @@ describe("app operations", () => {
353
545
  expect(getApp(app.id)).toBeUndefined();
354
546
  });
355
547
 
356
- test("cleans up client_secret from secure storage", async () => {
548
+ test("cleans up client_secret from secure storage using stored path", async () => {
357
549
  const app = await createTestApp("github", "client-1");
358
550
  mockDeleteSecureKeyAsync.mockClear();
359
551
 
360
552
  await deleteApp(app.id);
361
553
 
362
554
  expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
363
- `oauth_app/${app.id}/client_secret`,
555
+ app.clientSecretCredentialPath,
556
+ );
557
+ });
558
+
559
+ test("uses custom clientSecretCredentialPath when deleting", async () => {
560
+ seedTestProvider("github");
561
+ secureKeyValues.set("custom/secret/path", "the-secret");
562
+ const app = await upsertApp("github", "client-1", {
563
+ clientSecretCredentialPath: "custom/secret/path",
564
+ });
565
+ mockDeleteSecureKeyAsync.mockClear();
566
+
567
+ await deleteApp(app.id);
568
+
569
+ expect(mockDeleteSecureKeyAsync).toHaveBeenCalledWith(
570
+ "custom/secret/path",
364
571
  );
365
572
  });
366
573
 
@@ -506,6 +713,145 @@ describe("connection operations", () => {
506
713
  });
507
714
  });
508
715
 
716
+ describe("getConnectionByProviderAndAccount", () => {
717
+ test("returns the connection matching the given account", async () => {
718
+ const app = await createTestApp("github", "client-1");
719
+
720
+ const conn1 = createConnection({
721
+ oauthAppId: app.id,
722
+ providerKey: "github",
723
+ accountInfo: "user1@example.com",
724
+ grantedScopes: ["repo"],
725
+ hasRefreshToken: false,
726
+ createdAt: 1000,
727
+ });
728
+
729
+ createConnection({
730
+ oauthAppId: app.id,
731
+ providerKey: "github",
732
+ accountInfo: "user2@example.com",
733
+ grantedScopes: ["repo"],
734
+ hasRefreshToken: false,
735
+ createdAt: 2000,
736
+ });
737
+
738
+ const result = getConnectionByProviderAndAccount(
739
+ "github",
740
+ "user1@example.com",
741
+ );
742
+ expect(result).toBeDefined();
743
+ expect(result!.id).toBe(conn1.id);
744
+ });
745
+
746
+ test("falls back to getConnectionByProvider when accountInfo is undefined", async () => {
747
+ const app = await createTestApp("github", "client-1");
748
+
749
+ const conn = createConnection({
750
+ oauthAppId: app.id,
751
+ providerKey: "github",
752
+ accountInfo: "user@example.com",
753
+ grantedScopes: ["repo"],
754
+ hasRefreshToken: false,
755
+ });
756
+
757
+ const result = getConnectionByProviderAndAccount("github", undefined);
758
+ expect(result).toBeDefined();
759
+ expect(result!.id).toBe(conn.id);
760
+ });
761
+
762
+ test("returns undefined when no connection matches the account", async () => {
763
+ const app = await createTestApp("github", "client-1");
764
+
765
+ createConnection({
766
+ oauthAppId: app.id,
767
+ providerKey: "github",
768
+ accountInfo: "user@example.com",
769
+ grantedScopes: ["repo"],
770
+ hasRefreshToken: false,
771
+ });
772
+
773
+ const result = getConnectionByProviderAndAccount(
774
+ "github",
775
+ "other@example.com",
776
+ );
777
+ expect(result).toBeUndefined();
778
+ });
779
+
780
+ test("skips revoked connections", async () => {
781
+ const app = await createTestApp("github", "client-1");
782
+
783
+ const conn = createConnection({
784
+ oauthAppId: app.id,
785
+ providerKey: "github",
786
+ accountInfo: "user@example.com",
787
+ grantedScopes: ["repo"],
788
+ hasRefreshToken: false,
789
+ });
790
+ updateConnection(conn.id, { status: "revoked" });
791
+
792
+ const result = getConnectionByProviderAndAccount(
793
+ "github",
794
+ "user@example.com",
795
+ );
796
+ expect(result).toBeUndefined();
797
+ });
798
+ });
799
+
800
+ describe("listActiveConnectionsByProvider", () => {
801
+ test("returns all active connections for a provider", async () => {
802
+ const app = await createTestApp("github", "client-1");
803
+
804
+ createConnection({
805
+ oauthAppId: app.id,
806
+ providerKey: "github",
807
+ accountInfo: "user1@example.com",
808
+ grantedScopes: ["repo"],
809
+ hasRefreshToken: false,
810
+ });
811
+
812
+ createConnection({
813
+ oauthAppId: app.id,
814
+ providerKey: "github",
815
+ accountInfo: "user2@example.com",
816
+ grantedScopes: ["repo"],
817
+ hasRefreshToken: false,
818
+ });
819
+
820
+ const results = listActiveConnectionsByProvider("github");
821
+ expect(results).toHaveLength(2);
822
+ });
823
+
824
+ test("excludes revoked connections", async () => {
825
+ const app = await createTestApp("github", "client-1");
826
+
827
+ createConnection({
828
+ oauthAppId: app.id,
829
+ providerKey: "github",
830
+ accountInfo: "user1@example.com",
831
+ grantedScopes: ["repo"],
832
+ hasRefreshToken: false,
833
+ });
834
+
835
+ const conn2 = createConnection({
836
+ oauthAppId: app.id,
837
+ providerKey: "github",
838
+ accountInfo: "user2@example.com",
839
+ grantedScopes: ["repo"],
840
+ hasRefreshToken: false,
841
+ });
842
+ updateConnection(conn2.id, { status: "revoked" });
843
+
844
+ const results = listActiveConnectionsByProvider("github");
845
+ expect(results).toHaveLength(1);
846
+ expect(results[0]!.accountInfo).toBe("user1@example.com");
847
+ });
848
+
849
+ test("returns empty array when no active connections exist", () => {
850
+ const results = listActiveConnectionsByProvider("github");
851
+ expect(results).toHaveLength(0);
852
+ });
853
+ });
854
+
509
855
  describe("isProviderConnected", () => {
510
856
  test("returns true when active connection has an access token in secure storage", async () => {
511
857
  const app = await createTestApp("github", "client-1");
@@ -518,7 +864,7 @@ describe("connection operations", () => {
518
864
 
519
865
  secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok");
520
866
 
521
- expect(isProviderConnected("github")).toBe(true);
867
+ expect(await isProviderConnected("github")).toBe(true);
522
868
  });
523
869
 
524
870
  test("returns false when active connection exists but access token is missing", async () => {
@@ -531,11 +877,11 @@ describe("connection operations", () => {
531
877
  });
532
878
 
533
879
  // No secure key set — simulates failed token write
534
- expect(isProviderConnected("github")).toBe(false);
880
+ expect(await isProviderConnected("github")).toBe(false);
535
881
  });
536
882
 
537
- test("returns false when no connection exists", () => {
538
- expect(isProviderConnected("github")).toBe(false);
883
+ test("returns false when no connection exists", async () => {
884
+ expect(await isProviderConnected("github")).toBe(false);
539
885
  });
540
886
 
541
887
  test("returns false when connection is revoked even with token in store", async () => {
@@ -550,7 +896,7 @@ describe("connection operations", () => {
550
896
  updateConnection(conn.id, { status: "revoked" });
551
897
  secureKeyValues.set(`oauth_connection/${conn.id}/access_token`, "tok");
552
898
 
553
- expect(isProviderConnected("github")).toBe(false);
899
+ expect(await isProviderConnected("github")).toBe(false);
554
900
  });
555
901
  });
556
902
 
@@ -21,12 +21,14 @@ interface MockCallLog {
21
21
 
22
22
  let mockCollectionExists: boolean;
23
23
  let mockCollectionSize: number;
24
+ let mockUseNamedVectors: boolean;
24
25
  let mockSentinelPayload: Record<string, unknown> | null;
25
26
  let callLog: MockCallLog;
26
27
 
27
28
  function resetMockState() {
28
29
  mockCollectionExists = false;
29
30
  mockCollectionSize = 384;
31
+ mockUseNamedVectors = false;
30
32
  mockSentinelPayload = null;
31
33
  callLog = {
32
34
  collectionExists: 0,
@@ -51,7 +53,9 @@ mock.module("@qdrant/js-client-rest", () => ({
51
53
  return {
52
54
  config: {
53
55
  params: {
54
- vectors: { size: mockCollectionSize },
56
+ vectors: mockUseNamedVectors
57
+ ? { dense: { size: mockCollectionSize } }
58
+ : { size: mockCollectionSize },
55
59
  },
56
60
  },
57
61
  };
@@ -77,7 +81,12 @@ mock.module("@qdrant/js-client-rest", () => ({
77
81
  mockSentinelPayload &&
78
82
  opts.ids.includes("00000000-0000-0000-0000-000000000000")
79
83
  ) {
80
- return [{ id: "00000000-0000-0000-0000-000000000000", payload: mockSentinelPayload }];
84
+ return [
85
+ {
86
+ id: "00000000-0000-0000-0000-000000000000",
87
+ payload: mockSentinelPayload,
88
+ },
89
+ ];
81
90
  }
82
91
  return [];
83
92
  }
@@ -97,6 +106,7 @@ beforeEach(() => {
97
106
  describe("Qdrant collection migration", () => {
98
107
  test("deletes and recreates collection on dimension mismatch", async () => {
99
108
  mockCollectionExists = true;
109
+ mockUseNamedVectors = true;
100
110
  mockCollectionSize = 384; // Current collection has 384-dim vectors
101
111
 
102
112
  const client = new VellumQdrantClient({
@@ -108,14 +118,16 @@ describe("Qdrant collection migration", () => {
108
118
  embeddingModel: "gemini:gemini-embedding-2-preview",
109
119
  });
110
120
 
111
- await client.ensureCollection();
121
+ const result = await client.ensureCollection();
112
122
 
113
123
  expect(callLog.deleteCollection).toBe(1);
114
124
  expect(callLog.createCollection).toBe(1);
125
+ expect(result.migrated).toBe(true);
115
126
  });
116
127
 
117
128
  test("deletes and recreates collection on model-only mismatch", async () => {
118
129
  mockCollectionExists = true;
130
+ mockUseNamedVectors = true;
119
131
  mockCollectionSize = 768; // Same dimension
120
132
  mockSentinelPayload = {
121
133
  _meta: true,
@@ -131,16 +143,18 @@ describe("Qdrant collection migration", () => {
131
143
  embeddingModel: "gemini:gemini-embedding-2-preview", // New model
132
144
  });
133
145
 
134
- await client.ensureCollection();
146
+ const result = await client.ensureCollection();
135
147
 
136
148
  expect(callLog.deleteCollection).toBe(1);
137
149
  expect(callLog.createCollection).toBe(1);
138
150
  // Sentinel should be written for the new model
139
151
  expect(callLog.upsert).toBe(1);
152
+ expect(result.migrated).toBe(true);
140
153
  });
141
154
 
142
155
  test("leaves collection untouched when dimensions and model match", async () => {
143
156
  mockCollectionExists = true;
157
+ mockUseNamedVectors = true;
144
158
  mockCollectionSize = 768;
145
159
  mockSentinelPayload = {
146
160
  _meta: true,
@@ -156,14 +170,16 @@ describe("Qdrant collection migration", () => {
156
170
  embeddingModel: "gemini:gemini-embedding-2-preview",
157
171
  });
158
172
 
159
- await client.ensureCollection();
173
+ const result = await client.ensureCollection();
160
174
 
161
175
  expect(callLog.deleteCollection).toBe(0);
162
176
  expect(callLog.createCollection).toBe(0);
177
+ expect(result.migrated).toBe(false);
163
178
  });
164
179
 
165
180
  test("does not rebuild pre-existing collection without sentinel (graceful upgrade)", async () => {
166
181
  mockCollectionExists = true;
182
+ mockUseNamedVectors = true;
167
183
  mockCollectionSize = 768;
168
184
  mockSentinelPayload = null; // No sentinel — pre-existing collection
169
185
 
@@ -176,11 +192,12 @@ describe("Qdrant collection migration", () => {
176
192
  embeddingModel: "gemini:gemini-embedding-2-preview",
177
193
  });
178
194
 
179
- await client.ensureCollection();
195
+ const result = await client.ensureCollection();
180
196
 
181
197
  // No sentinel found → no model mismatch → collection kept
182
198
  expect(callLog.deleteCollection).toBe(0);
183
199
  expect(callLog.createCollection).toBe(0);
200
+ expect(result.migrated).toBe(false);
184
201
  });
185
202
 
186
203
  test("writes sentinel point when creating a new collection", async () => {
@@ -195,11 +212,37 @@ describe("Qdrant collection migration", () => {
195
212
  embeddingModel: "gemini:gemini-embedding-2-preview",
196
213
  });
197
214
 
198
- await client.ensureCollection();
215
+ const result = await client.ensureCollection();
199
216
 
200
217
  expect(callLog.createCollection).toBe(1);
201
218
  // Sentinel upsert should be called
202
219
  expect(callLog.upsert).toBe(1);
220
+ // Fresh collection, not a migration
221
+ expect(result.migrated).toBe(false);
222
+ });
223
+
224
+ test("deletes and recreates collection when migrating from unnamed to named vectors", async () => {
225
+ mockCollectionExists = true;
226
+ mockUseNamedVectors = false; // Legacy unnamed vectors
227
+ mockCollectionSize = 768;
228
+
229
+ const client = new VellumQdrantClient({
230
+ url: "http://localhost:6333",
231
+ collection: "memory",
232
+ vectorSize: 768, // Same dimension
233
+ onDisk: false,
234
+ quantization: "none",
235
+ embeddingModel: "gemini:gemini-embedding-2-preview",
236
+ });
237
+
238
+ const result = await client.ensureCollection();
239
+
240
+ // Unnamed vectors should trigger delete + recreate with named vectors
241
+ expect(callLog.deleteCollection).toBe(1);
242
+ expect(callLog.createCollection).toBe(1);
243
+ // Sentinel should be written for the new collection
244
+ expect(callLog.upsert).toBe(1);
245
+ expect(result.migrated).toBe(true);
203
246
  });
204
247
 
205
248
  test("does not write sentinel when embeddingModel is not provided", async () => {
@@ -214,10 +257,12 @@ describe("Qdrant collection migration", () => {
214
257
  // No embeddingModel
215
258
  });
216
259
 
217
- await client.ensureCollection();
260
+ const result = await client.ensureCollection();
218
261
 
219
262
  expect(callLog.createCollection).toBe(1);
220
263
  // No sentinel should be written
221
264
  expect(callLog.upsert).toBe(0);
265
+ // Fresh collection, not a migration
266
+ expect(result.migrated).toBe(false);
222
267
  });
223
268
  });
@@ -524,6 +524,5 @@ describe("computer-use registration split", () => {
524
524
  expect(registered.every((t) => t.name.startsWith("computer_use_"))).toBe(
525
525
  true,
526
526
  );
527
- expect(getTool("computer_use_request_control")).toBeUndefined();
528
527
  });
529
528
  });