@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
@@ -30,6 +30,56 @@ let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
30
30
  "not-found";
31
31
  let idCounter = 0;
32
32
 
33
+ // App upsert mock state
34
+ let mockUpsertAppCalls: Array<{
35
+ provider: string;
36
+ clientId: string;
37
+ clientSecretOpts?: {
38
+ clientSecretValue?: string;
39
+ clientSecretCredentialPath?: string;
40
+ };
41
+ }> = [];
42
+ let mockUpsertAppResult: Record<string, unknown> = {
43
+ id: "app-upsert-1",
44
+ providerKey: "integration:test",
45
+ clientId: "test-client-id",
46
+ createdAt: 1700000000000,
47
+ updatedAt: 1700000000000,
48
+ };
49
+ let mockUpsertAppImpl:
50
+ | ((
51
+ provider: string,
52
+ clientId: string,
53
+ clientSecretOpts?: {
54
+ clientSecretValue?: string;
55
+ clientSecretCredentialPath?: string;
56
+ },
57
+ ) => Promise<Record<string, unknown>>)
58
+ | undefined;
59
+
60
+ // Connect mock state
61
+ let mockOrchestrateOAuthConnect: (
62
+ opts: Record<string, unknown>,
63
+ ) => Promise<Record<string, unknown>>;
64
+ let mockGetAppByProviderAndClientId: (
65
+ providerKey: string,
66
+ clientId: string,
67
+ ) => Record<string, unknown> | undefined = () => undefined;
68
+ let mockGetMostRecentAppByProvider: (
69
+ providerKey: string,
70
+ ) => Record<string, unknown> | undefined = () => undefined;
71
+ let mockGetProvider: (
72
+ providerKey: string,
73
+ ) => Record<string, unknown> | undefined = () => undefined;
74
+ let mockGetProviderBehavior: (
75
+ providerKey: string,
76
+ ) => Record<string, unknown> | undefined = () => undefined;
77
+ let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
78
+ let mockGetCredentialMetadata: (
79
+ service: string,
80
+ field: string,
81
+ ) => Record<string, unknown> | undefined = () => undefined;
82
+
33
83
  function nextUUID(): string {
34
84
  idCounter += 1;
35
85
  return `00000000-0000-0000-0000-${String(idCounter).padStart(12, "0")}`;
@@ -72,13 +122,28 @@ mock.module("../oauth/oauth-store.js", () => ({
72
122
  listConnections: () => [],
73
123
  deleteConnection: () => false,
74
124
  // Stubs required by apps.ts and providers.ts (transitively loaded via oauth/index.ts)
75
- upsertApp: async () => ({}),
125
+ upsertApp: async (
126
+ provider: string,
127
+ clientId: string,
128
+ clientSecretOpts?: {
129
+ clientSecretValue?: string;
130
+ clientSecretCredentialPath?: string;
131
+ },
132
+ ) => {
133
+ if (mockUpsertAppImpl) {
134
+ return mockUpsertAppImpl(provider, clientId, clientSecretOpts);
135
+ }
136
+ mockUpsertAppCalls.push({ provider, clientId, clientSecretOpts });
137
+ return mockUpsertAppResult;
138
+ },
76
139
  getApp: () => undefined,
77
- getAppByProviderAndClientId: () => undefined,
78
- getMostRecentAppByProvider: () => undefined,
140
+ getAppByProviderAndClientId: (providerKey: string, clientId: string) =>
141
+ mockGetAppByProviderAndClientId(providerKey, clientId),
142
+ getMostRecentAppByProvider: (providerKey: string) =>
143
+ mockGetMostRecentAppByProvider(providerKey),
79
144
  listApps: () => [],
80
145
  deleteApp: async () => false,
81
- getProvider: () => undefined,
146
+ getProvider: (providerKey: string) => mockGetProvider(providerKey),
82
147
  listProviders: () => mockListProviders(),
83
148
  registerProvider: () => ({}),
84
149
  seedProviders: () => {},
@@ -89,7 +154,7 @@ mock.module("../oauth/oauth-store.js", () => ({
89
154
 
90
155
  // Stub out transitive dependencies that token-manager would normally pull in
91
156
  mock.module("../security/secure-keys.js", () => ({
92
- getSecureKey: () => undefined,
157
+ getSecureKey: (account: string) => mockGetSecureKey(account),
93
158
  setSecureKey: () => true,
94
159
  getSecureKeyAsync: async () => undefined,
95
160
  setSecureKeyAsync: async () => true,
@@ -116,7 +181,8 @@ mock.module("../security/secure-keys.js", () => ({
116
181
 
117
182
  mock.module("../tools/credentials/metadata-store.js", () => ({
118
183
  assertMetadataWritable: () => {},
119
- getCredentialMetadata: () => undefined,
184
+ getCredentialMetadata: (service: string, field: string) =>
185
+ mockGetCredentialMetadata(service, field),
120
186
  upsertCredentialMetadata: () => ({}),
121
187
  listCredentialMetadata: () => [],
122
188
  deleteCredentialMetadata: (service: string, field: string): boolean => {
@@ -129,6 +195,25 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
129
195
  },
130
196
  }));
131
197
 
198
+ // ---------------------------------------------------------------------------
199
+ // Mock connect-orchestrator
200
+ // ---------------------------------------------------------------------------
201
+
202
+ mock.module("../oauth/connect-orchestrator.js", () => ({
203
+ orchestrateOAuthConnect: (opts: Record<string, unknown>) =>
204
+ mockOrchestrateOAuthConnect(opts),
205
+ }));
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Mock provider-behaviors
209
+ // ---------------------------------------------------------------------------
210
+
211
+ mock.module("../oauth/provider-behaviors.js", () => ({
212
+ resolveService: (service: string) => service,
213
+ getProviderBehavior: (providerKey: string) =>
214
+ mockGetProviderBehavior(providerKey),
215
+ }));
216
+
132
217
  mock.module("../util/logger.js", () => ({
133
218
  getLogger: () => ({
134
219
  info: () => {},
@@ -252,11 +337,11 @@ describe("assistant oauth connections token <provider-key>", () => {
252
337
  const { exitCode, stdout } = await runCli([
253
338
  "connections",
254
339
  "token",
255
- "integration:gmail",
340
+ "integration:google",
256
341
  ]);
257
342
  expect(exitCode).toBe(0);
258
343
  expect(stdout).toBe("gmail-token\n");
259
- expect(capturedService).toBe("integration:gmail");
344
+ expect(capturedService).toBe("integration:google");
260
345
  });
261
346
 
262
347
  test("exits 1 when no token exists", async () => {
@@ -336,30 +421,30 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
336
421
  const result = await runCli([
337
422
  "connections",
338
423
  "disconnect",
339
- "integration:gmail",
424
+ "integration:google",
340
425
  "--json",
341
426
  ]);
342
427
  expect(result.exitCode).toBe(0);
343
428
  const parsed = JSON.parse(result.stdout);
344
429
  expect(parsed.ok).toBe(true);
345
- expect(parsed.service).toBe("integration:gmail");
430
+ expect(parsed.service).toBe("integration:google");
346
431
 
347
432
  // disconnectOAuthProvider should have been called with the full provider key
348
- expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
433
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:google"]);
349
434
  });
350
435
 
351
436
  test("reports not-found when nothing exists", async () => {
352
437
  const result = await runCli([
353
438
  "connections",
354
439
  "disconnect",
355
- "integration:gmail",
440
+ "integration:google",
356
441
  "--json",
357
442
  ]);
358
443
  expect(result.exitCode).toBe(1);
359
444
  const parsed = JSON.parse(result.stdout);
360
445
  expect(parsed.ok).toBe(false);
361
446
  expect(parsed.error).toContain("No OAuth connection or credentials");
362
- expect(parsed.error).toContain("integration:gmail");
447
+ expect(parsed.error).toContain("integration:google");
363
448
  });
364
449
 
365
450
  test("cleans up legacy credential keys if present", async () => {
@@ -372,12 +457,12 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
372
457
  ];
373
458
  for (const field of legacyFields) {
374
459
  secureKeyStore.set(
375
- credentialKey("integration:gmail", field),
460
+ credentialKey("integration:google", field),
376
461
  `legacy_${field}_value`,
377
462
  );
378
463
  metadataStore.push({
379
464
  credentialId: nextUUID(),
380
- service: "integration:gmail",
465
+ service: "integration:google",
381
466
  field,
382
467
  allowedTools: [],
383
468
  allowedDomains: [],
@@ -389,22 +474,22 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
389
474
  const result = await runCli([
390
475
  "connections",
391
476
  "disconnect",
392
- "integration:gmail",
477
+ "integration:google",
393
478
  "--json",
394
479
  ]);
395
480
  expect(result.exitCode).toBe(0);
396
481
  const parsed = JSON.parse(result.stdout);
397
482
  expect(parsed.ok).toBe(true);
398
- expect(parsed.service).toBe("integration:gmail");
483
+ expect(parsed.service).toBe("integration:google");
399
484
 
400
485
  // All legacy keys should be removed
401
486
  for (const field of legacyFields) {
402
487
  expect(
403
- secureKeyStore.has(credentialKey("integration:gmail", field)),
488
+ secureKeyStore.has(credentialKey("integration:google", field)),
404
489
  ).toBe(false);
405
490
  expect(
406
491
  metadataStore.find(
407
- (m) => m.service === "integration:gmail" && m.field === field,
492
+ (m) => m.service === "integration:google" && m.field === field,
408
493
  ),
409
494
  ).toBeUndefined();
410
495
  }
@@ -416,12 +501,12 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
416
501
 
417
502
  // Seed a legacy credential key
418
503
  secureKeyStore.set(
419
- credentialKey("integration:gmail", "access_token"),
504
+ credentialKey("integration:google", "access_token"),
420
505
  "legacy_token",
421
506
  );
422
507
  metadataStore.push({
423
508
  credentialId: nextUUID(),
424
- service: "integration:gmail",
509
+ service: "integration:google",
425
510
  field: "access_token",
426
511
  allowedTools: [],
427
512
  allowedDomains: [],
@@ -432,7 +517,7 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
432
517
  const result = await runCli([
433
518
  "connections",
434
519
  "disconnect",
435
- "integration:gmail",
520
+ "integration:google",
436
521
  "--json",
437
522
  ]);
438
523
  expect(result.exitCode).toBe(0);
@@ -440,9 +525,9 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
440
525
  expect(parsed.ok).toBe(true);
441
526
 
442
527
  // Both should be cleaned up
443
- expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
528
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:google"]);
444
529
  expect(
445
- secureKeyStore.has(credentialKey("integration:gmail", "access_token")),
530
+ secureKeyStore.has(credentialKey("integration:google", "access_token")),
446
531
  ).toBe(false);
447
532
  });
448
533
  });
@@ -454,7 +539,7 @@ describe("assistant oauth connections disconnect <provider-key>", () => {
454
539
  describe("assistant oauth providers list", () => {
455
540
  const fakeProviders = [
456
541
  {
457
- providerKey: "integration:gmail",
542
+ providerKey: "integration:google",
458
543
  authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
459
544
  tokenUrl: "https://oauth2.googleapis.com/token",
460
545
  defaultScopes: "[]",
@@ -511,7 +596,7 @@ describe("assistant oauth providers list", () => {
511
596
  const parsed = JSON.parse(stdout);
512
597
  expect(parsed).toHaveLength(4);
513
598
  const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
514
- expect(keys).toContain("integration:gmail");
599
+ expect(keys).toContain("integration:google");
515
600
  expect(keys).toContain("integration:google-calendar");
516
601
  expect(keys).toContain("integration:slack");
517
602
  expect(keys).toContain("integration:twitter");
@@ -522,13 +607,13 @@ describe("assistant oauth providers list", () => {
522
607
  "providers",
523
608
  "list",
524
609
  "--provider-key",
525
- "gmail",
610
+ "slack",
526
611
  "--json",
527
612
  ]);
528
613
  expect(exitCode).toBe(0);
529
614
  const parsed = JSON.parse(stdout);
530
615
  expect(parsed).toHaveLength(1);
531
- expect(parsed[0].providerKey).toBe("integration:gmail");
616
+ expect(parsed[0].providerKey).toBe("integration:slack");
532
617
  });
533
618
 
534
619
  test("filters by comma-separated OR values", async () => {
@@ -536,15 +621,16 @@ describe("assistant oauth providers list", () => {
536
621
  "providers",
537
622
  "list",
538
623
  "--provider-key",
539
- "gmail,google",
624
+ "slack,google",
540
625
  "--json",
541
626
  ]);
542
627
  expect(exitCode).toBe(0);
543
628
  const parsed = JSON.parse(stdout);
544
- expect(parsed).toHaveLength(2);
629
+ expect(parsed).toHaveLength(3);
545
630
  const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
546
- expect(keys).toContain("integration:gmail");
631
+ expect(keys).toContain("integration:google");
547
632
  expect(keys).toContain("integration:google-calendar");
633
+ expect(keys).toContain("integration:slack");
548
634
  });
549
635
 
550
636
  test("returns empty array when comma-separated filter has no matches", async () => {
@@ -559,4 +645,711 @@ describe("assistant oauth providers list", () => {
559
645
  const parsed = JSON.parse(stdout);
560
646
  expect(parsed).toHaveLength(0);
561
647
  });
648
+
649
+ test("trims whitespace around commas in --provider-key", async () => {
650
+ const { exitCode, stdout } = await runCli([
651
+ "providers",
652
+ "list",
653
+ "--provider-key",
654
+ "slack, google",
655
+ "--json",
656
+ ]);
657
+ expect(exitCode).toBe(0);
658
+ const parsed = JSON.parse(stdout);
659
+ expect(parsed).toHaveLength(3);
660
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
661
+ expect(keys).toContain("integration:google");
662
+ expect(keys).toContain("integration:google-calendar");
663
+ expect(keys).toContain("integration:slack");
664
+ });
665
+
666
+ test("ignores empty segments from extra commas in --provider-key", async () => {
667
+ const { exitCode, stdout } = await runCli([
668
+ "providers",
669
+ "list",
670
+ "--provider-key",
671
+ "slack,,google",
672
+ "--json",
673
+ ]);
674
+ expect(exitCode).toBe(0);
675
+ const parsed = JSON.parse(stdout);
676
+ expect(parsed).toHaveLength(3);
677
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
678
+ expect(keys).toContain("integration:google");
679
+ expect(keys).toContain("integration:google-calendar");
680
+ expect(keys).toContain("integration:slack");
681
+ });
682
+ });
683
+
684
+ // ---------------------------------------------------------------------------
685
+ // connect
686
+ // ---------------------------------------------------------------------------
687
+
688
+ describe("assistant oauth connections connect <provider-key>", () => {
689
+ beforeEach(() => {
690
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
691
+ secureKeyStore = new Map();
692
+ metadataStore = [];
693
+ disconnectOAuthProviderCalls = [];
694
+ disconnectOAuthProviderResult = "not-found";
695
+ idCounter = 0;
696
+ mockOrchestrateOAuthConnect = async () => ({
697
+ success: true,
698
+ deferred: false,
699
+ grantedScopes: [],
700
+ });
701
+ mockGetAppByProviderAndClientId = () => undefined;
702
+ mockGetMostRecentAppByProvider = () => undefined;
703
+ mockGetProvider = () => undefined;
704
+ mockGetProviderBehavior = () => undefined;
705
+ mockGetSecureKey = () => undefined;
706
+ });
707
+
708
+ test("completes interactive flow and prints success (human mode)", async () => {
709
+ mockGetAppByProviderAndClientId = () => ({
710
+ id: "app-1",
711
+ clientId: "test-id",
712
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
713
+ providerKey: "integration:google",
714
+ createdAt: 0,
715
+ updatedAt: 0,
716
+ });
717
+ mockOrchestrateOAuthConnect = async () => ({
718
+ success: true,
719
+ deferred: false,
720
+ grantedScopes: ["read"],
721
+ accountInfo: "user@example.com",
722
+ });
723
+
724
+ const { exitCode, stdout } = await runCli([
725
+ "connections",
726
+ "connect",
727
+ "integration:google",
728
+ "--client-id",
729
+ "test-id",
730
+ ]);
731
+ expect(exitCode).toBe(0);
732
+ expect(stdout).toContain("Connected");
733
+ });
734
+
735
+ test("completes interactive flow and returns JSON with --json flag", async () => {
736
+ mockGetAppByProviderAndClientId = () => ({
737
+ id: "app-1",
738
+ clientId: "test-id",
739
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
740
+ providerKey: "integration:google",
741
+ createdAt: 0,
742
+ updatedAt: 0,
743
+ });
744
+ mockOrchestrateOAuthConnect = async () => ({
745
+ success: true,
746
+ deferred: false,
747
+ grantedScopes: ["read"],
748
+ accountInfo: "user@example.com",
749
+ });
750
+
751
+ const { exitCode, stdout } = await runCli([
752
+ "connections",
753
+ "connect",
754
+ "integration:google",
755
+ "--client-id",
756
+ "test-id",
757
+ "--json",
758
+ ]);
759
+ expect(exitCode).toBe(0);
760
+ const parsed = JSON.parse(stdout);
761
+ expect(parsed).toEqual({
762
+ ok: true,
763
+ grantedScopes: ["read"],
764
+ accountInfo: "user@example.com",
765
+ });
766
+ });
767
+
768
+ test("returns auth URL in default (non-interactive) mode (JSON)", async () => {
769
+ mockGetAppByProviderAndClientId = () => ({
770
+ id: "app-1",
771
+ clientId: "test-id",
772
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
773
+ providerKey: "integration:google",
774
+ createdAt: 0,
775
+ updatedAt: 0,
776
+ });
777
+ mockOrchestrateOAuthConnect = async () => ({
778
+ success: true,
779
+ deferred: true,
780
+ authUrl: "https://example.com/auth",
781
+ state: "abc",
782
+ service: "integration:google",
783
+ });
784
+
785
+ const { exitCode, stdout } = await runCli([
786
+ "connections",
787
+ "connect",
788
+ "integration:google",
789
+ "--client-id",
790
+ "test-id",
791
+ "--json",
792
+ ]);
793
+ expect(exitCode).toBe(0);
794
+ const parsed = JSON.parse(stdout);
795
+ expect(parsed.ok).toBe(true);
796
+ expect(parsed.deferred).toBe(true);
797
+ expect(parsed.authUrl).toBe("https://example.com/auth");
798
+ });
799
+
800
+ test("fails when no client_id available", async () => {
801
+ mockGetMostRecentAppByProvider = () => undefined;
802
+
803
+ const { exitCode, stdout } = await runCli([
804
+ "connections",
805
+ "connect",
806
+ "integration:google",
807
+ "--json",
808
+ ]);
809
+ expect(exitCode).toBe(1);
810
+ const parsed = JSON.parse(stdout);
811
+ expect(parsed.ok).toBe(false);
812
+ expect(parsed.error).toContain("client_id");
813
+ });
814
+
815
+ test("resolves client_id from DB when not provided", async () => {
816
+ mockGetMostRecentAppByProvider = () => ({
817
+ id: "app-1",
818
+ clientId: "db-client-id",
819
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
820
+ providerKey: "integration:google",
821
+ createdAt: 0,
822
+ updatedAt: 0,
823
+ });
824
+
825
+ let capturedClientId: string | undefined;
826
+ mockOrchestrateOAuthConnect = async (opts) => {
827
+ capturedClientId = opts.clientId as string;
828
+ return {
829
+ success: true,
830
+ deferred: false,
831
+ grantedScopes: [],
832
+ };
833
+ };
834
+
835
+ await runCli(["connections", "connect", "integration:google"]);
836
+ expect(capturedClientId).toBe("db-client-id");
837
+ });
838
+
839
+ test("resolves client_secret from secure store when not provided", async () => {
840
+ mockGetMostRecentAppByProvider = () => ({
841
+ id: "app-1",
842
+ clientId: "db-client-id",
843
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
844
+ providerKey: "integration:google",
845
+ createdAt: 0,
846
+ updatedAt: 0,
847
+ });
848
+
849
+ mockGetSecureKey = (account: string) =>
850
+ account === "oauth_app/app-1/client_secret" ? "db-secret" : undefined;
851
+
852
+ let capturedOpts: Record<string, unknown> | undefined;
853
+ mockOrchestrateOAuthConnect = async (opts) => {
854
+ capturedOpts = opts;
855
+ return {
856
+ success: true,
857
+ deferred: false,
858
+ grantedScopes: [],
859
+ };
860
+ };
861
+
862
+ await runCli(["connections", "connect", "integration:google"]);
863
+ expect(capturedOpts).toBeDefined();
864
+ expect(capturedOpts!.clientId).toBe("db-client-id");
865
+ expect(capturedOpts!.clientSecret).toBe("db-secret");
866
+ });
867
+
868
+ test("outputs error from orchestrator", async () => {
869
+ mockGetAppByProviderAndClientId = () => ({
870
+ id: "app-1",
871
+ clientId: "x",
872
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
873
+ providerKey: "integration:google",
874
+ createdAt: 0,
875
+ updatedAt: 0,
876
+ });
877
+ mockOrchestrateOAuthConnect = async () => ({
878
+ success: false,
879
+ error: "Something went wrong",
880
+ });
881
+
882
+ const { exitCode, stdout } = await runCli([
883
+ "connections",
884
+ "connect",
885
+ "integration:google",
886
+ "--client-id",
887
+ "x",
888
+ "--json",
889
+ ]);
890
+ expect(exitCode).toBe(1);
891
+ const parsed = JSON.parse(stdout);
892
+ expect(parsed.ok).toBe(false);
893
+ expect(parsed.error).toBe("Something went wrong");
894
+ });
895
+
896
+ test("succeeds when callbackTransport is null (loopback default)", async () => {
897
+ // Provider row has callbackTransport: null — orchestrator should default
898
+ // to loopback and not require a public ingress URL.
899
+ mockGetMostRecentAppByProvider = () => ({
900
+ id: "app-loopback",
901
+ clientId: "loopback-client",
902
+ clientSecretCredentialPath: "oauth_app/app-loopback/client_secret",
903
+ providerKey: "integration:test-loopback",
904
+ createdAt: 0,
905
+ updatedAt: 0,
906
+ });
907
+
908
+ let capturedOpts: Record<string, unknown> | undefined;
909
+ mockOrchestrateOAuthConnect = async (opts) => {
910
+ capturedOpts = opts;
911
+ return {
912
+ success: true,
913
+ deferred: true,
914
+ authUrl: "https://example.com/auth?loopback",
915
+ state: "state-loopback",
916
+ service: "integration:test-loopback",
917
+ };
918
+ };
919
+
920
+ const { exitCode, stdout } = await runCli([
921
+ "connections",
922
+ "connect",
923
+ "integration:test-loopback",
924
+ "--json",
925
+ ]);
926
+ expect(exitCode).toBe(0);
927
+ const parsed = JSON.parse(stdout);
928
+ expect(parsed.ok).toBe(true);
929
+ expect(parsed.deferred).toBe(true);
930
+ expect(capturedOpts).toBeDefined();
931
+ expect(capturedOpts!.clientId).toBe("loopback-client");
932
+ });
933
+
934
+ test("returns ingress URL error when callbackTransport is explicitly gateway", async () => {
935
+ // Provider row has callbackTransport: "gateway" — orchestrator should
936
+ // require a public ingress URL, which is not configured in the test env.
937
+ mockGetMostRecentAppByProvider = () => ({
938
+ id: "app-gateway",
939
+ clientId: "gateway-client",
940
+ clientSecretCredentialPath: "oauth_app/app-gateway/client_secret",
941
+ providerKey: "integration:test-gateway",
942
+ createdAt: 0,
943
+ updatedAt: 0,
944
+ });
945
+
946
+ mockOrchestrateOAuthConnect = async () => ({
947
+ success: false,
948
+ error:
949
+ "oauth2_connect from a non-interactive session requires a public ingress URL. Configure ingress.publicBaseUrl first.",
950
+ });
951
+
952
+ const { exitCode, stdout } = await runCli([
953
+ "connections",
954
+ "connect",
955
+ "integration:test-gateway",
956
+ "--json",
957
+ ]);
958
+ expect(exitCode).toBe(1);
959
+ const parsed = JSON.parse(stdout);
960
+ expect(parsed.ok).toBe(false);
961
+ expect(parsed.error).toContain("requires a public ingress URL");
962
+ });
963
+
964
+ test("fails when client_secret is required but missing", async () => {
965
+ mockGetAppByProviderAndClientId = () => ({
966
+ id: "app-1",
967
+ clientId: "test-id",
968
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
969
+ providerKey: "integration:google",
970
+ createdAt: 0,
971
+ updatedAt: 0,
972
+ });
973
+ mockGetProviderBehavior = () => ({
974
+ setup: {
975
+ requiresClientSecret: true,
976
+ displayName: "Test",
977
+ dashboardUrl: "https://example.com",
978
+ appType: "app",
979
+ },
980
+ });
981
+
982
+ const { exitCode, stdout } = await runCli([
983
+ "connections",
984
+ "connect",
985
+ "integration:google",
986
+ "--client-id",
987
+ "test-id",
988
+ "--json",
989
+ ]);
990
+ expect(exitCode).toBe(1);
991
+ const parsed = JSON.parse(stdout);
992
+ expect(parsed.ok).toBe(false);
993
+ expect(parsed.error).toContain("client_secret");
994
+ expect(parsed.error).toContain("apps upsert");
995
+ });
996
+ });
997
+
998
+ // ---------------------------------------------------------------------------
999
+ // apps upsert --client-secret-credential-path
1000
+ // ---------------------------------------------------------------------------
1001
+
1002
+ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
1003
+ beforeEach(() => {
1004
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1005
+ secureKeyStore = new Map();
1006
+ metadataStore = [];
1007
+ disconnectOAuthProviderCalls = [];
1008
+ disconnectOAuthProviderResult = "not-found";
1009
+ idCounter = 0;
1010
+ mockUpsertAppCalls = [];
1011
+ mockUpsertAppResult = {
1012
+ id: "app-upsert-1",
1013
+ providerKey: "integration:google",
1014
+ clientId: "abc123",
1015
+ createdAt: 1700000000000,
1016
+ updatedAt: 1700000000000,
1017
+ };
1018
+ mockOrchestrateOAuthConnect = async () => ({
1019
+ success: true,
1020
+ deferred: false,
1021
+ grantedScopes: [],
1022
+ });
1023
+ mockGetAppByProviderAndClientId = () => undefined;
1024
+ mockGetMostRecentAppByProvider = () => undefined;
1025
+ mockGetProvider = () => undefined;
1026
+ mockGetProviderBehavior = () => undefined;
1027
+ mockGetSecureKey = () => undefined;
1028
+ mockGetCredentialMetadata = () => undefined;
1029
+ mockUpsertAppImpl = undefined;
1030
+ });
1031
+
1032
+ test("upsert with --client-secret-credential-path passes path to upsertApp", async () => {
1033
+ const { exitCode, stdout } = await runCli([
1034
+ "apps",
1035
+ "upsert",
1036
+ "--provider",
1037
+ "integration:google",
1038
+ "--client-id",
1039
+ "abc123",
1040
+ "--client-secret-credential-path",
1041
+ "custom/path",
1042
+ "--json",
1043
+ ]);
1044
+ expect(exitCode).toBe(0);
1045
+ expect(mockUpsertAppCalls).toHaveLength(1);
1046
+ expect(mockUpsertAppCalls[0]).toEqual({
1047
+ provider: "integration:google",
1048
+ clientId: "abc123",
1049
+ clientSecretOpts: { clientSecretCredentialPath: "custom/path" },
1050
+ });
1051
+ const parsed = JSON.parse(stdout);
1052
+ expect(parsed.id).toBe("app-upsert-1");
1053
+ });
1054
+
1055
+ test("upsert with both --client-secret and --client-secret-credential-path returns error", async () => {
1056
+ const { exitCode, stdout } = await runCli([
1057
+ "apps",
1058
+ "upsert",
1059
+ "--provider",
1060
+ "integration:google",
1061
+ "--client-id",
1062
+ "abc123",
1063
+ "--client-secret",
1064
+ "s3cret",
1065
+ "--client-secret-credential-path",
1066
+ "custom/path",
1067
+ "--json",
1068
+ ]);
1069
+ expect(exitCode).toBe(1);
1070
+ const parsed = JSON.parse(stdout);
1071
+ expect(parsed.ok).toBe(false);
1072
+ expect(parsed.error).toContain(
1073
+ "Cannot provide both --client-secret and --client-secret-credential-path",
1074
+ );
1075
+ // upsertApp should NOT have been called
1076
+ expect(mockUpsertAppCalls).toHaveLength(0);
1077
+ });
1078
+
1079
+ test("upsert with --client-secret passes clientSecretValue to upsertApp", async () => {
1080
+ const { exitCode } = await runCli([
1081
+ "apps",
1082
+ "upsert",
1083
+ "--provider",
1084
+ "integration:google",
1085
+ "--client-id",
1086
+ "abc123",
1087
+ "--client-secret",
1088
+ "s3cret",
1089
+ "--json",
1090
+ ]);
1091
+ expect(exitCode).toBe(0);
1092
+ expect(mockUpsertAppCalls).toHaveLength(1);
1093
+ expect(mockUpsertAppCalls[0]).toEqual({
1094
+ provider: "integration:google",
1095
+ clientId: "abc123",
1096
+ clientSecretOpts: { clientSecretValue: "s3cret" },
1097
+ });
1098
+ });
1099
+
1100
+ test("upsert without any secret option passes undefined", async () => {
1101
+ const { exitCode } = await runCli([
1102
+ "apps",
1103
+ "upsert",
1104
+ "--provider",
1105
+ "integration:google",
1106
+ "--client-id",
1107
+ "abc123",
1108
+ "--json",
1109
+ ]);
1110
+ expect(exitCode).toBe(0);
1111
+ expect(mockUpsertAppCalls).toHaveLength(1);
1112
+ expect(mockUpsertAppCalls[0]).toEqual({
1113
+ provider: "integration:google",
1114
+ clientId: "abc123",
1115
+ clientSecretOpts: undefined,
1116
+ });
1117
+ });
1118
+
1119
+ test("upsert resolves non-prefixed credential path via metadata store", async () => {
1120
+ // The resolution logic splits on the LAST colon, so
1121
+ // "integration:google:client_secret" → service="integration:google", field="client_secret"
1122
+ mockGetCredentialMetadata = (service, field) =>
1123
+ service === "integration:google" && field === "client_secret"
1124
+ ? {
1125
+ credentialId: "cred-1",
1126
+ service: "integration:google",
1127
+ field: "client_secret",
1128
+ allowedTools: [],
1129
+ allowedDomains: [],
1130
+ createdAt: Date.now(),
1131
+ updatedAt: Date.now(),
1132
+ }
1133
+ : undefined;
1134
+
1135
+ const { exitCode, stdout } = await runCli([
1136
+ "apps",
1137
+ "upsert",
1138
+ "--provider",
1139
+ "integration:google",
1140
+ "--client-id",
1141
+ "abc",
1142
+ "--client-secret-credential-path",
1143
+ "integration:google:client_secret",
1144
+ "--json",
1145
+ ]);
1146
+ expect(exitCode).toBe(0);
1147
+ expect(mockUpsertAppCalls).toHaveLength(1);
1148
+ // The non-prefixed path should have been resolved to the full credential key
1149
+ expect(mockUpsertAppCalls[0]).toEqual({
1150
+ provider: "integration:google",
1151
+ clientId: "abc",
1152
+ clientSecretOpts: {
1153
+ clientSecretCredentialPath:
1154
+ "credential/integration:google/client_secret",
1155
+ },
1156
+ });
1157
+ const parsed = JSON.parse(stdout);
1158
+ expect(parsed.id).toBe("app-upsert-1");
1159
+ });
1160
+
1161
+ test("upsert passes prefixed credential path through unchanged", async () => {
1162
+ const { exitCode, stdout } = await runCli([
1163
+ "apps",
1164
+ "upsert",
1165
+ "--provider",
1166
+ "integration:google",
1167
+ "--client-id",
1168
+ "abc",
1169
+ "--client-secret-credential-path",
1170
+ "credential/integration:google/client_secret",
1171
+ "--json",
1172
+ ]);
1173
+ expect(exitCode).toBe(0);
1174
+ expect(mockUpsertAppCalls).toHaveLength(1);
1175
+ // Already-prefixed path should be passed through as-is
1176
+ expect(mockUpsertAppCalls[0]).toEqual({
1177
+ provider: "integration:google",
1178
+ clientId: "abc",
1179
+ clientSecretOpts: {
1180
+ clientSecretCredentialPath:
1181
+ "credential/integration:google/client_secret",
1182
+ },
1183
+ });
1184
+ const parsed = JSON.parse(stdout);
1185
+ expect(parsed.id).toBe("app-upsert-1");
1186
+ });
1187
+
1188
+ test("upsert with invalid credential path returns error when no secret found", async () => {
1189
+ // Override upsertApp to throw when given an unresolvable credential path
1190
+ mockUpsertAppImpl = async (_provider, _clientId, clientSecretOpts) => {
1191
+ throw new Error(
1192
+ `No secret found at credential path: ${clientSecretOpts?.clientSecretCredentialPath}`,
1193
+ );
1194
+ };
1195
+
1196
+ const { exitCode, stdout } = await runCli([
1197
+ "apps",
1198
+ "upsert",
1199
+ "--provider",
1200
+ "integration:google",
1201
+ "--client-id",
1202
+ "abc",
1203
+ "--client-secret-credential-path",
1204
+ "bogus:nonexistent:path",
1205
+ "--json",
1206
+ ]);
1207
+ expect(exitCode).toBe(1);
1208
+ const parsed = JSON.parse(stdout);
1209
+ expect(parsed.ok).toBe(false);
1210
+ expect(parsed.error).toContain("No secret found");
1211
+ });
1212
+ });
1213
+
1214
+ // ---------------------------------------------------------------------------
1215
+ // ping
1216
+ // ---------------------------------------------------------------------------
1217
+
1218
+ describe("assistant oauth connections ping <provider-key>", () => {
1219
+ beforeEach(() => {
1220
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1221
+ secureKeyStore = new Map();
1222
+ metadataStore = [];
1223
+ disconnectOAuthProviderCalls = [];
1224
+ disconnectOAuthProviderResult = "not-found";
1225
+ idCounter = 0;
1226
+ });
1227
+
1228
+ test("returns ok when ping endpoint returns 200", async () => {
1229
+ mockGetProvider = () => ({
1230
+ providerKey: "integration:google",
1231
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1232
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1233
+ tokenUrl: "https://oauth2.googleapis.com/token",
1234
+ defaultScopes: "[]",
1235
+ scopePolicy: "{}",
1236
+ extraParams: null,
1237
+ createdAt: Date.now(),
1238
+ updatedAt: Date.now(),
1239
+ });
1240
+ const originalFetch = globalThis.fetch;
1241
+ globalThis.fetch = (async () =>
1242
+ new Response("{}", { status: 200 })) as unknown as typeof fetch;
1243
+ try {
1244
+ const { exitCode, stdout } = await runCli([
1245
+ "connections",
1246
+ "ping",
1247
+ "integration:google",
1248
+ "--json",
1249
+ ]);
1250
+ expect(exitCode).toBe(0);
1251
+ const parsed = JSON.parse(stdout);
1252
+ expect(parsed.ok).toBe(true);
1253
+ expect(parsed.status).toBe(200);
1254
+ } finally {
1255
+ globalThis.fetch = originalFetch;
1256
+ }
1257
+ });
1258
+
1259
+ test("exits 1 when provider not found", async () => {
1260
+ mockGetProvider = () => undefined;
1261
+ const { exitCode, stdout } = await runCli([
1262
+ "connections",
1263
+ "ping",
1264
+ "integration:unknown",
1265
+ "--json",
1266
+ ]);
1267
+ expect(exitCode).toBe(1);
1268
+ const parsed = JSON.parse(stdout);
1269
+ expect(parsed.ok).toBe(false);
1270
+ expect(parsed.error).toContain("Provider not found");
1271
+ });
1272
+
1273
+ test("exits 1 when no ping URL configured", async () => {
1274
+ mockGetProvider = () => ({
1275
+ providerKey: "telegram",
1276
+ pingUrl: null,
1277
+ authUrl: "urn:manual-token",
1278
+ tokenUrl: "urn:manual-token",
1279
+ defaultScopes: "[]",
1280
+ scopePolicy: "{}",
1281
+ extraParams: null,
1282
+ createdAt: Date.now(),
1283
+ updatedAt: Date.now(),
1284
+ });
1285
+ const { exitCode, stdout } = await runCli([
1286
+ "connections",
1287
+ "ping",
1288
+ "telegram",
1289
+ "--json",
1290
+ ]);
1291
+ expect(exitCode).toBe(1);
1292
+ const parsed = JSON.parse(stdout);
1293
+ expect(parsed.ok).toBe(false);
1294
+ expect(parsed.error).toContain("No ping URL configured");
1295
+ });
1296
+
1297
+ test("exits 1 when ping endpoint returns non-2xx", async () => {
1298
+ mockGetProvider = () => ({
1299
+ providerKey: "integration:google",
1300
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1301
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1302
+ tokenUrl: "https://oauth2.googleapis.com/token",
1303
+ defaultScopes: "[]",
1304
+ scopePolicy: "{}",
1305
+ extraParams: null,
1306
+ createdAt: Date.now(),
1307
+ updatedAt: Date.now(),
1308
+ });
1309
+ const originalFetch = globalThis.fetch;
1310
+ // Use 403 (not 401) — 401 now throws inside withValidToken for retry
1311
+ globalThis.fetch = (async () =>
1312
+ new Response("Forbidden", { status: 403 })) as unknown as typeof fetch;
1313
+ try {
1314
+ const { exitCode, stdout } = await runCli([
1315
+ "connections",
1316
+ "ping",
1317
+ "integration:google",
1318
+ "--json",
1319
+ ]);
1320
+ expect(exitCode).toBe(1);
1321
+ const parsed = JSON.parse(stdout);
1322
+ expect(parsed.ok).toBe(false);
1323
+ expect(parsed.status).toBe(403);
1324
+ } finally {
1325
+ globalThis.fetch = originalFetch;
1326
+ }
1327
+ });
1328
+
1329
+ test("exits 1 when no token exists", async () => {
1330
+ mockWithValidToken = async () => {
1331
+ throw new Error('No access token found for "integration:google".');
1332
+ };
1333
+ mockGetProvider = () => ({
1334
+ providerKey: "integration:google",
1335
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1336
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1337
+ tokenUrl: "https://oauth2.googleapis.com/token",
1338
+ defaultScopes: "[]",
1339
+ scopePolicy: "{}",
1340
+ extraParams: null,
1341
+ createdAt: Date.now(),
1342
+ updatedAt: Date.now(),
1343
+ });
1344
+ const { exitCode, stdout } = await runCli([
1345
+ "connections",
1346
+ "ping",
1347
+ "integration:google",
1348
+ "--json",
1349
+ ]);
1350
+ expect(exitCode).toBe(1);
1351
+ const parsed = JSON.parse(stdout);
1352
+ expect(parsed.ok).toBe(false);
1353
+ expect(parsed.error).toContain("No access token");
1354
+ });
562
1355
  });