@vellumai/assistant 0.8.0 → 0.8.1

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 (692) hide show
  1. package/AGENTS.md +11 -0
  2. package/Dockerfile +5 -4
  3. package/README.md +2 -2
  4. package/docker-entrypoint.sh +16 -0
  5. package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
  6. package/eslint-rules/cli-no-daemon-internals.js +283 -0
  7. package/eslint.config.mjs +12 -0
  8. package/knip.json +2 -1
  9. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
  10. package/openapi.yaml +4847 -1698
  11. package/package.json +3 -1
  12. package/scripts/generate-openapi.ts +52 -4
  13. package/scripts/sync-llm-catalog.ts +165 -0
  14. package/scripts/sync-web-search-catalog.ts +107 -0
  15. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
  17. package/src/__tests__/anthropic-provider.test.ts +92 -2
  18. package/src/__tests__/app-control-flow.test.ts +7 -0
  19. package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
  20. package/src/__tests__/avatar-identity-sync.test.ts +87 -0
  21. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
  22. package/src/__tests__/btw-routes.test.ts +1 -0
  23. package/src/__tests__/call-site-routing-provider.test.ts +172 -45
  24. package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
  25. package/src/__tests__/channel-policy.test.ts +12 -0
  26. package/src/__tests__/checker.test.ts +89 -0
  27. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +35 -7
  28. package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
  29. package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
  30. package/src/__tests__/config-loader-backfill.test.ts +526 -102
  31. package/src/__tests__/config-loader-corrupt.test.ts +68 -0
  32. package/src/__tests__/config-loader-platform-defaults.test.ts +77 -23
  33. package/src/__tests__/config-schema-cmd.test.ts +63 -29
  34. package/src/__tests__/config-schema.test.ts +14 -3
  35. package/src/__tests__/config-set-platform-guard.test.ts +75 -152
  36. package/src/__tests__/config-set-route.test.ts +198 -0
  37. package/src/__tests__/config-watcher.test.ts +6 -0
  38. package/src/__tests__/contacts-tools.test.ts +51 -199
  39. package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
  40. package/src/__tests__/context-search-agent-runner.test.ts +22 -138
  41. package/src/__tests__/context-search-conversations-source.test.ts +42 -16
  42. package/src/__tests__/context-search-fanout.test.ts +20 -157
  43. package/src/__tests__/context-search-memory-v2-source.test.ts +3 -3
  44. package/src/__tests__/context-search-types.test.ts +7 -2
  45. package/src/__tests__/context-window-manager.test.ts +389 -1
  46. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  47. package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
  48. package/src/__tests__/conversation-error.test.ts +38 -0
  49. package/src/__tests__/conversation-fork-crud.test.ts +241 -1
  50. package/src/__tests__/conversation-inference-profile-route.test.ts +14 -14
  51. package/src/__tests__/conversation-init.benchmark.test.ts +1 -0
  52. package/src/__tests__/conversation-lifecycle.test.ts +124 -0
  53. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
  54. package/src/__tests__/conversation-process-callsite.test.ts +21 -1
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +4 -4
  56. package/src/__tests__/conversation-slash-commands.test.ts +194 -2
  57. package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
  58. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  59. package/src/__tests__/daemon-credential-client.test.ts +56 -1
  60. package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
  61. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
  62. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
  63. package/src/__tests__/db-proxy-transaction.test.ts +206 -0
  64. package/src/__tests__/external-plugin-loader.test.ts +458 -0
  65. package/src/__tests__/filing-service.test.ts +23 -3
  66. package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
  67. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  68. package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
  69. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -8
  70. package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
  71. package/src/__tests__/heartbeat-service.test.ts +50 -233
  72. package/src/__tests__/history-repair.test.ts +89 -0
  73. package/src/__tests__/host-app-control-proxy.test.ts +109 -1
  74. package/src/__tests__/host-app-control-routes.test.ts +247 -1
  75. package/src/__tests__/host-browser-proxy.test.ts +416 -20
  76. package/src/__tests__/host-browser-routes.test.ts +325 -33
  77. package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
  78. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
  79. package/src/__tests__/inference-profile-reaper.test.ts +154 -0
  80. package/src/__tests__/inference-profile-session-handler.test.ts +398 -0
  81. package/src/__tests__/inference-profile-session-ipc.test.ts +236 -0
  82. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
  83. package/src/__tests__/install-skill-routing.test.ts +2 -2
  84. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +15 -0
  85. package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
  86. package/src/__tests__/llm-catalog-parity.test.ts +146 -0
  87. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +188 -0
  88. package/src/__tests__/llm-request-log-source-factory.test.ts +124 -0
  89. package/src/__tests__/llm-resolver.test.ts +46 -0
  90. package/src/__tests__/managed-profile-guard.test.ts +131 -2
  91. package/src/__tests__/mcp-auth-routes.test.ts +1 -0
  92. package/src/__tests__/mcp-cli.test.ts +182 -220
  93. package/src/__tests__/mcp-health-check.test.ts +56 -27
  94. package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
  95. package/src/__tests__/message-complete-display-id.test.ts +175 -0
  96. package/src/__tests__/notification-platform-adapter.test.ts +229 -0
  97. package/src/__tests__/oauth-cli.test.ts +38 -2009
  98. package/src/__tests__/oauth-commands-routes.test.ts +711 -0
  99. package/src/__tests__/oauth-connect-routes.test.ts +174 -11
  100. package/src/__tests__/oauth-providers-routes.test.ts +14 -10
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +33 -12
  102. package/src/__tests__/openai-responses-provider.test.ts +17 -0
  103. package/src/__tests__/plugin-bootstrap.test.ts +31 -2
  104. package/src/__tests__/plugin-route-contribution.test.ts +31 -3
  105. package/src/__tests__/plugin-tool-contribution.test.ts +31 -3
  106. package/src/__tests__/plugin-types.test.ts +13 -11
  107. package/src/__tests__/process-message-background-slack.test.ts +46 -0
  108. package/src/__tests__/profile-entry-status.test.ts +43 -0
  109. package/src/__tests__/provider-managed-proxy-integration.test.ts +12 -4
  110. package/src/__tests__/provider-registry-ollama.test.ts +12 -4
  111. package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
  112. package/src/__tests__/relay-server.test.ts +118 -0
  113. package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
  114. package/src/__tests__/schedule-retry.test.ts +56 -4
  115. package/src/__tests__/schedule-routes.test.ts +104 -0
  116. package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
  117. package/src/__tests__/scheduler-recurrence.test.ts +87 -34
  118. package/src/__tests__/scheduler-reuse-conversation.test.ts +161 -5
  119. package/src/__tests__/scheduler-wake.test.ts +0 -63
  120. package/src/__tests__/secret-allowlist.test.ts +1 -0
  121. package/src/__tests__/secret-routes-managed-proxy.test.ts +12 -4
  122. package/src/__tests__/shell-credential-ref.test.ts +95 -3
  123. package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
  124. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  125. package/src/__tests__/skill-load-tool.test.ts +2 -4
  126. package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
  127. package/src/__tests__/suggestion-routes.test.ts +3 -3
  128. package/src/__tests__/sync-message-contract.test.ts +63 -0
  129. package/src/__tests__/task-scheduler.test.ts +88 -23
  130. package/src/__tests__/update-bulletin-job.test.ts +96 -193
  131. package/src/__tests__/usage-cli.test.ts +11 -73
  132. package/src/__tests__/user-plugin-loader.test.ts +145 -0
  133. package/src/__tests__/vercel-config.test.ts +168 -0
  134. package/src/__tests__/web-search-catalog-parity.test.ts +86 -0
  135. package/src/__tests__/web-search.test.ts +303 -2
  136. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
  137. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +58 -0
  138. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +53 -20
  139. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +191 -0
  140. package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
  141. package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
  142. package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
  143. package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
  144. package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
  145. package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
  146. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
  147. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
  148. package/src/acp/__tests__/helpers/which-stub.ts +4 -2
  149. package/src/acp/resolve-agent.test.ts +25 -0
  150. package/src/acp/resolve-agent.ts +13 -2
  151. package/src/acp/session-manager.ts +14 -0
  152. package/src/approvals/guardian-request-resolvers.ts +32 -87
  153. package/src/calls/relay-server.ts +35 -0
  154. package/src/calls/relay-setup-router.ts +36 -0
  155. package/src/calls/types.ts +1 -0
  156. package/src/calls/voice-session-bridge.ts +23 -4
  157. package/src/channels/config.ts +14 -1
  158. package/src/channels/types.ts +1 -0
  159. package/src/cli/AGENTS.md +164 -4
  160. package/src/cli/__tests__/notifications.test.ts +54 -0
  161. package/src/cli/commands/__tests__/avatar.test.ts +540 -0
  162. package/src/cli/commands/__tests__/backup.test.ts +236 -776
  163. package/src/cli/commands/__tests__/cache.test.ts +1 -1
  164. package/src/cli/commands/__tests__/changelog.test.ts +593 -0
  165. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
  166. package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
  167. package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
  168. package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
  169. package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
  170. package/src/cli/commands/__tests__/email-core.test.ts +579 -0
  171. package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
  172. package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
  173. package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
  174. package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
  175. package/src/cli/commands/__tests__/skills.test.ts +563 -0
  176. package/src/cli/commands/__tests__/status.test.ts +249 -0
  177. package/src/cli/commands/__tests__/stt.test.ts +320 -0
  178. package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
  179. package/src/cli/commands/__tests__/tts.test.ts +321 -0
  180. package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
  181. package/src/cli/commands/attachment.ts +8 -3
  182. package/src/cli/commands/audit.ts +95 -64
  183. package/src/cli/commands/auth.ts +61 -58
  184. package/src/cli/commands/avatar.ts +276 -390
  185. package/src/cli/commands/backup.ts +409 -505
  186. package/src/cli/commands/bash.ts +9 -5
  187. package/src/cli/commands/browser.ts +28 -9
  188. package/src/cli/commands/cache.ts +9 -4
  189. package/src/cli/commands/changelog.ts +414 -0
  190. package/src/cli/commands/channel-verification-sessions.ts +238 -317
  191. package/src/cli/commands/clients.ts +8 -3
  192. package/src/cli/commands/completions.ts +9 -9
  193. package/src/cli/commands/config.ts +102 -72
  194. package/src/cli/commands/contacts.ts +575 -696
  195. package/src/cli/commands/conversations-defer.ts +17 -69
  196. package/src/cli/commands/conversations-import.ts +90 -253
  197. package/src/cli/commands/conversations.ts +346 -436
  198. package/src/cli/commands/credential-execution.ts +9 -6
  199. package/src/cli/commands/credentials.ts +456 -736
  200. package/src/cli/commands/domain.ts +128 -206
  201. package/src/cli/commands/email.ts +606 -794
  202. package/src/cli/commands/gateway.ts +8 -1
  203. package/src/cli/commands/image-generation.ts +157 -205
  204. package/src/cli/commands/inference-providers.ts +352 -0
  205. package/src/cli/commands/inference-session.ts +415 -0
  206. package/src/cli/commands/inference.ts +87 -65
  207. package/src/cli/commands/keys.ts +8 -3
  208. package/src/cli/commands/mcp.ts +103 -287
  209. package/src/cli/commands/memory-v2.ts +162 -516
  210. package/src/cli/commands/notifications.ts +33 -7
  211. package/src/cli/commands/oauth/apps.ts +292 -261
  212. package/src/cli/commands/oauth/connect.ts +176 -297
  213. package/src/cli/commands/oauth/disconnect.ts +16 -215
  214. package/src/cli/commands/oauth/index.ts +49 -45
  215. package/src/cli/commands/oauth/mode.ts +43 -199
  216. package/src/cli/commands/oauth/ping.ts +17 -125
  217. package/src/cli/commands/oauth/providers.ts +732 -921
  218. package/src/cli/commands/oauth/request.ts +60 -350
  219. package/src/cli/commands/oauth/shared.ts +11 -121
  220. package/src/cli/commands/oauth/status.ts +31 -121
  221. package/src/cli/commands/oauth/token.ts +13 -55
  222. package/src/cli/commands/pending.ts +19 -10
  223. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
  224. package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
  225. package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
  226. package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
  227. package/src/cli/commands/platform/connect.ts +16 -80
  228. package/src/cli/commands/platform/disconnect.ts +14 -112
  229. package/src/cli/commands/platform/index.ts +177 -246
  230. package/src/cli/commands/routes.ts +153 -336
  231. package/src/cli/commands/sequence.ts +316 -360
  232. package/src/cli/commands/skills.ts +449 -671
  233. package/src/cli/commands/status.ts +58 -37
  234. package/src/cli/commands/stt.ts +94 -262
  235. package/src/cli/commands/task.ts +14 -40
  236. package/src/cli/commands/trust.ts +8 -3
  237. package/src/cli/commands/tts.ts +162 -167
  238. package/src/cli/commands/ui.ts +35 -42
  239. package/src/cli/commands/usage.ts +188 -126
  240. package/src/cli/commands/watchers.ts +8 -3
  241. package/src/cli/commands/webhooks.ts +99 -193
  242. package/src/cli/lib/__tests__/register-command.test.ts +85 -0
  243. package/src/cli/lib/daemon-credential-client.ts +4 -5
  244. package/src/cli/lib/nested-value.ts +44 -0
  245. package/src/cli/lib/open-browser.ts +36 -0
  246. package/src/cli/lib/register-command.ts +19 -0
  247. package/src/cli/lib/time-ago.ts +34 -0
  248. package/src/cli/program.ts +2 -4
  249. package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
  250. package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
  251. package/src/cli/utils/conversation-id.ts +30 -0
  252. package/src/cli/utils/parse-duration.ts +41 -0
  253. package/src/config/acp-defaults.test.ts +5 -1
  254. package/src/config/acp-defaults.ts +11 -4
  255. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  256. package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
  257. package/src/config/bundled-skills/contacts/SKILL.md +12 -45
  258. package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
  259. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
  260. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
  261. package/src/config/bundled-tool-registry.ts +0 -2
  262. package/src/config/feature-flag-registry.json +16 -0
  263. package/src/config/llm-resolver.ts +16 -1
  264. package/src/config/loader.ts +76 -14
  265. package/src/config/raw-config-utils.ts +2 -30
  266. package/src/config/schema.ts +4 -0
  267. package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
  268. package/src/config/schemas/call-site-catalog.ts +29 -7
  269. package/src/config/schemas/llm-request-logs.ts +57 -0
  270. package/src/config/schemas/llm.ts +52 -2
  271. package/src/config/schemas/memory-retrospective.ts +48 -0
  272. package/src/config/schemas/memory-v2.ts +32 -1
  273. package/src/config/schemas/memory.ts +4 -0
  274. package/src/config/schemas/services.ts +15 -12
  275. package/src/config/seed-inference-profiles.ts +195 -134
  276. package/src/contacts/contact-store.ts +0 -61
  277. package/src/context/window-manager.ts +191 -5
  278. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +79 -0
  279. package/src/daemon/__tests__/conversation-tool-setup.test.ts +109 -4
  280. package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
  281. package/src/daemon/approval-generators.ts +23 -29
  282. package/src/daemon/config-watcher.ts +2 -0
  283. package/src/daemon/conversation-agent-loop-handlers.ts +24 -0
  284. package/src/daemon/conversation-agent-loop.ts +127 -97
  285. package/src/daemon/conversation-error.ts +21 -0
  286. package/src/daemon/conversation-lifecycle.ts +46 -5
  287. package/src/daemon/conversation-process.ts +36 -19
  288. package/src/daemon/conversation-runtime-assembly.ts +14 -5
  289. package/src/daemon/conversation-slash.ts +175 -23
  290. package/src/daemon/conversation-store.ts +17 -10
  291. package/src/daemon/conversation-surfaces.ts +76 -12
  292. package/src/daemon/conversation-tool-setup.ts +24 -14
  293. package/src/daemon/conversation.ts +48 -9
  294. package/src/daemon/external-plugins-bootstrap.ts +18 -8
  295. package/src/daemon/guardian-action-generators.ts +7 -22
  296. package/src/daemon/handlers/config-model.ts +8 -126
  297. package/src/daemon/handlers/config-slack-channel.ts +10 -7
  298. package/src/daemon/handlers/config-vercel.ts +3 -1
  299. package/src/daemon/handlers/skills.ts +84 -5
  300. package/src/daemon/history-repair.ts +33 -6
  301. package/src/daemon/host-app-control-proxy.ts +44 -19
  302. package/src/daemon/host-bash-proxy.ts +85 -158
  303. package/src/daemon/host-browser-proxy.ts +96 -35
  304. package/src/daemon/host-proxy-base.ts +13 -1
  305. package/src/daemon/host-proxy-preactivation.ts +25 -1
  306. package/src/daemon/identity-helpers.ts +19 -0
  307. package/src/daemon/lifecycle.ts +42 -43
  308. package/src/daemon/meet-host-supervisor.ts +15 -15
  309. package/src/daemon/memory-v2-startup.ts +9 -2
  310. package/src/daemon/message-protocol.ts +6 -0
  311. package/src/daemon/message-types/bookmarks.ts +18 -0
  312. package/src/daemon/message-types/conversations.ts +12 -9
  313. package/src/daemon/message-types/messages.ts +9 -1
  314. package/src/daemon/message-types/sync.ts +60 -0
  315. package/src/daemon/pkb-reminder-builder.test.ts +54 -13
  316. package/src/daemon/pkb-reminder-builder.ts +21 -7
  317. package/src/daemon/process-message.ts +56 -23
  318. package/src/daemon/server.ts +23 -18
  319. package/src/daemon/shutdown-handlers.ts +0 -2
  320. package/src/daemon/tool-setup-types.ts +9 -0
  321. package/src/daemon/tool-side-effects.ts +6 -4
  322. package/src/daemon/wake-target-adapter.ts +11 -0
  323. package/src/export/transcript-formatter.ts +61 -2
  324. package/src/filing/filing-service.ts +40 -53
  325. package/src/heartbeat/__tests__/heartbeat-service.test.ts +359 -0
  326. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  327. package/src/heartbeat/heartbeat-service.ts +148 -127
  328. package/src/home/__tests__/feed-types.test.ts +63 -131
  329. package/src/home/__tests__/feed-writer.test.ts +77 -278
  330. package/src/home/__tests__/post-connect-feed.test.ts +9 -12
  331. package/src/home/feed-types.ts +19 -73
  332. package/src/home/feed-writer.ts +25 -156
  333. package/src/home/post-connect-feed.ts +1 -3
  334. package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
  335. package/src/ipc/__tests__/email-ipc.test.ts +506 -0
  336. package/src/ipc/__tests__/exit-helper.test.ts +104 -0
  337. package/src/ipc/__tests__/streaming-client.test.ts +237 -0
  338. package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
  339. package/src/ipc/assistant-server.ts +55 -6
  340. package/src/ipc/cli-client.ts +370 -50
  341. package/src/ipc/routes/db-proxy-transaction.ts +151 -0
  342. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
  343. package/src/ipc/skill-routes/events.ts +30 -3
  344. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
  345. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
  346. package/src/live-voice/live-voice-session-manager.ts +11 -4
  347. package/src/live-voice/live-voice-session.ts +14 -6
  348. package/src/memory/__tests__/bookmark-crud.test.ts +258 -0
  349. package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
  350. package/src/memory/__tests__/conversation-types.test.ts +36 -0
  351. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
  352. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
  353. package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
  354. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +213 -0
  355. package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
  356. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
  357. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
  358. package/src/memory/bookmark-crud.ts +179 -0
  359. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
  360. package/src/memory/context-search/agent-protocol.ts +5 -1
  361. package/src/memory/context-search/agent-runner.ts +60 -85
  362. package/src/memory/context-search/limits.ts +1 -4
  363. package/src/memory/context-search/search.ts +23 -113
  364. package/src/memory/context-search/sources/conversations.ts +18 -6
  365. package/src/memory/context-search/sources/memory-v2.ts +39 -14
  366. package/src/memory/context-search/sources/memory.ts +7 -0
  367. package/src/memory/context-search/sources/workspace.ts +13 -10
  368. package/src/memory/context-search/types.ts +1 -1
  369. package/src/memory/conversation-bootstrap.ts +11 -0
  370. package/src/memory/conversation-crud.ts +312 -10
  371. package/src/memory/conversation-queries.ts +9 -5
  372. package/src/memory/conversation-title-service.ts +1 -0
  373. package/src/memory/conversation-types.ts +16 -0
  374. package/src/memory/db-init.ts +14 -0
  375. package/src/memory/embedding-backend.ts +2 -1
  376. package/src/memory/embedding-runtime-manager.ts +1 -2
  377. package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
  378. package/src/memory/graph/conversation-graph-memory.ts +76 -5
  379. package/src/memory/graph/extraction.ts +4 -0
  380. package/src/memory/graph/graph-memory-state-store.ts +16 -3
  381. package/src/memory/graph/tool-handlers.ts +17 -7
  382. package/src/memory/graph/tools.ts +44 -5
  383. package/src/memory/indexer.ts +17 -0
  384. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +13 -15
  385. package/src/memory/jobs/embed-concept-page.ts +45 -9
  386. package/src/memory/jobs-store.ts +51 -1
  387. package/src/memory/jobs-worker.ts +52 -3
  388. package/src/memory/llm-request-log-source-clickhouse.ts +317 -0
  389. package/src/memory/llm-request-log-source-local.ts +26 -0
  390. package/src/memory/llm-request-log-source.ts +97 -0
  391. package/src/memory/llm-request-log-store.ts +1 -1
  392. package/src/memory/memory-retrospective-constants.ts +13 -0
  393. package/src/memory/memory-retrospective-enqueue.ts +114 -0
  394. package/src/memory/memory-retrospective-job.ts +351 -0
  395. package/src/memory/memory-retrospective-startup-cleanup.ts +108 -0
  396. package/src/memory/memory-retrospective-state.ts +162 -0
  397. package/src/memory/memory-retrospective-trigger-check.ts +91 -0
  398. package/src/memory/memory-v2-activation-log-store.ts +49 -5
  399. package/src/memory/memory-v2-concept-frequency.ts +4 -0
  400. package/src/memory/message-content.ts +38 -1
  401. package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
  402. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
  403. package/src/memory/migrations/229-delete-private-conversations.test.ts +70 -1
  404. package/src/memory/migrations/229-delete-private-conversations.ts +12 -0
  405. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
  406. package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
  407. package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
  408. package/src/memory/migrations/242-message-bookmarks.ts +38 -0
  409. package/src/memory/migrations/243-provider-connections.ts +68 -0
  410. package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
  411. package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
  412. package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
  413. package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
  414. package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
  415. package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
  416. package/src/memory/migrations/index.ts +7 -0
  417. package/src/memory/published-pages-store.ts +16 -0
  418. package/src/memory/schema/bookmarks.ts +38 -0
  419. package/src/memory/schema/conversations.ts +2 -0
  420. package/src/memory/schema/index.ts +2 -0
  421. package/src/memory/schema/inference.ts +29 -0
  422. package/src/memory/schema/memory-core.ts +9 -0
  423. package/src/memory/search/semantic.ts +1 -4
  424. package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
  425. package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
  426. package/src/memory/v2/__tests__/activation.test.ts +11 -4
  427. package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
  428. package/src/memory/v2/__tests__/consolidation-job.test.ts +123 -135
  429. package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
  430. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
  431. package/src/memory/v2/__tests__/injection.test.ts +628 -10
  432. package/src/memory/v2/__tests__/migration.test.ts +7 -3
  433. package/src/memory/v2/__tests__/page-index.test.ts +277 -0
  434. package/src/memory/v2/__tests__/page-store.test.ts +14 -1
  435. package/src/memory/v2/__tests__/prompts-router.test.ts +257 -0
  436. package/src/memory/v2/__tests__/qdrant.test.ts +72 -0
  437. package/src/memory/v2/__tests__/reranker.test.ts +4 -4
  438. package/src/memory/v2/__tests__/router.test.ts +516 -0
  439. package/src/memory/v2/__tests__/sim.test.ts +45 -1
  440. package/src/memory/v2/__tests__/skill-store.test.ts +58 -3
  441. package/src/memory/v2/__tests__/static-context.test.ts +7 -22
  442. package/src/memory/v2/__tests__/sweep-job.test.ts +95 -0
  443. package/src/memory/v2/activation-store.ts +34 -5
  444. package/src/memory/v2/activation.ts +40 -27
  445. package/src/memory/v2/backfill-jobs.ts +17 -84
  446. package/src/memory/v2/consolidation-job.ts +85 -78
  447. package/src/memory/v2/frontmatter-sweep.ts +91 -0
  448. package/src/memory/v2/injection.ts +440 -109
  449. package/src/memory/v2/migration.ts +117 -20
  450. package/src/memory/v2/page-index.ts +191 -0
  451. package/src/memory/v2/page-store.ts +3 -0
  452. package/src/memory/v2/prompts/consolidation.ts +9 -7
  453. package/src/memory/v2/prompts/router.ts +192 -0
  454. package/src/memory/v2/qdrant.ts +100 -87
  455. package/src/memory/v2/reranker.ts +14 -7
  456. package/src/memory/v2/router.ts +322 -0
  457. package/src/memory/v2/sim.ts +25 -12
  458. package/src/memory/v2/skill-store.ts +118 -29
  459. package/src/memory/v2/static-context.ts +16 -9
  460. package/src/memory/v2/sweep-job.ts +122 -96
  461. package/src/memory/v2/types.ts +10 -6
  462. package/src/memory/validation.ts +13 -0
  463. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
  464. package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
  465. package/src/notifications/__tests__/signal-registry.test.ts +17 -0
  466. package/src/notifications/adapters/platform.ts +171 -0
  467. package/src/notifications/conversation-pairing.ts +2 -2
  468. package/src/notifications/copy-composer.ts +15 -0
  469. package/src/notifications/destination-resolver.ts +21 -0
  470. package/src/notifications/emit-signal.ts +28 -1
  471. package/src/notifications/home-feed-side-effect.ts +111 -0
  472. package/src/notifications/signal.ts +5 -0
  473. package/src/permissions/checker.ts +12 -0
  474. package/src/permissions/ipc-risk-types.ts +2 -0
  475. package/src/plugin-api/index.ts +13 -0
  476. package/src/plugin-api/package.json +12 -0
  477. package/src/plugin-api/types.ts +62 -0
  478. package/src/plugins/defaults/injectors.ts +19 -3
  479. package/src/plugins/external-plugin-loader.ts +294 -0
  480. package/src/plugins/types.ts +46 -30
  481. package/src/plugins/user-loader.ts +64 -41
  482. package/src/proactive-artifact/job.test.ts +12 -4
  483. package/src/proactive-artifact/job.ts +4 -0
  484. package/src/proactive-artifact/trigger-state.test.ts +9 -0
  485. package/src/proactive-artifact/trigger-state.ts +4 -0
  486. package/src/prompts/__tests__/system-prompt.test.ts +105 -0
  487. package/src/prompts/system-prompt.ts +22 -1
  488. package/src/prompts/update-bulletin-job.ts +61 -73
  489. package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
  490. package/src/providers/__tests__/inference.test.ts +288 -0
  491. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  492. package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
  493. package/src/providers/__tests__/retry-callsite.test.ts +14 -32
  494. package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
  495. package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
  496. package/src/providers/anthropic/client.ts +95 -26
  497. package/src/providers/call-site-routing.ts +94 -16
  498. package/src/providers/connection-resolution.ts +163 -0
  499. package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
  500. package/src/providers/inference/adapter-factory.ts +173 -0
  501. package/src/providers/inference/auth.ts +112 -0
  502. package/src/providers/inference/backfill.ts +196 -0
  503. package/src/providers/inference/connections.ts +356 -0
  504. package/src/providers/inference/resolve-auth.ts +65 -0
  505. package/src/providers/model-catalog.ts +104 -6
  506. package/src/providers/openai/responses-provider.ts +4 -2
  507. package/src/providers/provider-env-vars.ts +17 -7
  508. package/src/providers/provider-secret-catalog.ts +49 -30
  509. package/src/providers/provider-send-message.ts +41 -20
  510. package/src/providers/registry.ts +143 -159
  511. package/src/providers/retry.ts +18 -10
  512. package/src/providers/search-provider-catalog.ts +121 -0
  513. package/src/runtime/AGENTS.md +18 -5
  514. package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
  515. package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
  516. package/src/runtime/actor-trust-resolver.ts +32 -10
  517. package/src/runtime/agent-wake.ts +35 -6
  518. package/src/runtime/assistant-event-hub.ts +3 -85
  519. package/src/runtime/auth/route-policy.ts +303 -8
  520. package/src/runtime/auth/same-actor.ts +2 -0
  521. package/src/runtime/background-job-runner.ts +339 -0
  522. package/src/runtime/btw-sidechain.ts +1 -0
  523. package/src/runtime/http-router.ts +36 -1
  524. package/src/runtime/http-server.ts +31 -5
  525. package/src/runtime/http-types.ts +2 -0
  526. package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
  527. package/src/runtime/middleware/request-logger.ts +62 -1
  528. package/src/runtime/pre-first-message-gate.ts +83 -0
  529. package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
  530. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +251 -0
  531. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
  532. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +315 -0
  533. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +189 -0
  534. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
  535. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
  536. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +4 -4
  537. package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
  538. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
  539. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  540. package/src/runtime/routes/acp-routes.ts +10 -8
  541. package/src/runtime/routes/app-management-routes.ts +228 -3
  542. package/src/runtime/routes/approval-routes.ts +0 -18
  543. package/src/runtime/routes/audit-routes.ts +43 -0
  544. package/src/runtime/routes/auth-routes.ts +72 -0
  545. package/src/runtime/routes/avatar-routes.ts +273 -20
  546. package/src/runtime/routes/backup-routes.ts +406 -2
  547. package/src/runtime/routes/bookmark-routes.ts +154 -0
  548. package/src/runtime/routes/channel-verification-routes.ts +2 -1
  549. package/src/runtime/routes/contact-routes.ts +0 -160
  550. package/src/runtime/routes/conversation-cli-routes.ts +192 -0
  551. package/src/runtime/routes/conversation-management-routes.ts +30 -43
  552. package/src/runtime/routes/conversation-query-routes.ts +334 -86
  553. package/src/runtime/routes/conversation-routes.ts +31 -10
  554. package/src/runtime/routes/conversations-import-routes.ts +229 -0
  555. package/src/runtime/routes/credential-routes.ts +540 -0
  556. package/src/runtime/routes/debug-routes.ts +2 -2
  557. package/src/runtime/routes/document-pdf-renderer.ts +5 -1
  558. package/src/runtime/routes/domain-routes.ts +167 -0
  559. package/src/runtime/routes/email-routes.ts +603 -0
  560. package/src/runtime/routes/errors.ts +2 -2
  561. package/src/runtime/routes/events-routes.ts +192 -0
  562. package/src/runtime/routes/home-feed-routes.ts +6 -78
  563. package/src/runtime/routes/host-app-control-routes.ts +44 -2
  564. package/src/runtime/routes/host-browser-routes.ts +103 -22
  565. package/src/runtime/routes/http-adapter.ts +2 -0
  566. package/src/runtime/routes/identity-routes.ts +5 -0
  567. package/src/runtime/routes/image-generation-routes.ts +99 -0
  568. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +137 -1
  569. package/src/runtime/routes/inbound-stages/background-dispatch.ts +87 -7
  570. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
  571. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -4
  572. package/src/runtime/routes/index.ts +36 -0
  573. package/src/runtime/routes/inference-profile-session-handler.ts +312 -0
  574. package/src/runtime/routes/inference-profile-session-reaper.ts +98 -0
  575. package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
  576. package/src/runtime/routes/inference-provider-connection-routes.ts +317 -0
  577. package/src/runtime/routes/inference-send-routes.ts +115 -0
  578. package/src/runtime/routes/integrations/twilio.ts +1 -0
  579. package/src/runtime/routes/mcp-auth-routes.ts +283 -9
  580. package/src/runtime/routes/memory-v2-routes.ts +13 -398
  581. package/src/runtime/routes/notification-routes.ts +2 -0
  582. package/src/runtime/routes/oauth-apps.ts +112 -7
  583. package/src/runtime/routes/oauth-commands-routes.ts +1007 -0
  584. package/src/runtime/routes/oauth-connect-routes.ts +67 -5
  585. package/src/runtime/routes/oauth-providers.ts +298 -8
  586. package/src/runtime/routes/platform-routes.ts +336 -0
  587. package/src/runtime/routes/playground/inject-failures.ts +2 -1
  588. package/src/runtime/routes/playground/reset-circuit.ts +2 -1
  589. package/src/runtime/routes/playground/state.ts +2 -1
  590. package/src/runtime/routes/publish-routes.ts +221 -0
  591. package/src/runtime/routes/schedule-routes.ts +82 -0
  592. package/src/runtime/routes/sequence-routes.ts +291 -0
  593. package/src/runtime/routes/settings-routes.ts +2 -10
  594. package/src/runtime/routes/skills-routes.ts +31 -1
  595. package/src/runtime/routes/stt-routes.ts +240 -3
  596. package/src/runtime/routes/surface-action-routes.ts +43 -7
  597. package/src/runtime/routes/tts-routes.ts +67 -0
  598. package/src/runtime/routes/types.ts +32 -0
  599. package/src/runtime/routes/user-routes-cli.ts +243 -0
  600. package/src/runtime/routes/webhook-routes.ts +165 -0
  601. package/src/runtime/sync/resource-sync-events.ts +25 -0
  602. package/src/runtime/sync/sync-publisher.test.ts +105 -0
  603. package/src/runtime/sync/sync-publisher.ts +21 -0
  604. package/src/schedule/scheduler.ts +200 -123
  605. package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
  606. package/src/security/secret-patterns.ts +3 -0
  607. package/src/sequence/engine.ts +38 -40
  608. package/src/subagent/manager.ts +20 -15
  609. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
  610. package/src/tools/browser/browser-execution.ts +15 -4
  611. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
  612. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
  613. package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
  614. package/src/tools/browser/cdp-client/factory.ts +66 -5
  615. package/src/tools/browser/runtime-check.ts +77 -0
  616. package/src/tools/memory/register.test.ts +3 -3
  617. package/src/tools/memory/register.ts +9 -1
  618. package/src/tools/network/__tests__/web-search.test.ts +156 -0
  619. package/src/tools/network/web-search.ts +280 -37
  620. package/src/tools/permission-checker.ts +13 -5
  621. package/src/tools/subagent/spawn.ts +3 -3
  622. package/src/tools/terminal/shell.ts +44 -0
  623. package/src/usage/attribution.ts +3 -2
  624. package/src/util/pricing.ts +86 -160
  625. package/src/watcher/__tests__/engine.test.ts +301 -0
  626. package/src/watcher/constants.ts +7 -0
  627. package/src/watcher/engine.ts +90 -90
  628. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
  629. package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
  630. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +28 -4
  631. package/src/workspace/migrations/069-seed-onboarding-threads.ts +8 -2
  632. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +104 -0
  633. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +93 -0
  634. package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
  635. package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
  636. package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
  637. package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
  638. package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
  639. package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
  640. package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
  641. package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
  642. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
  643. package/src/workspace/migrations/registry.ts +22 -0
  644. package/src/workspace/migrations/runner.ts +13 -2
  645. package/src/workspace/migrations/types.ts +13 -3
  646. package/src/workspace/provider-commit-message-generator.ts +3 -2
  647. package/src/__tests__/context-search-pkb-source.test.ts +0 -498
  648. package/src/__tests__/credentials-cli.test.ts +0 -1225
  649. package/src/__tests__/memory-admin-recall.test.ts +0 -213
  650. package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
  651. package/src/cli/commands/__tests__/email-download.test.ts +0 -260
  652. package/src/cli/commands/__tests__/email-list.test.ts +0 -216
  653. package/src/cli/commands/__tests__/email-register.test.ts +0 -186
  654. package/src/cli/commands/__tests__/email-send.test.ts +0 -416
  655. package/src/cli/commands/__tests__/email-status.test.ts +0 -185
  656. package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
  657. package/src/cli/commands/__tests__/routes.test.ts +0 -562
  658. package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
  659. package/src/cli/commands/autonomy.ts +0 -365
  660. package/src/cli/commands/memory.ts +0 -424
  661. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -947
  662. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
  663. package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
  664. package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
  665. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
  666. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
  667. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
  668. package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
  669. package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
  670. package/src/cli/lib/daemon-avatar-client.ts +0 -37
  671. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
  672. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
  673. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
  674. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
  675. package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
  676. package/src/home/__tests__/emit-feed-event.test.ts +0 -169
  677. package/src/home/__tests__/feed-population-integration.test.ts +0 -312
  678. package/src/home/__tests__/feed-scheduler.test.ts +0 -222
  679. package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
  680. package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
  681. package/src/home/__tests__/rollup-producer.test.ts +0 -507
  682. package/src/home/assistant-feed-authoring.ts +0 -135
  683. package/src/home/emit-feed-event.ts +0 -169
  684. package/src/home/feed-scheduler.ts +0 -281
  685. package/src/home/platform-gmail-digest.ts +0 -163
  686. package/src/home/rewrite-command-preview.ts +0 -66
  687. package/src/home/rewrite-feed-title.ts +0 -58
  688. package/src/home/rollup-producer.ts +0 -426
  689. package/src/memory/admin.ts +0 -326
  690. package/src/memory/context-search/sources/pkb.ts +0 -476
  691. package/src/memory/graph/compaction.ts +0 -299
  692. /package/src/cli/{commands → lib}/cache-fs.ts +0 -0
@@ -1,382 +1,32 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
1
+ /**
2
+ * CLI plumbing tests for `assistant/src/cli/commands/oauth/`.
3
+ *
4
+ * The oauth CLI commands are thin wrappers around `cliIpcCall(...)`; daemon
5
+ * route handlers in `runtime/routes/oauth-commands-routes.ts` execute the
6
+ * actual work. Tests here focus on argument handling and helpers that live
7
+ * entirely in the CLI process.
8
+ *
9
+ * What lives here today:
10
+ * - `requirePlatformConnection` helper — gates managed-mode operations on
11
+ * a connected platform; used by multiple oauth subcommands.
12
+ *
13
+ * Daemon-side coverage of the IPC endpoints lives in
14
+ * `oauth-commands-routes.test.ts`. Underlying store and token-refresh logic
15
+ * is covered by `oauth-store.test.ts` and `credential-vault.test.ts`.
16
+ *
17
+ * Follow-up opportunities for CLI-layer coverage:
18
+ * - `exitFromIpcResult` exit-code mapping
19
+ * - `shouldOutputJson` / `writeOutput` output formatting
20
+ * - `oauth token` shell-lockdown guard (`VELLUM_UNTRUSTED_SHELL=1`)
21
+ * - per-subcommand argument parsing & help text
22
+ */
23
+
24
+ import { describe, expect, mock, test } from "bun:test";
2
25
 
3
26
  import { Command } from "commander";
4
27
 
5
- // ---------------------------------------------------------------------------
6
- // Mock state
7
- // ---------------------------------------------------------------------------
8
-
9
- let mockWithValidToken: <T>(
10
- service: string,
11
- cb: (token: string) => Promise<T>,
12
- ) => Promise<T>;
13
-
14
- let mockListProviders: () => Array<Record<string, unknown>> = () => [];
15
- const secureKeyStore = new Map<string, string>();
16
- const metadataStore: Array<{
17
- credentialId: string;
18
- service: string;
19
- field: string;
20
- allowedTools: string[];
21
- allowedDomains: string[];
22
- createdAt: number;
23
- updatedAt: number;
24
- }> = [];
25
- const disconnectOAuthProviderCalls: string[] = [];
26
- const disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
27
- "not-found";
28
-
29
- // In-memory provider store used by registerProvider/updateProvider/getProvider
30
- // mocks below. Tests that exercise the providers register/update/get commands
31
- // can read and write through this map directly.
32
- const mockProviderStore = new Map<string, Record<string, unknown>>();
33
-
34
- // App upsert mock state
35
- let mockUpsertAppCalls: Array<{
36
- provider: string;
37
- clientId: string;
38
- clientSecretOpts?: {
39
- clientSecretValue?: string;
40
- clientSecretCredentialPath?: string;
41
- };
42
- }> = [];
43
- let mockUpsertAppResult: Record<string, unknown> = {
44
- id: "app-upsert-1",
45
- provider: "test",
46
- clientId: "test-client-id",
47
- createdAt: 1700000000000,
48
- updatedAt: 1700000000000,
49
- };
50
- let mockUpsertAppImpl:
51
- | ((
52
- provider: string,
53
- clientId: string,
54
- clientSecretOpts?: {
55
- clientSecretValue?: string;
56
- clientSecretCredentialPath?: string;
57
- },
58
- ) => Promise<Record<string, unknown>>)
59
- | undefined;
60
-
61
- // Transitive mock state (connect-orchestrator, etc.)
62
- let mockOrchestrateOAuthConnect: (
63
- opts: Record<string, unknown>,
64
- ) => Promise<Record<string, unknown>>;
65
- let mockCliIpcCall: (
66
- operationId: string,
67
- params?: Record<string, unknown>,
68
- ) => Promise<Record<string, unknown>> = async () => ({
69
- ok: false,
70
- error: "Could not connect to assistant daemon (test default)",
71
- });
72
- let mockGetAppByProviderAndClientId: (
73
- provider: string,
74
- clientId: string,
75
- ) => Record<string, unknown> | undefined = () => undefined;
76
- let mockGetMostRecentAppByProvider: (
77
- provider: string,
78
- ) => Record<string, unknown> | undefined = () => undefined;
79
- let mockGetProvider: (
80
- provider: string,
81
- ) => Record<string, unknown> | undefined = () => undefined;
82
- let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
83
- let mockResolveOAuthConnection: (
84
- provider: string,
85
- options?: Record<string, unknown>,
86
- ) => Promise<{
87
- request: (req: Record<string, unknown>) => Promise<{
88
- status: number;
89
- headers: Record<string, string>;
90
- body: unknown;
91
- }>;
92
- withToken: <T>(fn: (token: string) => Promise<T>) => Promise<T>;
93
- id: string;
94
- provider: string;
95
- accountInfo: string | null;
96
- }> = async () => {
97
- throw new Error("resolveOAuthConnection not configured in test");
98
- };
99
- let mockGetCredentialMetadata: (
100
- service: string,
101
- field: string,
102
- ) => Record<string, unknown> | undefined = () => undefined;
103
- let mockPlatformClientCreate: () => Promise<Record<
104
- string,
105
- unknown
106
- > | null> = async () => null;
107
-
108
- // ---------------------------------------------------------------------------
109
- // Mock token-manager
110
- // ---------------------------------------------------------------------------
111
-
112
- mock.module("../security/token-manager.js", () => ({
113
- withValidToken: <T>(
114
- service: string,
115
- cb: (token: string) => Promise<T>,
116
- ): Promise<T> => mockWithValidToken(service, cb),
117
- // Stubs for any transitive imports that reference other exports:
118
- TokenExpiredError: class TokenExpiredError extends Error {
119
- constructor(
120
- public readonly service: string,
121
- message?: string,
122
- ) {
123
- super(message ?? `Token expired for "${service}".`);
124
- this.name = "TokenExpiredError";
125
- }
126
- },
127
- }));
128
-
129
- // ---------------------------------------------------------------------------
130
- // Mock oauth-store
131
- // ---------------------------------------------------------------------------
132
-
133
- mock.module("../oauth/oauth-store.js", () => ({
134
- disconnectOAuthProvider: async (
135
- provider: string,
136
- ): Promise<"disconnected" | "not-found" | "error"> => {
137
- disconnectOAuthProviderCalls.push(provider);
138
- return disconnectOAuthProviderResult;
139
- },
140
- getConnection: () => undefined,
141
- getConnectionByProvider: () => undefined,
142
- listConnections: () => [],
143
- deleteConnection: () => false,
144
- // Stubs required by apps.ts and providers.ts (transitively loaded via oauth/index.ts)
145
- upsertApp: async (
146
- provider: string,
147
- clientId: string,
148
- clientSecretOpts?: {
149
- clientSecretValue?: string;
150
- clientSecretCredentialPath?: string;
151
- },
152
- ) => {
153
- if (mockUpsertAppImpl) {
154
- return mockUpsertAppImpl(provider, clientId, clientSecretOpts);
155
- }
156
- mockUpsertAppCalls.push({ provider, clientId, clientSecretOpts });
157
- return mockUpsertAppResult;
158
- },
159
- getApp: () => undefined,
160
- getAppByProviderAndClientId: (provider: string, clientId: string) =>
161
- mockGetAppByProviderAndClientId(provider, clientId),
162
- getMostRecentAppByProvider: (provider: string) =>
163
- mockGetMostRecentAppByProvider(provider),
164
- listApps: () => [],
165
- deleteApp: async () => false,
166
- getProvider: (provider: string) => {
167
- // If the test has plugged in a custom mockGetProvider, prefer that.
168
- const custom = mockGetProvider(provider);
169
- if (custom !== undefined) return custom;
170
- return mockProviderStore.get(provider);
171
- },
172
- listProviders: () => mockListProviders(),
173
- registerProvider: (params: Record<string, unknown>) => {
174
- const now = Date.now();
175
- const row: Record<string, unknown> = {
176
- provider: params.provider,
177
- authorizeUrl: params.authorizeUrl,
178
- tokenExchangeUrl: params.tokenExchangeUrl,
179
- refreshUrl: (params.refreshUrl as string | undefined) ?? null,
180
- tokenEndpointAuthMethod:
181
- (params.tokenEndpointAuthMethod as string | undefined) ||
182
- "client_secret_post",
183
- tokenExchangeBodyFormat:
184
- (params.tokenExchangeBodyFormat as string | undefined) ?? "form",
185
- userinfoUrl: params.userinfoUrl ?? null,
186
- baseUrl: params.baseUrl ?? null,
187
- defaultScopes: JSON.stringify(params.defaultScopes ?? []),
188
- availableScopes: params.availableScopes
189
- ? JSON.stringify(params.availableScopes)
190
- : null,
191
- scopeSeparator: (params.scopeSeparator as string | undefined) ?? " ",
192
- authorizeParams: params.authorizeParams
193
- ? JSON.stringify(params.authorizeParams)
194
- : null,
195
- pingUrl: params.pingUrl ?? null,
196
- pingMethod: params.pingMethod ?? null,
197
- pingHeaders: params.pingHeaders
198
- ? JSON.stringify(params.pingHeaders)
199
- : null,
200
- pingBody:
201
- params.pingBody !== undefined ? JSON.stringify(params.pingBody) : null,
202
- revokeUrl: (params.revokeUrl as string | undefined) ?? null,
203
- revokeBodyTemplate: params.revokeBodyTemplate
204
- ? JSON.stringify(params.revokeBodyTemplate)
205
- : null,
206
- managedServiceConfigKey: params.managedServiceConfigKey ?? null,
207
- displayLabel: params.displayLabel ?? null,
208
- description: params.description ?? null,
209
- dashboardUrl: params.dashboardUrl ?? null,
210
- logoUrl: params.logoUrl ?? null,
211
- clientIdPlaceholder: params.clientIdPlaceholder ?? null,
212
- requiresClientSecret: params.requiresClientSecret ?? 1,
213
- loopbackPort: params.loopbackPort ?? null,
214
- injectionTemplates: params.injectionTemplates
215
- ? JSON.stringify(params.injectionTemplates)
216
- : null,
217
- appType: params.appType ?? null,
218
- setupNotes: params.setupNotes ? JSON.stringify(params.setupNotes) : null,
219
- identityUrl: params.identityUrl ?? null,
220
- identityMethod: params.identityMethod ?? null,
221
- identityHeaders: params.identityHeaders
222
- ? JSON.stringify(params.identityHeaders)
223
- : null,
224
- identityBody:
225
- params.identityBody !== undefined
226
- ? JSON.stringify(params.identityBody)
227
- : null,
228
- identityResponsePaths: params.identityResponsePaths
229
- ? JSON.stringify(params.identityResponsePaths)
230
- : null,
231
- identityFormat: params.identityFormat ?? null,
232
- identityOkField: params.identityOkField ?? null,
233
- featureFlag: params.featureFlag ?? null,
234
- createdAt: now,
235
- updatedAt: now,
236
- };
237
- mockProviderStore.set(params.provider as string, row);
238
- return row;
239
- },
240
- updateProvider: (provider: string, params: Record<string, unknown>) => {
241
- const existing = mockProviderStore.get(provider);
242
- if (!existing) return undefined;
243
- const updated: Record<string, unknown> = { ...existing };
244
- if (params.scopeSeparator !== undefined) {
245
- updated.scopeSeparator = params.scopeSeparator;
246
- }
247
- if (params.authorizeUrl !== undefined) {
248
- updated.authorizeUrl = params.authorizeUrl;
249
- }
250
- if (params.tokenExchangeUrl !== undefined) {
251
- updated.tokenExchangeUrl = params.tokenExchangeUrl;
252
- }
253
- if (params.refreshUrl !== undefined) {
254
- updated.refreshUrl = params.refreshUrl;
255
- }
256
- if (params.revokeUrl !== undefined) {
257
- updated.revokeUrl = params.revokeUrl;
258
- }
259
- if (params.revokeBodyTemplate !== undefined) {
260
- updated.revokeBodyTemplate =
261
- params.revokeBodyTemplate === null
262
- ? null
263
- : JSON.stringify(params.revokeBodyTemplate);
264
- }
265
- if (params.defaultScopes !== undefined) {
266
- updated.defaultScopes = JSON.stringify(params.defaultScopes);
267
- }
268
- if (params.displayLabel !== undefined) {
269
- updated.displayLabel = params.displayLabel;
270
- }
271
- if (params.logoUrl !== undefined) {
272
- updated.logoUrl = params.logoUrl;
273
- }
274
- updated.updatedAt = Date.now();
275
- mockProviderStore.set(provider, updated);
276
- return updated;
277
- },
278
- deleteProvider: () => false,
279
- seedProviders: () => {},
280
- getActiveConnection: () => undefined,
281
- listActiveConnectionsByProvider: () => [],
282
- createConnection: () => ({}),
283
- isProviderConnected: () => false,
284
- updateConnection: () => ({}),
285
- }));
286
-
287
- // Stub out transitive dependencies that token-manager would normally pull in.
288
- // All named exports must be present to avoid SyntaxError when bun's module
289
- // mock is shared across test files in the same run.
290
- mock.module("../security/secure-keys.js", () => ({
291
- getSecureKeyAsync: async (account: string) => mockGetSecureKey(account),
292
- getSecureKeyResultAsync: async (account: string) => ({
293
- value: mockGetSecureKey(account),
294
- unreachable: false,
295
- }),
296
- setSecureKeyAsync: async () => true,
297
- deleteSecureKeyAsync: async (account: string) => {
298
- if (secureKeyStore.has(account)) {
299
- secureKeyStore.delete(account);
300
- return "deleted" as const;
301
- }
302
- return "not-found" as const;
303
- },
304
- bulkSetSecureKeysAsync: async () => true,
305
- getProviderKeyAsync: async () => undefined,
306
- getMaskedProviderKey: async () => undefined,
307
- listSecureKeysAsync: async () => ({
308
- accounts: [...secureKeyStore.keys()],
309
- unreachable: false,
310
- }),
311
- getActiveBackendName: () => "mock",
312
- setCesClient: () => {},
313
- onCesClientChanged: () => () => {},
314
- setCesReconnect: () => {},
315
- _resetBackend: () => {},
316
- }));
317
-
318
- mock.module("../tools/credentials/metadata-store.js", () => ({
319
- assertMetadataWritable: () => {},
320
- getCredentialMetadata: (service: string, field: string) =>
321
- mockGetCredentialMetadata(service, field),
322
- getCredentialMetadataById: () => undefined,
323
- upsertCredentialMetadata: () => ({}),
324
- listCredentialMetadata: () => [],
325
- deleteCredentialMetadata: (service: string, field: string): boolean => {
326
- const idx = metadataStore.findIndex(
327
- (c) => c.service === service && c.field === field,
328
- );
329
- if (idx === -1) return false;
330
- metadataStore.splice(idx, 1);
331
- return true;
332
- },
333
- _setMetadataPath: () => {},
334
- }));
335
-
336
- // ---------------------------------------------------------------------------
337
- // Mock connect-orchestrator
338
- // ---------------------------------------------------------------------------
339
-
340
- mock.module("../oauth/connect-orchestrator.js", () => ({
341
- orchestrateOAuthConnect: (opts: Record<string, unknown>) =>
342
- mockOrchestrateOAuthConnect(opts),
343
- }));
344
-
345
- // ---------------------------------------------------------------------------
346
- // Mock cli-client (IPC) — used by `oauth connect` for daemon-orchestrated flow
347
- // ---------------------------------------------------------------------------
348
-
349
- mock.module("../ipc/cli-client.js", () => ({
350
- cliIpcCall: (operationId: string, params?: Record<string, unknown>) =>
351
- mockCliIpcCall(operationId, params),
352
- }));
353
-
354
- mock.module("../oauth/seed-providers.js", () => ({
355
- SEEDED_PROVIDER_KEYS: new Set([
356
- "google",
357
- "slack",
358
- "github",
359
- "notion",
360
- "twitter",
361
- "linear",
362
- ]),
363
- seedOAuthProviders: () => {},
364
- }));
365
-
366
- // ---------------------------------------------------------------------------
367
- // Mock connection-resolver (needed by request.ts)
368
- // ---------------------------------------------------------------------------
369
-
370
- mock.module("../oauth/connection-resolver.js", () => ({
371
- resolveOAuthConnection: (
372
- provider: string,
373
- options?: Record<string, unknown>,
374
- ) => mockResolveOAuthConnection(provider, options),
375
- }));
376
-
377
- // ---------------------------------------------------------------------------
378
- // Mock platform/client (needed by request.ts)
379
- // ---------------------------------------------------------------------------
28
+ let mockPlatformClientCreate: () => Promise<Record<string, unknown> | null> =
29
+ async () => null;
380
30
 
381
31
  mock.module("../platform/client.js", () => ({
382
32
  VellumPlatformClient: {
@@ -384,18 +34,13 @@ mock.module("../platform/client.js", () => ({
384
34
  },
385
35
  }));
386
36
 
387
- // ---------------------------------------------------------------------------
388
- // Mock config/loader (needed by isManagedMode in shared.ts)
389
- // ---------------------------------------------------------------------------
390
-
391
- let mockGetConfig: () => Record<string, unknown> = () => ({
392
- services: {},
393
- });
394
-
37
+ // Some shared helpers in oauth/shared.ts touch getConfig() — stub it so the
38
+ // import resolves cleanly even though the requirePlatformConnection path
39
+ // never reads service configuration.
395
40
  mock.module("../config/loader.js", () => ({
396
- getConfig: () => mockGetConfig(),
397
- getConfigReadOnly: () => mockGetConfig(),
398
- loadConfig: () => mockGetConfig(),
41
+ getConfig: () => ({ services: {} }),
42
+ getConfigReadOnly: () => ({ services: {} }),
43
+ loadConfig: () => ({ services: {} }),
399
44
  invalidateConfigCache: () => {},
400
45
  loadRawConfig: () => ({}),
401
46
  saveRawConfig: () => {},
@@ -405,1022 +50,17 @@ mock.module("../config/loader.js", () => ({
405
50
  getNestedValue: () => undefined,
406
51
  setNestedValue: () => {},
407
52
  _appendQuarantineBulletin: () => {},
408
- API_KEY_PROVIDERS: [
409
- "anthropic",
410
- "openai",
411
- "gemini",
412
- "ollama",
413
- "fireworks",
414
- "openrouter",
415
- "brave",
416
- "perplexity",
417
- ],
53
+ API_KEY_PROVIDERS: ["anthropic", "openai", "gemini"],
418
54
  }));
419
55
 
420
56
  mock.module("../util/logger.js", () => ({
421
- getLogger: () => ({
422
- info: () => {},
423
- warn: () => {},
424
- error: () => {},
425
- debug: () => {},
426
- }),
427
- getCliLogger: () => ({
428
- info: () => {},
429
- warn: () => {},
430
- error: () => {},
431
- debug: () => {},
432
- }),
57
+ getLogger: () => ({ info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }),
58
+ getCliLogger: () => ({ info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }),
433
59
  }));
434
60
 
435
- // ---------------------------------------------------------------------------
436
- // Import the module under test (after mocks are registered)
437
- // ---------------------------------------------------------------------------
438
-
439
- const { registerOAuthCommand } = await import("../cli/commands/oauth/index.js");
440
- const { requirePlatformClient, requirePlatformConnection } =
441
- await import("../cli/commands/oauth/shared.js");
442
-
443
- // ---------------------------------------------------------------------------
444
- // Test helper
445
- // ---------------------------------------------------------------------------
446
-
447
- async function runCli(
448
- args: string[],
449
- ): Promise<{ exitCode: number; stdout: string }> {
450
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
451
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
452
- const stdoutChunks: string[] = [];
453
-
454
- process.stdout.write = ((chunk: unknown) => {
455
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
456
- return true;
457
- }) as typeof process.stdout.write;
458
-
459
- process.stderr.write = (() => true) as typeof process.stderr.write;
460
-
461
- process.exitCode = 0;
462
-
463
- try {
464
- const program = new Command();
465
- program.exitOverride();
466
- program.configureOutput({
467
- writeErr: () => {},
468
- writeOut: (str: string) => stdoutChunks.push(str),
469
- });
470
- registerOAuthCommand(program);
471
- await program.parseAsync(["node", "assistant", "oauth", ...args]);
472
- } catch {
473
- if (process.exitCode === 0) process.exitCode = 1;
474
- } finally {
475
- process.stdout.write = originalStdoutWrite;
476
- process.stderr.write = originalStderrWrite;
477
- }
478
-
479
- const exitCode = process.exitCode ?? 0;
480
- process.exitCode = 0;
481
-
482
- return {
483
- exitCode,
484
- stdout: stdoutChunks.join(""),
485
- };
486
- }
487
-
488
- // ---------------------------------------------------------------------------
489
- // Tests
490
- // ---------------------------------------------------------------------------
491
-
492
- describe("assistant oauth token <provider-key>", () => {
493
- beforeEach(() => {
494
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
495
- });
496
-
497
- test("prints bare token in human mode", async () => {
498
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
499
- expect(exitCode).toBe(0);
500
- expect(stdout).toBe("mock-access-token-xyz\n");
501
- });
502
-
503
- test("prints JSON in --json mode", async () => {
504
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
505
- expect(exitCode).toBe(0);
506
- const parsed = JSON.parse(stdout);
507
- expect(parsed).toEqual({ ok: true, token: "mock-access-token-xyz" });
508
- });
509
-
510
- test("passes provider key directly to withValidToken", async () => {
511
- let capturedService: string | undefined;
512
- mockWithValidToken = async (service, cb) => {
513
- capturedService = service;
514
- return cb("tok");
515
- };
516
-
517
- await runCli(["token", "twitter"]);
518
- expect(capturedService).toBe("twitter");
519
- });
520
-
521
- test("works with other provider keys", async () => {
522
- let capturedService: string | undefined;
523
- mockWithValidToken = async (service, cb) => {
524
- capturedService = service;
525
- return cb("gmail-token");
526
- };
527
-
528
- const { exitCode, stdout } = await runCli(["token", "google"]);
529
- expect(exitCode).toBe(0);
530
- expect(stdout).toBe("gmail-token\n");
531
- expect(capturedService).toBe("google");
532
- });
533
-
534
- test("exits 1 when no token exists", async () => {
535
- mockWithValidToken = async () => {
536
- throw new Error(
537
- 'No access token found for "twitter". Authorization required.',
538
- );
539
- };
540
-
541
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
542
- expect(exitCode).toBe(1);
543
- const parsed = JSON.parse(stdout);
544
- expect(parsed.ok).toBe(false);
545
- expect(parsed.error).toContain("No access token found");
546
- });
547
-
548
- test("exits 1 when refresh fails", async () => {
549
- mockWithValidToken = async () => {
550
- throw new Error('Token refresh failed for "twitter": invalid_grant.');
551
- };
552
-
553
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
554
- expect(exitCode).toBe(1);
555
- const parsed = JSON.parse(stdout);
556
- expect(parsed.ok).toBe(false);
557
- expect(parsed.error).toContain("Token refresh failed");
558
- });
559
-
560
- test("returns refreshed token transparently", async () => {
561
- // Simulate withValidToken refreshing and returning a new token
562
- mockWithValidToken = async (_service, cb) => cb("refreshed-new-token");
563
-
564
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
565
- expect(exitCode).toBe(0);
566
- expect(stdout).toBe("refreshed-new-token\n");
567
- });
568
-
569
- test("missing provider-key argument exits non-zero", async () => {
570
- const { exitCode } = await runCli(["token"]);
571
- expect(exitCode).not.toBe(0);
572
- });
573
- });
574
-
575
- // ---------------------------------------------------------------------------
576
- // providers list
577
- // ---------------------------------------------------------------------------
578
-
579
- describe("assistant oauth providers list", () => {
580
- const fakeProviders = [
581
- {
582
- provider: "google",
583
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
584
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
585
- defaultScopes: "[]",
586
- availableScopes: null,
587
- authorizeParams: null,
588
- managedServiceConfigKey: "google-oauth",
589
- createdAt: "2025-01-01T00:00:00.000Z",
590
- updatedAt: "2025-01-01T00:00:00.000Z",
591
- },
592
- {
593
- provider: "google-calendar",
594
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
595
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
596
- defaultScopes: "[]",
597
- availableScopes: null,
598
- authorizeParams: null,
599
- managedServiceConfigKey: "google-calendar-oauth",
600
- createdAt: "2025-01-01T00:00:00.000Z",
601
- updatedAt: "2025-01-01T00:00:00.000Z",
602
- },
603
- {
604
- provider: "slack",
605
- authorizeUrl: "https://slack.com/oauth/v2/authorize",
606
- tokenExchangeUrl: "https://slack.com/api/oauth.v2.access",
607
- defaultScopes: "[]",
608
- availableScopes: null,
609
- authorizeParams: null,
610
- managedServiceConfigKey: null,
611
- createdAt: "2025-01-01T00:00:00.000Z",
612
- updatedAt: "2025-01-01T00:00:00.000Z",
613
- },
614
- {
615
- provider: "twitter",
616
- authorizeUrl: "https://twitter.com/i/oauth2/authorize",
617
- tokenExchangeUrl: "https://api.twitter.com/2/oauth2/token",
618
- defaultScopes: "[]",
619
- availableScopes: null,
620
- authorizeParams: null,
621
- managedServiceConfigKey: null,
622
- createdAt: "2025-01-01T00:00:00.000Z",
623
- updatedAt: "2025-01-01T00:00:00.000Z",
624
- },
625
- ];
626
-
627
- beforeEach(() => {
628
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
629
- mockListProviders = () => fakeProviders;
630
- });
631
-
632
- test("returns all providers when no --provider-key is given", async () => {
633
- const { exitCode, stdout } = await runCli(["providers", "list", "--json"]);
634
- expect(exitCode).toBe(0);
635
- const parsed = JSON.parse(stdout);
636
- expect(parsed).toHaveLength(4);
637
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
638
- expect(keys).toContain("google");
639
- expect(keys).toContain("google-calendar");
640
- expect(keys).toContain("slack");
641
- expect(keys).toContain("twitter");
642
- });
643
-
644
- test("filters by single --provider-key value", async () => {
645
- const { exitCode, stdout } = await runCli([
646
- "providers",
647
- "list",
648
- "--provider-key",
649
- "slack",
650
- "--json",
651
- ]);
652
- expect(exitCode).toBe(0);
653
- const parsed = JSON.parse(stdout);
654
- expect(parsed).toHaveLength(1);
655
- expect(parsed[0].providerKey).toBe("slack");
656
- });
657
-
658
- test("filters by comma-separated OR values", async () => {
659
- const { exitCode, stdout } = await runCli([
660
- "providers",
661
- "list",
662
- "--provider-key",
663
- "slack,google",
664
- "--json",
665
- ]);
666
- expect(exitCode).toBe(0);
667
- const parsed = JSON.parse(stdout);
668
- expect(parsed).toHaveLength(3);
669
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
670
- expect(keys).toContain("google");
671
- expect(keys).toContain("google-calendar");
672
- expect(keys).toContain("slack");
673
- });
674
-
675
- test("returns empty array when comma-separated filter has no matches", async () => {
676
- const { exitCode, stdout } = await runCli([
677
- "providers",
678
- "list",
679
- "--provider-key",
680
- "notion,linear",
681
- "--json",
682
- ]);
683
- expect(exitCode).toBe(0);
684
- const parsed = JSON.parse(stdout);
685
- expect(parsed).toHaveLength(0);
686
- });
687
-
688
- test("trims whitespace around commas in --provider-key", async () => {
689
- const { exitCode, stdout } = await runCli([
690
- "providers",
691
- "list",
692
- "--provider-key",
693
- "slack, google",
694
- "--json",
695
- ]);
696
- expect(exitCode).toBe(0);
697
- const parsed = JSON.parse(stdout);
698
- expect(parsed).toHaveLength(3);
699
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
700
- expect(keys).toContain("google");
701
- expect(keys).toContain("google-calendar");
702
- expect(keys).toContain("slack");
703
- });
704
-
705
- test("ignores empty segments from extra commas in --provider-key", async () => {
706
- const { exitCode, stdout } = await runCli([
707
- "providers",
708
- "list",
709
- "--provider-key",
710
- "slack,,google",
711
- "--json",
712
- ]);
713
- expect(exitCode).toBe(0);
714
- const parsed = JSON.parse(stdout);
715
- expect(parsed).toHaveLength(3);
716
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
717
- expect(keys).toContain("google");
718
- expect(keys).toContain("google-calendar");
719
- expect(keys).toContain("slack");
720
- });
721
-
722
- test("--supports-managed returns only providers with managedServiceConfigKey set", async () => {
723
- const { exitCode, stdout } = await runCli([
724
- "providers",
725
- "list",
726
- "--supports-managed",
727
- "--json",
728
- ]);
729
- expect(exitCode).toBe(0);
730
- const parsed = JSON.parse(stdout);
731
- expect(parsed).toHaveLength(2);
732
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
733
- expect(keys).toContain("google");
734
- expect(keys).toContain("google-calendar");
735
- expect(keys).not.toContain("slack");
736
- expect(keys).not.toContain("twitter");
737
- });
738
-
739
- test("--supports-managed combined with --provider-key applies both filters (AND)", async () => {
740
- const { exitCode, stdout } = await runCli([
741
- "providers",
742
- "list",
743
- "--supports-managed",
744
- "--provider-key",
745
- "google",
746
- "--json",
747
- ]);
748
- expect(exitCode).toBe(0);
749
- const parsed = JSON.parse(stdout);
750
- // Both google and google-calendar match --provider-key "google" AND have
751
- // managedServiceConfigKey set, so both are returned.
752
- expect(parsed).toHaveLength(2);
753
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
754
- expect(keys).toContain("google");
755
- expect(keys).toContain("google-calendar");
756
- });
757
-
758
- test("without --supports-managed all providers are returned (existing behavior)", async () => {
759
- const { exitCode, stdout } = await runCli(["providers", "list", "--json"]);
760
- expect(exitCode).toBe(0);
761
- const parsed = JSON.parse(stdout);
762
- expect(parsed).toHaveLength(4);
763
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
764
- expect(keys).toContain("google");
765
- expect(keys).toContain("google-calendar");
766
- expect(keys).toContain("slack");
767
- expect(keys).toContain("twitter");
768
- });
769
- });
770
-
771
- // ---------------------------------------------------------------------------
772
- // apps upsert --client-secret-credential-path
773
- // ---------------------------------------------------------------------------
774
-
775
- describe("assistant oauth apps upsert --client-secret-credential-path", () => {
776
- beforeEach(() => {
777
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
778
- mockUpsertAppCalls = [];
779
- mockUpsertAppResult = {
780
- id: "app-upsert-1",
781
- provider: "google",
782
- clientId: "abc123",
783
- createdAt: 1700000000000,
784
- updatedAt: 1700000000000,
785
- };
786
- mockOrchestrateOAuthConnect = async () => ({
787
- success: true,
788
- deferred: false,
789
- grantedScopes: [],
790
- });
791
- mockGetAppByProviderAndClientId = () => undefined;
792
- mockGetMostRecentAppByProvider = () => undefined;
793
- mockGetProvider = () => undefined;
794
- mockGetSecureKey = () => undefined;
795
- mockGetCredentialMetadata = () => undefined;
796
- mockUpsertAppImpl = undefined;
797
- });
798
-
799
- test("upsert with --client-secret-credential-path passes path to upsertApp", async () => {
800
- // "custom/path" has no colon and no credential/ or oauth_app/ prefix.
801
- // resolveCredentialPath passes it through unchanged since it doesn't
802
- // match the service:field shorthand pattern.
803
- const { exitCode, stdout } = await runCli([
804
- "apps",
805
- "upsert",
806
- "--provider",
807
- "google",
808
- "--client-id",
809
- "abc123",
810
- "--client-secret-credential-path",
811
- "custom/path",
812
- "--json",
813
- ]);
814
- expect(exitCode).toBe(0);
815
- expect(mockUpsertAppCalls).toHaveLength(1);
816
- expect(mockUpsertAppCalls[0]).toEqual({
817
- provider: "google",
818
- clientId: "abc123",
819
- clientSecretOpts: { clientSecretCredentialPath: "custom/path" },
820
- });
821
- const parsed = JSON.parse(stdout);
822
- expect(parsed.id).toBe("app-upsert-1");
823
- });
824
-
825
- test("upsert with both --client-secret and --client-secret-credential-path returns error", async () => {
826
- const { exitCode, stdout } = await runCli([
827
- "apps",
828
- "upsert",
829
- "--provider",
830
- "google",
831
- "--client-id",
832
- "abc123",
833
- "--client-secret",
834
- "s3cret",
835
- "--client-secret-credential-path",
836
- "custom/path",
837
- "--json",
838
- ]);
839
- expect(exitCode).toBe(1);
840
- const parsed = JSON.parse(stdout);
841
- expect(parsed.ok).toBe(false);
842
- expect(parsed.error).toContain(
843
- "Cannot provide both --client-secret and --client-secret-credential-path",
844
- );
845
- // upsertApp should NOT have been called
846
- expect(mockUpsertAppCalls).toHaveLength(0);
847
- });
848
-
849
- test("upsert with --client-secret passes clientSecretValue to upsertApp", async () => {
850
- const { exitCode } = await runCli([
851
- "apps",
852
- "upsert",
853
- "--provider",
854
- "google",
855
- "--client-id",
856
- "abc123",
857
- "--client-secret",
858
- "s3cret",
859
- "--json",
860
- ]);
861
- expect(exitCode).toBe(0);
862
- expect(mockUpsertAppCalls).toHaveLength(1);
863
- expect(mockUpsertAppCalls[0]).toEqual({
864
- provider: "google",
865
- clientId: "abc123",
866
- clientSecretOpts: { clientSecretValue: "s3cret" },
867
- });
868
- });
869
-
870
- test("upsert without any secret option passes undefined", async () => {
871
- const { exitCode } = await runCli([
872
- "apps",
873
- "upsert",
874
- "--provider",
875
- "google",
876
- "--client-id",
877
- "abc123",
878
- "--json",
879
- ]);
880
- expect(exitCode).toBe(0);
881
- expect(mockUpsertAppCalls).toHaveLength(1);
882
- expect(mockUpsertAppCalls[0]).toEqual({
883
- provider: "google",
884
- clientId: "abc123",
885
- clientSecretOpts: undefined,
886
- });
887
- });
888
-
889
- test("upsert resolves service:field shorthand to full credential path", async () => {
890
- // The service:field shorthand is resolved to credential/{service}/{field}
891
- const { exitCode, stdout } = await runCli([
892
- "apps",
893
- "upsert",
894
- "--provider",
895
- "google",
896
- "--client-id",
897
- "abc",
898
- "--client-secret-credential-path",
899
- "google:client_secret",
900
- "--json",
901
- ]);
902
- expect(exitCode).toBe(0);
903
- expect(mockUpsertAppCalls).toHaveLength(1);
904
- expect(mockUpsertAppCalls[0]).toEqual({
905
- provider: "google",
906
- clientId: "abc",
907
- clientSecretOpts: {
908
- clientSecretCredentialPath: "credential/google/client_secret",
909
- },
910
- });
911
- const parsed = JSON.parse(stdout);
912
- expect(parsed.id).toBe("app-upsert-1");
913
- });
914
-
915
- test("upsert resolves slack:client_secret shorthand to full credential path", async () => {
916
- const { exitCode, stdout } = await runCli([
917
- "apps",
918
- "upsert",
919
- "--provider",
920
- "slack",
921
- "--client-id",
922
- "slack-abc",
923
- "--client-secret-credential-path",
924
- "slack:client_secret",
925
- "--json",
926
- ]);
927
- expect(exitCode).toBe(0);
928
- expect(mockUpsertAppCalls).toHaveLength(1);
929
- expect(mockUpsertAppCalls[0]).toEqual({
930
- provider: "slack",
931
- clientId: "slack-abc",
932
- clientSecretOpts: {
933
- clientSecretCredentialPath: "credential/slack/client_secret",
934
- },
935
- });
936
- const parsed = JSON.parse(stdout);
937
- expect(parsed.id).toBe("app-upsert-1");
938
- });
939
-
940
- test("upsert passes prefixed credential path through unchanged", async () => {
941
- const { exitCode, stdout } = await runCli([
942
- "apps",
943
- "upsert",
944
- "--provider",
945
- "google",
946
- "--client-id",
947
- "abc",
948
- "--client-secret-credential-path",
949
- "credential/google/client_secret",
950
- "--json",
951
- ]);
952
- expect(exitCode).toBe(0);
953
- expect(mockUpsertAppCalls).toHaveLength(1);
954
- // Already-prefixed path should be passed through as-is
955
- expect(mockUpsertAppCalls[0]).toEqual({
956
- provider: "google",
957
- clientId: "abc",
958
- clientSecretOpts: {
959
- clientSecretCredentialPath: "credential/google/client_secret",
960
- },
961
- });
962
- const parsed = JSON.parse(stdout);
963
- expect(parsed.id).toBe("app-upsert-1");
964
- });
965
-
966
- test("upsert passes oauth_app/ prefixed credential path through unchanged", async () => {
967
- const { exitCode, stdout } = await runCli([
968
- "apps",
969
- "upsert",
970
- "--provider",
971
- "google",
972
- "--client-id",
973
- "abc",
974
- "--client-secret-credential-path",
975
- "oauth_app/some-id/client_secret",
976
- "--json",
977
- ]);
978
- expect(exitCode).toBe(0);
979
- expect(mockUpsertAppCalls).toHaveLength(1);
980
- // oauth_app/ prefixed path should be passed through as-is
981
- expect(mockUpsertAppCalls[0]).toEqual({
982
- provider: "google",
983
- clientId: "abc",
984
- clientSecretOpts: {
985
- clientSecretCredentialPath: "oauth_app/some-id/client_secret",
986
- },
987
- });
988
- const parsed = JSON.parse(stdout);
989
- expect(parsed.id).toBe("app-upsert-1");
990
- });
991
-
992
- test("upsert with invalid credential path returns error when no secret found", async () => {
993
- // Override upsertApp to throw when given an unresolvable credential path
994
- mockUpsertAppImpl = async (_provider, _clientId, clientSecretOpts) => {
995
- throw new Error(
996
- `No secret found at credential path: ${clientSecretOpts?.clientSecretCredentialPath}`,
997
- );
998
- };
999
-
1000
- const { exitCode, stdout } = await runCli([
1001
- "apps",
1002
- "upsert",
1003
- "--provider",
1004
- "google",
1005
- "--client-id",
1006
- "abc",
1007
- "--client-secret-credential-path",
1008
- "bogus:nonexistent:path",
1009
- "--json",
1010
- ]);
1011
- expect(exitCode).toBe(1);
1012
- const parsed = JSON.parse(stdout);
1013
- expect(parsed.ok).toBe(false);
1014
- expect(parsed.error).toContain("No secret found");
1015
- });
1016
- });
1017
-
1018
- // ---------------------------------------------------------------------------
1019
- // ping
1020
- // ---------------------------------------------------------------------------
1021
-
1022
- describe("assistant oauth ping <provider-key>", () => {
1023
- beforeEach(() => {
1024
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1025
- // Reset resolveOAuthConnection to default (unconfigured)
1026
- mockResolveOAuthConnection = async () => {
1027
- throw new Error("resolveOAuthConnection not configured in test");
1028
- };
1029
- });
1030
-
1031
- test("returns ok when ping endpoint returns 200", async () => {
1032
- mockGetProvider = () => ({
1033
- provider: "google",
1034
- pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1035
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1036
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1037
- defaultScopes: "[]",
1038
- availableScopes: null,
1039
- authorizeParams: null,
1040
- createdAt: Date.now(),
1041
- updatedAt: Date.now(),
1042
- });
1043
- mockResolveOAuthConnection = async () => ({
1044
- id: "conn-1",
1045
- provider: "google",
1046
- accountInfo: null,
1047
- request: async () => ({ status: 200, headers: {}, body: {} }),
1048
- withToken: async (fn) => fn("mock-access-token-xyz"),
1049
- });
1050
- const { exitCode, stdout } = await runCli(["ping", "google", "--json"]);
1051
- expect(exitCode).toBe(0);
1052
- const parsed = JSON.parse(stdout);
1053
- expect(parsed.ok).toBe(true);
1054
- expect(parsed.status).toBe(200);
1055
- });
1056
-
1057
- test("exits 1 when provider not found", async () => {
1058
- mockGetProvider = () => undefined;
1059
- const { exitCode, stdout } = await runCli(["ping", "unknown", "--json"]);
1060
- expect(exitCode).toBe(1);
1061
- const parsed = JSON.parse(stdout);
1062
- expect(parsed.ok).toBe(false);
1063
- expect(parsed.error).toContain("Unknown provider");
1064
- });
1065
-
1066
- test("exits 1 when no ping URL configured", async () => {
1067
- mockGetProvider = () => ({
1068
- provider: "telegram",
1069
- pingUrl: null,
1070
- authorizeUrl: "urn:manual-token",
1071
- tokenExchangeUrl: "urn:manual-token",
1072
- defaultScopes: "[]",
1073
- availableScopes: null,
1074
- authorizeParams: null,
1075
- createdAt: Date.now(),
1076
- updatedAt: Date.now(),
1077
- });
1078
- const { exitCode, stdout } = await runCli(["ping", "telegram", "--json"]);
1079
- expect(exitCode).toBe(1);
1080
- const parsed = JSON.parse(stdout);
1081
- expect(parsed.ok).toBe(false);
1082
- expect(parsed.error).toContain("No ping URL configured");
1083
- });
1084
-
1085
- test("exits 1 when ping endpoint returns non-2xx", async () => {
1086
- mockGetProvider = () => ({
1087
- provider: "google",
1088
- pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1089
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1090
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1091
- defaultScopes: "[]",
1092
- availableScopes: null,
1093
- authorizeParams: null,
1094
- createdAt: Date.now(),
1095
- updatedAt: Date.now(),
1096
- });
1097
- mockResolveOAuthConnection = async () => ({
1098
- id: "conn-1",
1099
- provider: "google",
1100
- accountInfo: null,
1101
- request: async () => ({ status: 403, headers: {}, body: "Forbidden" }),
1102
- withToken: async (fn) => fn("mock-access-token-xyz"),
1103
- });
1104
- const { exitCode, stdout } = await runCli(["ping", "google", "--json"]);
1105
- expect(exitCode).toBe(1);
1106
- const parsed = JSON.parse(stdout);
1107
- expect(parsed.ok).toBe(false);
1108
- expect(parsed.status).toBe(403);
1109
- });
1110
-
1111
- test("exits 1 when no connection can be resolved", async () => {
1112
- mockGetProvider = () => ({
1113
- provider: "google",
1114
- pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1115
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1116
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1117
- defaultScopes: "[]",
1118
- availableScopes: null,
1119
- authorizeParams: null,
1120
- createdAt: Date.now(),
1121
- updatedAt: Date.now(),
1122
- });
1123
- mockResolveOAuthConnection = async () => {
1124
- throw new Error('No access token found for "google".');
1125
- };
1126
- const { exitCode, stdout } = await runCli(["ping", "google", "--json"]);
1127
- expect(exitCode).toBe(1);
1128
- const parsed = JSON.parse(stdout);
1129
- expect(parsed.ok).toBe(false);
1130
- expect(parsed.error).toContain("No access token");
1131
- });
1132
- });
1133
-
1134
- // ---------------------------------------------------------------------------
1135
- // oauth connect — managed mode 401/403 error messages
1136
- // ---------------------------------------------------------------------------
1137
-
1138
- describe("assistant oauth connect managed mode — platform 401/403 errors", () => {
1139
- /**
1140
- * Helper: create a mock platform client whose `fetch` always returns the
1141
- * given status code and body text.
1142
- */
1143
- function makeMockPlatformClient(status: number, body = "") {
1144
- return {
1145
- platformAssistantId: "asst-test-123",
1146
- fetch: async () =>
1147
- new Response(body, { status, statusText: `HTTP ${status}` }),
1148
- };
1149
- }
1150
-
1151
- beforeEach(() => {
1152
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1153
- mockOrchestrateOAuthConnect = async () => ({
1154
- success: true,
1155
- deferred: false,
1156
- grantedScopes: [],
1157
- });
1158
- mockGetAppByProviderAndClientId = () => undefined;
1159
- mockGetMostRecentAppByProvider = () => undefined;
1160
- mockGetSecureKey = () => undefined;
1161
- mockGetCredentialMetadata = () => undefined;
1162
-
1163
- // Set up managed mode: provider has managedServiceConfigKey, config
1164
- // returns the matching service with mode "managed".
1165
- mockGetProvider = () => ({
1166
- provider: "google",
1167
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1168
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1169
- defaultScopes: "[]",
1170
- availableScopes: null,
1171
- authorizeParams: null,
1172
- managedServiceConfigKey: "google-oauth",
1173
- createdAt: Date.now(),
1174
- updatedAt: Date.now(),
1175
- });
1176
- mockGetConfig = () => ({
1177
- services: {
1178
- "google-oauth": { mode: "managed" },
1179
- },
1180
- });
1181
- });
1182
-
1183
- afterEach(() => {
1184
- mockPlatformClientCreate = async () => null;
1185
- mockGetConfig = () => ({ services: {} });
1186
- });
1187
-
1188
- test("401 response includes 'vellum platform connect' suggestion", async () => {
1189
- mockPlatformClientCreate = async () =>
1190
- makeMockPlatformClient(401, "Unauthorized");
1191
- const { exitCode, stdout } = await runCli([
1192
- "connect",
1193
- "google",
1194
- "--no-browser",
1195
- "--json",
1196
- ]);
1197
- expect(exitCode).toBe(1);
1198
- const parsed = JSON.parse(stdout);
1199
- expect(parsed.ok).toBe(false);
1200
- expect(parsed.error).toContain("Platform returned HTTP 401");
1201
- expect(parsed.error).toContain("Unauthorized");
1202
- expect(parsed.error).toContain("vellum platform connect");
1203
- });
1204
-
1205
- test("403 response includes 'vellum platform connect' suggestion", async () => {
1206
- mockPlatformClientCreate = async () =>
1207
- makeMockPlatformClient(403, "Forbidden");
1208
- const { exitCode, stdout } = await runCli([
1209
- "connect",
1210
- "google",
1211
- "--no-browser",
1212
- "--json",
1213
- ]);
1214
- expect(exitCode).toBe(1);
1215
- const parsed = JSON.parse(stdout);
1216
- expect(parsed.ok).toBe(false);
1217
- expect(parsed.error).toContain("Platform returned HTTP 403");
1218
- expect(parsed.error).toContain("Forbidden");
1219
- expect(parsed.error).toContain("vellum platform connect");
1220
- });
1221
-
1222
- test("500 response does NOT include 'vellum platform connect' suggestion", async () => {
1223
- mockPlatformClientCreate = async () =>
1224
- makeMockPlatformClient(500, "Internal Server Error");
1225
- const { exitCode, stdout } = await runCli([
1226
- "connect",
1227
- "google",
1228
- "--no-browser",
1229
- "--json",
1230
- ]);
1231
- expect(exitCode).toBe(1);
1232
- const parsed = JSON.parse(stdout);
1233
- expect(parsed.ok).toBe(false);
1234
- expect(parsed.error).toContain("Platform returned HTTP 500");
1235
- expect(parsed.error).not.toContain("vellum platform connect");
1236
- });
1237
- });
1238
-
1239
- // ---------------------------------------------------------------------------
1240
- // `assistant oauth connect <provider>` BYO mode — daemon-unreachable behavior.
1241
- //
1242
- // We deleted the in-process `orchestrateOAuthConnect` fallback (the same
1243
- // pattern as the MCP CLI consolidation in #29484). When the daemon is
1244
- // unreachable, the CLI must surface a clear error and exit 1 — never
1245
- // silently fall back to in-process flow.
1246
- // ---------------------------------------------------------------------------
1247
-
1248
- describe("assistant oauth connect <provider> — daemon unreachable (BYO mode)", () => {
1249
- beforeEach(() => {
1250
- // BYO provider with a registered app and no managed-mode wiring.
1251
- mockGetProvider = () => ({
1252
- provider: "github",
1253
- authorizeUrl: "https://github.com/login/oauth/authorize",
1254
- tokenExchangeUrl: "https://github.com/login/oauth/access_token",
1255
- defaultScopes: "[]",
1256
- availableScopes: null,
1257
- authorizeParams: null,
1258
- managedServiceConfigKey: null,
1259
- requiresClientSecret: false,
1260
- createdAt: Date.now(),
1261
- updatedAt: Date.now(),
1262
- });
1263
- mockGetMostRecentAppByProvider = () => ({
1264
- provider: "github",
1265
- clientId: "test-client-id",
1266
- clientSecretCredentialPath: "oauth_app/github/test/client_secret",
1267
- });
1268
- mockGetSecureKey = () => "test-secret";
1269
- mockGetConfig = () => ({ services: {} });
1270
- });
1271
-
1272
- test("daemon connect-refused → exit 1 with 'Is the assistant running?'", async () => {
1273
- mockCliIpcCall = async () => ({
1274
- ok: false,
1275
- error: "Could not connect to assistant daemon: ECONNREFUSED",
1276
- });
1277
-
1278
- const { exitCode, stdout } = await runCli([
1279
- "connect",
1280
- "github",
1281
- "--no-browser",
1282
- "--json",
1283
- ]);
1284
-
1285
- expect(exitCode).toBe(1);
1286
- const parsed = JSON.parse(stdout);
1287
- expect(parsed.ok).toBe(false);
1288
- expect(parsed.error).toContain("Could not reach the assistant");
1289
- expect(parsed.error).toContain("Is the assistant running?");
1290
- });
1291
-
1292
- test("daemon route missing (Unknown method) → exit 1, never falls through to in-process", async () => {
1293
- let orchestratorCalls = 0;
1294
- mockOrchestrateOAuthConnect = async () => {
1295
- orchestratorCalls++;
1296
- return { success: true, deferred: false, grantedScopes: [] };
1297
- };
1298
- mockCliIpcCall = async () => ({
1299
- ok: false,
1300
- error: "Unknown method: internal_oauth_connect_start",
1301
- });
1302
-
1303
- const { exitCode } = await runCli([
1304
- "connect",
1305
- "github",
1306
- "--no-browser",
1307
- "--json",
1308
- ]);
1309
-
1310
- expect(exitCode).toBe(1);
1311
- // Critical regression guard: the in-process `orchestrateOAuthConnect`
1312
- // must NOT be invoked from the CLI. The daemon-orchestrated path is
1313
- // the sole code path; this is the same invariant #29484 established
1314
- // for the MCP CLI.
1315
- expect(orchestratorCalls).toBe(0);
1316
- });
1317
-
1318
- test("daemon HTTP error (statusCode set) → surfaces error verbatim, no fallback", async () => {
1319
- let orchestratorCalls = 0;
1320
- mockOrchestrateOAuthConnect = async () => {
1321
- orchestratorCalls++;
1322
- return { success: true, deferred: false, grantedScopes: [] };
1323
- };
1324
- mockCliIpcCall = async () => ({
1325
- ok: false,
1326
- statusCode: 400,
1327
- error: "service must be registered before connecting",
1328
- });
1329
-
1330
- const { exitCode, stdout } = await runCli([
1331
- "connect",
1332
- "github",
1333
- "--no-browser",
1334
- "--json",
1335
- ]);
1336
-
1337
- expect(exitCode).toBe(1);
1338
- const parsed = JSON.parse(stdout);
1339
- expect(parsed.error).toContain("service must be registered");
1340
- expect(orchestratorCalls).toBe(0);
1341
- });
1342
- });
1343
-
1344
- // ---------------------------------------------------------------------------
1345
- // requirePlatformClient — improved error messages
1346
- // ---------------------------------------------------------------------------
1347
-
1348
- describe("requirePlatformClient", () => {
1349
- test("returns error mentioning 'vellum platform connect' when not connected", async () => {
1350
- mockPlatformClientCreate = async () => null;
1351
- const stdoutChunks: string[] = [];
1352
- const originalWrite = process.stdout.write.bind(process.stdout);
1353
- process.stdout.write = ((chunk: unknown) => {
1354
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
1355
- return true;
1356
- }) as typeof process.stdout.write;
1357
- process.exitCode = 0;
1358
-
1359
- try {
1360
- const cmd = new Command();
1361
- cmd.option("--json");
1362
- cmd.parse(["node", "test", "--json"]);
1363
- const result = await requirePlatformClient(cmd);
1364
- expect(result).toBeNull();
1365
- expect(process.exitCode).toBe(1);
1366
- const output = stdoutChunks.join("");
1367
- const parsed = JSON.parse(output);
1368
- expect(parsed.ok).toBe(false);
1369
- expect(parsed.error).toContain("vellum platform connect");
1370
- expect(parsed.error).toContain("Not connected");
1371
- } finally {
1372
- process.stdout.write = originalWrite;
1373
- process.exitCode = 0;
1374
- }
1375
- });
1376
-
1377
- test("returns distinct error when connected but missing assistant ID", async () => {
1378
- mockPlatformClientCreate = async () => ({
1379
- platformAssistantId: "",
1380
- fetch: async () => new Response(),
1381
- });
1382
- const stdoutChunks: string[] = [];
1383
- const originalWrite = process.stdout.write.bind(process.stdout);
1384
- process.stdout.write = ((chunk: unknown) => {
1385
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
1386
- return true;
1387
- }) as typeof process.stdout.write;
1388
- process.exitCode = 0;
1389
-
1390
- try {
1391
- const cmd = new Command();
1392
- cmd.option("--json");
1393
- cmd.parse(["node", "test", "--json"]);
1394
- const result = await requirePlatformClient(cmd);
1395
- expect(result).toBeNull();
1396
- expect(process.exitCode).toBe(1);
1397
- const output = stdoutChunks.join("");
1398
- const parsed = JSON.parse(output);
1399
- expect(parsed.ok).toBe(false);
1400
- expect(parsed.error).toContain("no assistant ID is configured");
1401
- expect(parsed.error).toContain("registered on the platform");
1402
- } finally {
1403
- process.stdout.write = originalWrite;
1404
- process.exitCode = 0;
1405
- }
1406
- });
1407
-
1408
- test("returns client when connected with assistant ID", async () => {
1409
- mockPlatformClientCreate = async () => ({
1410
- platformAssistantId: "asst-123",
1411
- fetch: async () => new Response(),
1412
- });
1413
- process.exitCode = 0;
1414
-
1415
- const cmd = new Command();
1416
- cmd.option("--json");
1417
- cmd.parse(["node", "test", "--json"]);
1418
- const result = await requirePlatformClient(cmd);
1419
- expect(result).not.toBeNull();
1420
- expect(result!.platformAssistantId).toBe("asst-123");
1421
- expect(process.exitCode).toBe(0);
1422
- });
1423
- });
61
+ const { requirePlatformConnection } = await import(
62
+ "../cli/commands/oauth/shared.js"
63
+ );
1424
64
 
1425
65
  // ---------------------------------------------------------------------------
1426
66
  // requirePlatformConnection
@@ -1485,614 +125,3 @@ describe("requirePlatformConnection", () => {
1485
125
  expect(process.exitCode).toBe(0);
1486
126
  });
1487
127
  });
1488
-
1489
- // ---------------------------------------------------------------------------
1490
- // oauth mode — platform connection guard
1491
- // ---------------------------------------------------------------------------
1492
-
1493
- describe("assistant oauth mode", () => {
1494
- beforeEach(() => {
1495
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1496
- mockGetProvider = () => ({
1497
- provider: "google",
1498
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1499
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1500
- defaultScopes: "[]",
1501
- availableScopes: null,
1502
- authorizeParams: null,
1503
- managedServiceConfigKey: "google-oauth",
1504
- createdAt: Date.now(),
1505
- updatedAt: Date.now(),
1506
- });
1507
- mockGetConfig = () => ({
1508
- services: {
1509
- "google-oauth": { mode: "your-own" },
1510
- },
1511
- });
1512
- });
1513
-
1514
- afterEach(() => {
1515
- mockPlatformClientCreate = async () => null;
1516
- mockGetConfig = () => ({ services: {} });
1517
- mockGetProvider = () => undefined;
1518
- });
1519
-
1520
- test("oauth mode <provider> --set managed fails when not connected to platform", async () => {
1521
- mockPlatformClientCreate = async () => null;
1522
- const { exitCode, stdout } = await runCli([
1523
- "mode",
1524
- "google",
1525
- "--set",
1526
- "managed",
1527
- "--json",
1528
- ]);
1529
- expect(exitCode).toBe(1);
1530
- const parsed = JSON.parse(stdout);
1531
- expect(parsed.ok).toBe(false);
1532
- expect(parsed.error).toContain("vellum platform connect");
1533
- });
1534
-
1535
- test("oauth mode <provider> --set your-own succeeds without platform connection", async () => {
1536
- mockPlatformClientCreate = async () => null;
1537
- const { exitCode, stdout } = await runCli([
1538
- "mode",
1539
- "google",
1540
- "--set",
1541
- "your-own",
1542
- "--json",
1543
- ]);
1544
- // Setting to "your-own" doesn't need platform — it's a local-only operation
1545
- expect(exitCode).toBe(0);
1546
- const parsed = JSON.parse(stdout);
1547
- expect(parsed.ok).toBe(true);
1548
- expect(parsed.mode).toBe("your-own");
1549
- });
1550
-
1551
- test("oauth mode <provider> (read) succeeds without platform connection", async () => {
1552
- mockPlatformClientCreate = async () => null;
1553
- const { exitCode, stdout } = await runCli(["mode", "google", "--json"]);
1554
- expect(exitCode).toBe(0);
1555
- const parsed = JSON.parse(stdout);
1556
- expect(parsed.ok).toBe(true);
1557
- expect(parsed.provider).toBe("google");
1558
- expect(parsed.mode).toBe("your-own");
1559
- });
1560
- });
1561
-
1562
- // ---------------------------------------------------------------------------
1563
- // providers register / update / get — --scope-separator wiring
1564
- // ---------------------------------------------------------------------------
1565
-
1566
- describe("assistant oauth providers --scope-separator", () => {
1567
- beforeEach(() => {
1568
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1569
- mockProviderStore.clear();
1570
- // Default getProvider falls through to mockProviderStore via the
1571
- // oauth-store mock module. Tests in this describe block don't need
1572
- // a per-test mockGetProvider override.
1573
- mockGetProvider = () => undefined;
1574
- mockGetConfig = () => ({ services: {} });
1575
- });
1576
-
1577
- afterEach(() => {
1578
- mockProviderStore.clear();
1579
- mockGetProvider = () => undefined;
1580
- });
1581
-
1582
- test("providers register --scope-separator , stores ',' on the provider row", async () => {
1583
- const { exitCode } = await runCli([
1584
- "providers",
1585
- "register",
1586
- "--provider-key",
1587
- "custom-linear",
1588
- "--auth-url",
1589
- "https://linear.app/oauth/authorize",
1590
- "--token-url",
1591
- "https://api.linear.app/oauth/token",
1592
- "--scopes",
1593
- "read,write",
1594
- "--scope-separator",
1595
- ",",
1596
- "--json",
1597
- ]);
1598
- expect(exitCode).toBe(0);
1599
- const stored = mockProviderStore.get("custom-linear");
1600
- expect(stored).toBeDefined();
1601
- expect(stored?.scopeSeparator).toBe(",");
1602
- });
1603
-
1604
- test("providers register without --scope-separator stores the default ' '", async () => {
1605
- const { exitCode } = await runCli([
1606
- "providers",
1607
- "register",
1608
- "--provider-key",
1609
- "custom-default-sep",
1610
- "--auth-url",
1611
- "https://example.com/oauth/authorize",
1612
- "--token-url",
1613
- "https://example.com/oauth/token",
1614
- "--scopes",
1615
- "read,write",
1616
- "--json",
1617
- ]);
1618
- expect(exitCode).toBe(0);
1619
- const stored = mockProviderStore.get("custom-default-sep");
1620
- expect(stored).toBeDefined();
1621
- expect(stored?.scopeSeparator).toBe(" ");
1622
- });
1623
-
1624
- test("providers update --scope-separator , updates an existing custom provider", async () => {
1625
- // Seed the store with an existing custom provider that uses the default
1626
- // " " separator.
1627
- await runCli([
1628
- "providers",
1629
- "register",
1630
- "--provider-key",
1631
- "custom-update-target",
1632
- "--auth-url",
1633
- "https://example.com/oauth/authorize",
1634
- "--token-url",
1635
- "https://example.com/oauth/token",
1636
- "--scopes",
1637
- "read",
1638
- "--json",
1639
- ]);
1640
- expect(mockProviderStore.get("custom-update-target")?.scopeSeparator).toBe(
1641
- " ",
1642
- );
1643
-
1644
- const { exitCode } = await runCli([
1645
- "providers",
1646
- "update",
1647
- "custom-update-target",
1648
- "--scope-separator",
1649
- ",",
1650
- "--json",
1651
- ]);
1652
- expect(exitCode).toBe(0);
1653
- expect(mockProviderStore.get("custom-update-target")?.scopeSeparator).toBe(
1654
- ",",
1655
- );
1656
- });
1657
-
1658
- test("providers get <key> --json includes scopeSeparator from the serialized output", async () => {
1659
- // Seed the store with a custom provider that uses ',' as the separator.
1660
- await runCli([
1661
- "providers",
1662
- "register",
1663
- "--provider-key",
1664
- "custom-get-target",
1665
- "--auth-url",
1666
- "https://example.com/oauth/authorize",
1667
- "--token-url",
1668
- "https://example.com/oauth/token",
1669
- "--scopes",
1670
- "read,write",
1671
- "--scope-separator",
1672
- ",",
1673
- "--json",
1674
- ]);
1675
-
1676
- const { exitCode, stdout } = await runCli([
1677
- "providers",
1678
- "get",
1679
- "custom-get-target",
1680
- "--json",
1681
- ]);
1682
- expect(exitCode).toBe(0);
1683
- const parsed = JSON.parse(stdout);
1684
- expect(parsed.scopeSeparator).toBe(",");
1685
- });
1686
- });
1687
-
1688
- // ---------------------------------------------------------------------------
1689
- // providers register / update / get — --refresh-url wiring
1690
- // ---------------------------------------------------------------------------
1691
-
1692
- describe("assistant oauth providers --refresh-url", () => {
1693
- beforeEach(() => {
1694
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1695
- mockProviderStore.clear();
1696
- // Default getProvider falls through to mockProviderStore via the
1697
- // oauth-store mock module.
1698
- mockGetProvider = () => undefined;
1699
- mockGetConfig = () => ({ services: {} });
1700
- });
1701
-
1702
- afterEach(() => {
1703
- mockProviderStore.clear();
1704
- mockGetProvider = () => undefined;
1705
- });
1706
-
1707
- test("providers register --refresh-url stores the URL on the provider row", async () => {
1708
- const { exitCode } = await runCli([
1709
- "providers",
1710
- "register",
1711
- "--provider-key",
1712
- "custom-refresh-url",
1713
- "--auth-url",
1714
- "https://example.com/oauth/authorize",
1715
- "--token-url",
1716
- "https://example.com/oauth/token",
1717
- "--refresh-url",
1718
- "https://refresh.example.com/token",
1719
- "--json",
1720
- ]);
1721
- expect(exitCode).toBe(0);
1722
- const stored = mockProviderStore.get("custom-refresh-url");
1723
- expect(stored).toBeDefined();
1724
- expect(stored?.refreshUrl).toBe("https://refresh.example.com/token");
1725
- });
1726
-
1727
- test("providers register without --refresh-url stores null", async () => {
1728
- const { exitCode } = await runCli([
1729
- "providers",
1730
- "register",
1731
- "--provider-key",
1732
- "custom-no-refresh-url",
1733
- "--auth-url",
1734
- "https://example.com/oauth/authorize",
1735
- "--token-url",
1736
- "https://example.com/oauth/token",
1737
- "--json",
1738
- ]);
1739
- expect(exitCode).toBe(0);
1740
- const stored = mockProviderStore.get("custom-no-refresh-url");
1741
- expect(stored).toBeDefined();
1742
- expect(stored?.refreshUrl).toBeNull();
1743
- });
1744
-
1745
- test("providers update --refresh-url updates an existing custom provider", async () => {
1746
- // Seed the store with an existing custom provider that has no refresh URL.
1747
- await runCli([
1748
- "providers",
1749
- "register",
1750
- "--provider-key",
1751
- "custom-update-refresh",
1752
- "--auth-url",
1753
- "https://example.com/oauth/authorize",
1754
- "--token-url",
1755
- "https://example.com/oauth/token",
1756
- "--json",
1757
- ]);
1758
- expect(
1759
- mockProviderStore.get("custom-update-refresh")?.refreshUrl,
1760
- ).toBeNull();
1761
-
1762
- const { exitCode } = await runCli([
1763
- "providers",
1764
- "update",
1765
- "custom-update-refresh",
1766
- "--refresh-url",
1767
- "https://new-refresh.example.com/token",
1768
- "--json",
1769
- ]);
1770
- expect(exitCode).toBe(0);
1771
- expect(mockProviderStore.get("custom-update-refresh")?.refreshUrl).toBe(
1772
- "https://new-refresh.example.com/token",
1773
- );
1774
- });
1775
-
1776
- test("providers get <key> --json includes refreshUrl from the serialized output", async () => {
1777
- // Seed the store with a custom provider that has a refresh URL set.
1778
- await runCli([
1779
- "providers",
1780
- "register",
1781
- "--provider-key",
1782
- "custom-get-refresh",
1783
- "--auth-url",
1784
- "https://example.com/oauth/authorize",
1785
- "--token-url",
1786
- "https://example.com/oauth/token",
1787
- "--refresh-url",
1788
- "https://refresh.example.com/token",
1789
- "--json",
1790
- ]);
1791
-
1792
- const { exitCode, stdout } = await runCli([
1793
- "providers",
1794
- "get",
1795
- "custom-get-refresh",
1796
- "--json",
1797
- ]);
1798
- expect(exitCode).toBe(0);
1799
- const parsed = JSON.parse(stdout);
1800
- expect(parsed.refreshUrl).toBe("https://refresh.example.com/token");
1801
- });
1802
- });
1803
-
1804
- // ---------------------------------------------------------------------------
1805
- // providers register / update / get — --revoke-url and --revoke-body-template wiring
1806
- // ---------------------------------------------------------------------------
1807
-
1808
- describe("assistant oauth providers --revoke-url and --revoke-body-template", () => {
1809
- beforeEach(() => {
1810
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1811
- mockProviderStore.clear();
1812
- mockGetProvider = () => undefined;
1813
- mockGetConfig = () => ({ services: {} });
1814
- });
1815
-
1816
- afterEach(() => {
1817
- mockProviderStore.clear();
1818
- mockGetProvider = () => undefined;
1819
- });
1820
-
1821
- test("providers register --revoke-url and --revoke-body-template stores both fields", async () => {
1822
- const { exitCode } = await runCli([
1823
- "providers",
1824
- "register",
1825
- "--provider-key",
1826
- "custom-revoke-roundtrip",
1827
- "--auth-url",
1828
- "https://example.com/oauth/authorize",
1829
- "--token-url",
1830
- "https://example.com/oauth/token",
1831
- "--revoke-url",
1832
- "https://revoke.example.com",
1833
- "--revoke-body-template",
1834
- '{"token":"{access_token}"}',
1835
- "--json",
1836
- ]);
1837
- expect(exitCode).toBe(0);
1838
- const stored = mockProviderStore.get("custom-revoke-roundtrip");
1839
- expect(stored).toBeDefined();
1840
- expect(stored?.revokeUrl).toBe("https://revoke.example.com");
1841
- // revokeBodyTemplate is JSON-stringified at the storage layer.
1842
- expect(JSON.parse(stored?.revokeBodyTemplate as string)).toEqual({
1843
- token: "{access_token}",
1844
- });
1845
- });
1846
-
1847
- test("providers register without --revoke-url or --revoke-body-template stores both as null", async () => {
1848
- const { exitCode } = await runCli([
1849
- "providers",
1850
- "register",
1851
- "--provider-key",
1852
- "custom-no-revoke",
1853
- "--auth-url",
1854
- "https://example.com/oauth/authorize",
1855
- "--token-url",
1856
- "https://example.com/oauth/token",
1857
- "--json",
1858
- ]);
1859
- expect(exitCode).toBe(0);
1860
- const stored = mockProviderStore.get("custom-no-revoke");
1861
- expect(stored).toBeDefined();
1862
- expect(stored?.revokeUrl).toBeNull();
1863
- expect(stored?.revokeBodyTemplate).toBeNull();
1864
- });
1865
-
1866
- test("providers register with malformed --revoke-body-template fails with a JSON parse error", async () => {
1867
- const { exitCode, stdout } = await runCli([
1868
- "providers",
1869
- "register",
1870
- "--provider-key",
1871
- "custom-bad-revoke",
1872
- "--auth-url",
1873
- "https://example.com/oauth/authorize",
1874
- "--token-url",
1875
- "https://example.com/oauth/token",
1876
- "--revoke-body-template",
1877
- "not-json{",
1878
- "--json",
1879
- ]);
1880
- expect(exitCode).toBe(1);
1881
- const parsed = JSON.parse(stdout);
1882
- expect(parsed.ok).toBe(false);
1883
- // The error message comes from JSON.parse — match permissively.
1884
- expect(parsed.error).toMatch(/JSON|Unexpected|parse/i);
1885
- // Provider should not have been written.
1886
- expect(mockProviderStore.get("custom-bad-revoke")).toBeUndefined();
1887
- });
1888
-
1889
- test("providers update --revoke-url updates only the URL", async () => {
1890
- // Seed an existing custom provider.
1891
- await runCli([
1892
- "providers",
1893
- "register",
1894
- "--provider-key",
1895
- "custom-update-revoke-url",
1896
- "--auth-url",
1897
- "https://example.com/oauth/authorize",
1898
- "--token-url",
1899
- "https://example.com/oauth/token",
1900
- "--json",
1901
- ]);
1902
- expect(
1903
- mockProviderStore.get("custom-update-revoke-url")?.revokeUrl,
1904
- ).toBeNull();
1905
-
1906
- const { exitCode } = await runCli([
1907
- "providers",
1908
- "update",
1909
- "custom-update-revoke-url",
1910
- "--revoke-url",
1911
- "https://new-revoke.example.com",
1912
- "--json",
1913
- ]);
1914
- expect(exitCode).toBe(0);
1915
- expect(mockProviderStore.get("custom-update-revoke-url")?.revokeUrl).toBe(
1916
- "https://new-revoke.example.com",
1917
- );
1918
- // revokeBodyTemplate should still be null (unchanged).
1919
- expect(
1920
- mockProviderStore.get("custom-update-revoke-url")?.revokeBodyTemplate,
1921
- ).toBeNull();
1922
- });
1923
-
1924
- test("providers update --revoke-url with empty string clears the stored URL to null", async () => {
1925
- // Seed an existing custom provider that already has a non-null revokeUrl.
1926
- await runCli([
1927
- "providers",
1928
- "register",
1929
- "--provider-key",
1930
- "custom-clear-revoke-url",
1931
- "--auth-url",
1932
- "https://example.com/oauth/authorize",
1933
- "--token-url",
1934
- "https://example.com/oauth/token",
1935
- "--revoke-url",
1936
- "https://revoke.example.com",
1937
- "--json",
1938
- ]);
1939
- expect(mockProviderStore.get("custom-clear-revoke-url")?.revokeUrl).toBe(
1940
- "https://revoke.example.com",
1941
- );
1942
-
1943
- // Pass an empty string to --revoke-url — the help text promises this
1944
- // clears the field, so the stored value should end up as null (not "").
1945
- const { exitCode, stdout } = await runCli([
1946
- "providers",
1947
- "update",
1948
- "custom-clear-revoke-url",
1949
- "--revoke-url",
1950
- "",
1951
- "--json",
1952
- ]);
1953
- expect(exitCode).toBe(0);
1954
- expect(
1955
- mockProviderStore.get("custom-clear-revoke-url")?.revokeUrl,
1956
- ).toBeNull();
1957
-
1958
- // The serialized --json output should also reflect null, not "".
1959
- const parsed = JSON.parse(stdout);
1960
- expect(parsed.revokeUrl).toBeNull();
1961
- });
1962
-
1963
- test("providers update --revoke-body-template updates only the template (JSON round-trip)", async () => {
1964
- await runCli([
1965
- "providers",
1966
- "register",
1967
- "--provider-key",
1968
- "custom-update-revoke-body",
1969
- "--auth-url",
1970
- "https://example.com/oauth/authorize",
1971
- "--token-url",
1972
- "https://example.com/oauth/token",
1973
- "--json",
1974
- ]);
1975
-
1976
- const { exitCode } = await runCli([
1977
- "providers",
1978
- "update",
1979
- "custom-update-revoke-body",
1980
- "--revoke-body-template",
1981
- '{"token":"{access_token}","client_id":"{client_id}"}',
1982
- "--json",
1983
- ]);
1984
- expect(exitCode).toBe(0);
1985
- const stored = mockProviderStore.get("custom-update-revoke-body");
1986
- expect(stored).toBeDefined();
1987
- expect(JSON.parse(stored?.revokeBodyTemplate as string)).toEqual({
1988
- token: "{access_token}",
1989
- client_id: "{client_id}",
1990
- });
1991
- // revokeUrl should still be null (unchanged).
1992
- expect(stored?.revokeUrl).toBeNull();
1993
- });
1994
-
1995
- test("providers update --revoke-body-template with empty string clears the stored template to null", async () => {
1996
- // Seed an existing custom provider that already has a non-null
1997
- // revokeBodyTemplate set via register.
1998
- await runCli([
1999
- "providers",
2000
- "register",
2001
- "--provider-key",
2002
- "custom-clear-revoke-body",
2003
- "--auth-url",
2004
- "https://example.com/oauth/authorize",
2005
- "--token-url",
2006
- "https://example.com/oauth/token",
2007
- "--revoke-url",
2008
- "https://revoke.example.com",
2009
- "--revoke-body-template",
2010
- '{"token":"{access_token}"}',
2011
- "--json",
2012
- ]);
2013
- const initial = mockProviderStore.get("custom-clear-revoke-body");
2014
- expect(initial?.revokeBodyTemplate).toBeDefined();
2015
- expect(JSON.parse(initial?.revokeBodyTemplate as string)).toEqual({
2016
- token: "{access_token}",
2017
- });
2018
-
2019
- // Pass an empty string to --revoke-body-template — the help text promises
2020
- // this clears the field, so the stored value should end up as null
2021
- // (not the string "null" or a JSON.parse crash).
2022
- const { exitCode, stdout } = await runCli([
2023
- "providers",
2024
- "update",
2025
- "custom-clear-revoke-body",
2026
- "--revoke-body-template",
2027
- "",
2028
- "--json",
2029
- ]);
2030
- expect(exitCode).toBe(0);
2031
- expect(
2032
- mockProviderStore.get("custom-clear-revoke-body")?.revokeBodyTemplate,
2033
- ).toBeNull();
2034
-
2035
- // The serialized --json output should also reflect null.
2036
- const parsed = JSON.parse(stdout);
2037
- expect(parsed.revokeBodyTemplate).toBeNull();
2038
- });
2039
-
2040
- test("providers get google --json includes both fields populated from PR 1's seed data", async () => {
2041
- // Simulate the seeded "google" provider row by overriding mockGetProvider
2042
- // to return a row matching what PR 1's seed inserts.
2043
- const now = Date.now();
2044
- mockGetProvider = (provider: string) => {
2045
- if (provider !== "google") return undefined;
2046
- return {
2047
- provider: "google",
2048
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
2049
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
2050
- refreshUrl: null,
2051
- tokenEndpointAuthMethod: "client_secret_post",
2052
- userinfoUrl: null,
2053
- baseUrl: null,
2054
- defaultScopes: "[]",
2055
- availableScopes: null,
2056
- scopeSeparator: " ",
2057
- authorizeParams: null,
2058
- pingUrl: null,
2059
- pingMethod: null,
2060
- pingHeaders: null,
2061
- pingBody: null,
2062
- revokeUrl: "https://oauth2.googleapis.com/revoke",
2063
- revokeBodyTemplate: JSON.stringify({ token: "{access_token}" }),
2064
- managedServiceConfigKey: null,
2065
- displayLabel: "Google",
2066
- description: null,
2067
- dashboardUrl: null,
2068
- clientIdPlaceholder: null,
2069
- requiresClientSecret: 1,
2070
- loopbackPort: null,
2071
- injectionTemplates: null,
2072
- appType: null,
2073
- setupNotes: null,
2074
- identityUrl: null,
2075
- identityMethod: null,
2076
- identityHeaders: null,
2077
- identityBody: null,
2078
- identityResponsePaths: null,
2079
- identityFormat: null,
2080
- identityOkField: null,
2081
- featureFlag: null,
2082
- createdAt: now,
2083
- updatedAt: now,
2084
- };
2085
- };
2086
-
2087
- const { exitCode, stdout } = await runCli([
2088
- "providers",
2089
- "get",
2090
- "google",
2091
- "--json",
2092
- ]);
2093
- expect(exitCode).toBe(0);
2094
- const parsed = JSON.parse(stdout);
2095
- expect(parsed.revokeUrl).toBe("https://oauth2.googleapis.com/revoke");
2096
- expect(parsed.revokeBodyTemplate).toEqual({ token: "{access_token}" });
2097
- });
2098
- });