@vellumai/assistant 0.5.9 → 0.5.11

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 (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. package/src/security/keychain-broker-client.ts +0 -446
@@ -2,8 +2,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import { Command } from "commander";
4
4
 
5
- import { credentialKey } from "../security/credential-key.js";
6
-
7
5
  // ---------------------------------------------------------------------------
8
6
  // Mock state
9
7
  // ---------------------------------------------------------------------------
@@ -13,10 +11,9 @@ let mockWithValidToken: <T>(
13
11
  cb: (token: string) => Promise<T>,
14
12
  ) => Promise<T>;
15
13
 
16
- // Disconnect mock state
17
14
  let mockListProviders: () => Array<Record<string, unknown>> = () => [];
18
- let secureKeyStore = new Map<string, string>();
19
- let metadataStore: Array<{
15
+ const secureKeyStore = new Map<string, string>();
16
+ const metadataStore: Array<{
20
17
  credentialId: string;
21
18
  service: string;
22
19
  field: string;
@@ -25,10 +22,9 @@ let metadataStore: Array<{
25
22
  createdAt: number;
26
23
  updatedAt: number;
27
24
  }> = [];
28
- let disconnectOAuthProviderCalls: string[] = [];
29
- let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
25
+ const disconnectOAuthProviderCalls: string[] = [];
26
+ const disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
30
27
  "not-found";
31
- let idCounter = 0;
32
28
 
33
29
  // App upsert mock state
34
30
  let mockUpsertAppCalls: Array<{
@@ -57,7 +53,7 @@ let mockUpsertAppImpl:
57
53
  ) => Promise<Record<string, unknown>>)
58
54
  | undefined;
59
55
 
60
- // Connect mock state
56
+ // Transitive mock state (connect-orchestrator, provider-behaviors, etc.)
61
57
  let mockOrchestrateOAuthConnect: (
62
58
  opts: Record<string, unknown>,
63
59
  ) => Promise<Record<string, unknown>>;
@@ -75,16 +71,27 @@ let mockGetProviderBehavior: (
75
71
  providerKey: string,
76
72
  ) => Record<string, unknown> | undefined = () => undefined;
77
73
  let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
74
+ let mockResolveOAuthConnection: (
75
+ providerKey: string,
76
+ options?: Record<string, unknown>,
77
+ ) => Promise<{
78
+ request: (req: Record<string, unknown>) => Promise<{
79
+ status: number;
80
+ headers: Record<string, string>;
81
+ body: unknown;
82
+ }>;
83
+ withToken: <T>(fn: (token: string) => Promise<T>) => Promise<T>;
84
+ id: string;
85
+ providerKey: string;
86
+ accountInfo: string | null;
87
+ }> = async () => {
88
+ throw new Error("resolveOAuthConnection not configured in test");
89
+ };
78
90
  let mockGetCredentialMetadata: (
79
91
  service: string,
80
92
  field: string,
81
93
  ) => Record<string, unknown> | undefined = () => undefined;
82
94
 
83
- function nextUUID(): string {
84
- idCounter += 1;
85
- return `00000000-0000-0000-0000-${String(idCounter).padStart(12, "0")}`;
86
- }
87
-
88
95
  // ---------------------------------------------------------------------------
89
96
  // Mock token-manager
90
97
  // ---------------------------------------------------------------------------
@@ -107,7 +114,7 @@ mock.module("../security/token-manager.js", () => ({
107
114
  }));
108
115
 
109
116
  // ---------------------------------------------------------------------------
110
- // Mock oauth-store (stateful for disconnect tests)
117
+ // Mock oauth-store
111
118
  // ---------------------------------------------------------------------------
112
119
 
113
120
  mock.module("../oauth/oauth-store.js", () => ({
@@ -147,6 +154,8 @@ mock.module("../oauth/oauth-store.js", () => ({
147
154
  listProviders: () => mockListProviders(),
148
155
  registerProvider: () => ({}),
149
156
  seedProviders: () => {},
157
+ getActiveConnection: () => undefined,
158
+ listActiveConnectionsByProvider: () => [],
150
159
  createConnection: () => ({}),
151
160
  isProviderConnected: () => false,
152
161
  updateConnection: () => ({}),
@@ -167,7 +176,10 @@ mock.module("../security/secure-keys.js", () => ({
167
176
  }
168
177
  return "not-found" as const;
169
178
  },
170
- listSecureKeysAsync: async () => ({ accounts: [...secureKeyStore.keys()], unreachable: false }),
179
+ listSecureKeysAsync: async () => ({
180
+ accounts: [...secureKeyStore.keys()],
181
+ unreachable: false,
182
+ }),
171
183
  _resetBackend: () => {},
172
184
  }));
173
185
 
@@ -206,6 +218,27 @@ mock.module("../oauth/provider-behaviors.js", () => ({
206
218
  mockGetProviderBehavior(providerKey),
207
219
  }));
208
220
 
221
+ // ---------------------------------------------------------------------------
222
+ // Mock connection-resolver (needed by request.ts)
223
+ // ---------------------------------------------------------------------------
224
+
225
+ mock.module("../oauth/connection-resolver.js", () => ({
226
+ resolveOAuthConnection: (
227
+ providerKey: string,
228
+ options?: Record<string, unknown>,
229
+ ) => mockResolveOAuthConnection(providerKey, options),
230
+ }));
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Mock platform/client (needed by request.ts)
234
+ // ---------------------------------------------------------------------------
235
+
236
+ mock.module("../platform/client.js", () => ({
237
+ VellumPlatformClient: {
238
+ create: async () => null,
239
+ },
240
+ }));
241
+
209
242
  mock.module("../util/logger.js", () => ({
210
243
  getLogger: () => ({
211
244
  info: () => {},
@@ -276,29 +309,19 @@ async function runCli(
276
309
  // Tests
277
310
  // ---------------------------------------------------------------------------
278
311
 
279
- describe("assistant oauth connections token <provider-key>", () => {
312
+ describe("assistant oauth token <provider-key>", () => {
280
313
  beforeEach(() => {
281
314
  mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
282
- secureKeyStore = new Map();
283
- metadataStore = [];
284
- disconnectOAuthProviderCalls = [];
285
- disconnectOAuthProviderResult = "not-found";
286
- idCounter = 0;
287
315
  });
288
316
 
289
317
  test("prints bare token in human mode", async () => {
290
- const { exitCode, stdout } = await runCli([
291
- "connections",
292
- "token",
293
- "integration:twitter",
294
- ]);
318
+ const { exitCode, stdout } = await runCli(["token", "integration:twitter"]);
295
319
  expect(exitCode).toBe(0);
296
320
  expect(stdout).toBe("mock-access-token-xyz\n");
297
321
  });
298
322
 
299
323
  test("prints JSON in --json mode", async () => {
300
324
  const { exitCode, stdout } = await runCli([
301
- "connections",
302
325
  "token",
303
326
  "integration:twitter",
304
327
  "--json",
@@ -315,7 +338,7 @@ describe("assistant oauth connections token <provider-key>", () => {
315
338
  return cb("tok");
316
339
  };
317
340
 
318
- await runCli(["connections", "token", "integration:twitter"]);
341
+ await runCli(["token", "integration:twitter"]);
319
342
  expect(capturedService).toBe("integration:twitter");
320
343
  });
321
344
 
@@ -326,11 +349,7 @@ describe("assistant oauth connections token <provider-key>", () => {
326
349
  return cb("gmail-token");
327
350
  };
328
351
 
329
- const { exitCode, stdout } = await runCli([
330
- "connections",
331
- "token",
332
- "integration:google",
333
- ]);
352
+ const { exitCode, stdout } = await runCli(["token", "integration:google"]);
334
353
  expect(exitCode).toBe(0);
335
354
  expect(stdout).toBe("gmail-token\n");
336
355
  expect(capturedService).toBe("integration:google");
@@ -344,7 +363,6 @@ describe("assistant oauth connections token <provider-key>", () => {
344
363
  };
345
364
 
346
365
  const { exitCode, stdout } = await runCli([
347
- "connections",
348
366
  "token",
349
367
  "integration:twitter",
350
368
  "--json",
@@ -363,7 +381,6 @@ describe("assistant oauth connections token <provider-key>", () => {
363
381
  };
364
382
 
365
383
  const { exitCode, stdout } = await runCli([
366
- "connections",
367
384
  "token",
368
385
  "integration:twitter",
369
386
  "--json",
@@ -378,152 +395,17 @@ describe("assistant oauth connections token <provider-key>", () => {
378
395
  // Simulate withValidToken refreshing and returning a new token
379
396
  mockWithValidToken = async (_service, cb) => cb("refreshed-new-token");
380
397
 
381
- const { exitCode, stdout } = await runCli([
382
- "connections",
383
- "token",
384
- "integration:twitter",
385
- ]);
398
+ const { exitCode, stdout } = await runCli(["token", "integration:twitter"]);
386
399
  expect(exitCode).toBe(0);
387
400
  expect(stdout).toBe("refreshed-new-token\n");
388
401
  });
389
402
 
390
403
  test("missing provider-key argument exits non-zero", async () => {
391
- const { exitCode } = await runCli(["connections", "token"]);
404
+ const { exitCode } = await runCli(["token"]);
392
405
  expect(exitCode).not.toBe(0);
393
406
  });
394
407
  });
395
408
 
396
- // ---------------------------------------------------------------------------
397
- // disconnect
398
- // ---------------------------------------------------------------------------
399
-
400
- describe("assistant oauth connections disconnect <provider-key>", () => {
401
- beforeEach(() => {
402
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
403
- secureKeyStore = new Map();
404
- metadataStore = [];
405
- disconnectOAuthProviderCalls = [];
406
- disconnectOAuthProviderResult = "not-found";
407
- idCounter = 0;
408
- });
409
-
410
- test("succeeds when an OAuth connection exists", async () => {
411
- disconnectOAuthProviderResult = "disconnected";
412
-
413
- const result = await runCli([
414
- "connections",
415
- "disconnect",
416
- "integration:google",
417
- "--json",
418
- ]);
419
- expect(result.exitCode).toBe(0);
420
- const parsed = JSON.parse(result.stdout);
421
- expect(parsed.ok).toBe(true);
422
- expect(parsed.service).toBe("integration:google");
423
-
424
- // disconnectOAuthProvider should have been called with the full provider key
425
- expect(disconnectOAuthProviderCalls).toEqual(["integration:google"]);
426
- });
427
-
428
- test("reports not-found when nothing exists", async () => {
429
- const result = await runCli([
430
- "connections",
431
- "disconnect",
432
- "integration:google",
433
- "--json",
434
- ]);
435
- expect(result.exitCode).toBe(1);
436
- const parsed = JSON.parse(result.stdout);
437
- expect(parsed.ok).toBe(false);
438
- expect(parsed.error).toContain("No OAuth connection or credentials");
439
- expect(parsed.error).toContain("integration:google");
440
- });
441
-
442
- test("cleans up legacy credential keys if present", async () => {
443
- // Seed legacy credential keys (no OAuth connection)
444
- const legacyFields = [
445
- "access_token",
446
- "refresh_token",
447
- "client_id",
448
- "client_secret",
449
- ];
450
- for (const field of legacyFields) {
451
- secureKeyStore.set(
452
- credentialKey("integration:google", field),
453
- `legacy_${field}_value`,
454
- );
455
- metadataStore.push({
456
- credentialId: nextUUID(),
457
- service: "integration:google",
458
- field,
459
- allowedTools: [],
460
- allowedDomains: [],
461
- createdAt: Date.now(),
462
- updatedAt: Date.now(),
463
- });
464
- }
465
-
466
- const result = await runCli([
467
- "connections",
468
- "disconnect",
469
- "integration:google",
470
- "--json",
471
- ]);
472
- expect(result.exitCode).toBe(0);
473
- const parsed = JSON.parse(result.stdout);
474
- expect(parsed.ok).toBe(true);
475
- expect(parsed.service).toBe("integration:google");
476
-
477
- // All legacy keys should be removed
478
- for (const field of legacyFields) {
479
- expect(
480
- secureKeyStore.has(credentialKey("integration:google", field)),
481
- ).toBe(false);
482
- expect(
483
- metadataStore.find(
484
- (m) => m.service === "integration:google" && m.field === field,
485
- ),
486
- ).toBeUndefined();
487
- }
488
- });
489
-
490
- test("cleans up both OAuth connection and legacy keys when both exist", async () => {
491
- // Seed OAuth connection
492
- disconnectOAuthProviderResult = "disconnected";
493
-
494
- // Seed a legacy credential key
495
- secureKeyStore.set(
496
- credentialKey("integration:google", "access_token"),
497
- "legacy_token",
498
- );
499
- metadataStore.push({
500
- credentialId: nextUUID(),
501
- service: "integration:google",
502
- field: "access_token",
503
- allowedTools: [],
504
- allowedDomains: [],
505
- createdAt: Date.now(),
506
- updatedAt: Date.now(),
507
- });
508
-
509
- const result = await runCli([
510
- "connections",
511
- "disconnect",
512
- "integration:google",
513
- "--json",
514
- ]);
515
- expect(result.exitCode).toBe(0);
516
- const parsed = JSON.parse(result.stdout);
517
- expect(parsed.ok).toBe(true);
518
-
519
- // Both should be cleaned up
520
- expect(disconnectOAuthProviderCalls).toEqual(["integration:google"]);
521
- expect(
522
- secureKeyStore.has(credentialKey("integration:google", "access_token")),
523
- ).toBe(false);
524
- });
525
- });
526
-
527
409
  // ---------------------------------------------------------------------------
528
410
  // providers list
529
411
  // ---------------------------------------------------------------------------
@@ -575,11 +457,6 @@ describe("assistant oauth providers list", () => {
575
457
  beforeEach(() => {
576
458
  mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
577
459
  mockListProviders = () => fakeProviders;
578
- secureKeyStore = new Map();
579
- metadataStore = [];
580
- disconnectOAuthProviderCalls = [];
581
- disconnectOAuthProviderResult = "not-found";
582
- idCounter = 0;
583
460
  });
584
461
 
585
462
  test("returns all providers when no --provider-key is given", async () => {
@@ -673,320 +550,6 @@ describe("assistant oauth providers list", () => {
673
550
  });
674
551
  });
675
552
 
676
- // ---------------------------------------------------------------------------
677
- // connect
678
- // ---------------------------------------------------------------------------
679
-
680
- describe("assistant oauth connections connect <provider-key>", () => {
681
- beforeEach(() => {
682
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
683
- secureKeyStore = new Map();
684
- metadataStore = [];
685
- disconnectOAuthProviderCalls = [];
686
- disconnectOAuthProviderResult = "not-found";
687
- idCounter = 0;
688
- mockOrchestrateOAuthConnect = async () => ({
689
- success: true,
690
- deferred: false,
691
- grantedScopes: [],
692
- });
693
- mockGetAppByProviderAndClientId = () => undefined;
694
- mockGetMostRecentAppByProvider = () => undefined;
695
- mockGetProvider = () => undefined;
696
- mockGetProviderBehavior = () => undefined;
697
- mockGetSecureKey = () => undefined;
698
- });
699
-
700
- test("completes interactive flow and prints success (human mode)", async () => {
701
- mockGetAppByProviderAndClientId = () => ({
702
- id: "app-1",
703
- clientId: "test-id",
704
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
705
- providerKey: "integration:google",
706
- createdAt: 0,
707
- updatedAt: 0,
708
- });
709
- mockOrchestrateOAuthConnect = async () => ({
710
- success: true,
711
- deferred: false,
712
- grantedScopes: ["read"],
713
- accountInfo: "user@example.com",
714
- });
715
-
716
- const { exitCode, stdout } = await runCli([
717
- "connections",
718
- "connect",
719
- "integration:google",
720
- "--client-id",
721
- "test-id",
722
- ]);
723
- expect(exitCode).toBe(0);
724
- expect(stdout).toContain("Connected");
725
- });
726
-
727
- test("completes interactive flow and returns JSON with --json flag", async () => {
728
- mockGetAppByProviderAndClientId = () => ({
729
- id: "app-1",
730
- clientId: "test-id",
731
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
732
- providerKey: "integration:google",
733
- createdAt: 0,
734
- updatedAt: 0,
735
- });
736
- mockOrchestrateOAuthConnect = async () => ({
737
- success: true,
738
- deferred: false,
739
- grantedScopes: ["read"],
740
- accountInfo: "user@example.com",
741
- });
742
-
743
- const { exitCode, stdout } = await runCli([
744
- "connections",
745
- "connect",
746
- "integration:google",
747
- "--client-id",
748
- "test-id",
749
- "--json",
750
- ]);
751
- expect(exitCode).toBe(0);
752
- const parsed = JSON.parse(stdout);
753
- expect(parsed).toEqual({
754
- ok: true,
755
- grantedScopes: ["read"],
756
- accountInfo: "user@example.com",
757
- });
758
- });
759
-
760
- test("returns auth URL in default (non-interactive) mode (JSON)", async () => {
761
- mockGetAppByProviderAndClientId = () => ({
762
- id: "app-1",
763
- clientId: "test-id",
764
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
765
- providerKey: "integration:google",
766
- createdAt: 0,
767
- updatedAt: 0,
768
- });
769
- mockOrchestrateOAuthConnect = async () => ({
770
- success: true,
771
- deferred: true,
772
- authUrl: "https://example.com/auth",
773
- state: "abc",
774
- service: "integration:google",
775
- });
776
-
777
- const { exitCode, stdout } = await runCli([
778
- "connections",
779
- "connect",
780
- "integration:google",
781
- "--client-id",
782
- "test-id",
783
- "--json",
784
- ]);
785
- expect(exitCode).toBe(0);
786
- const parsed = JSON.parse(stdout);
787
- expect(parsed.ok).toBe(true);
788
- expect(parsed.deferred).toBe(true);
789
- expect(parsed.authUrl).toBe("https://example.com/auth");
790
- });
791
-
792
- test("fails when no client_id available", async () => {
793
- mockGetMostRecentAppByProvider = () => undefined;
794
-
795
- const { exitCode, stdout } = await runCli([
796
- "connections",
797
- "connect",
798
- "integration:google",
799
- "--json",
800
- ]);
801
- expect(exitCode).toBe(1);
802
- const parsed = JSON.parse(stdout);
803
- expect(parsed.ok).toBe(false);
804
- expect(parsed.error).toContain("client_id");
805
- });
806
-
807
- test("resolves client_id from DB when not provided", async () => {
808
- mockGetMostRecentAppByProvider = () => ({
809
- id: "app-1",
810
- clientId: "db-client-id",
811
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
812
- providerKey: "integration:google",
813
- createdAt: 0,
814
- updatedAt: 0,
815
- });
816
-
817
- let capturedClientId: string | undefined;
818
- mockOrchestrateOAuthConnect = async (opts) => {
819
- capturedClientId = opts.clientId as string;
820
- return {
821
- success: true,
822
- deferred: false,
823
- grantedScopes: [],
824
- };
825
- };
826
-
827
- await runCli(["connections", "connect", "integration:google"]);
828
- expect(capturedClientId).toBe("db-client-id");
829
- });
830
-
831
- test("resolves client_secret from secure store when not provided", async () => {
832
- mockGetMostRecentAppByProvider = () => ({
833
- id: "app-1",
834
- clientId: "db-client-id",
835
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
836
- providerKey: "integration:google",
837
- createdAt: 0,
838
- updatedAt: 0,
839
- });
840
-
841
- mockGetSecureKey = (account: string) =>
842
- account === "oauth_app/app-1/client_secret" ? "db-secret" : undefined;
843
-
844
- let capturedOpts: Record<string, unknown> | undefined;
845
- mockOrchestrateOAuthConnect = async (opts) => {
846
- capturedOpts = opts;
847
- return {
848
- success: true,
849
- deferred: false,
850
- grantedScopes: [],
851
- };
852
- };
853
-
854
- await runCli(["connections", "connect", "integration:google"]);
855
- expect(capturedOpts).toBeDefined();
856
- expect(capturedOpts!.clientId).toBe("db-client-id");
857
- expect(capturedOpts!.clientSecret).toBe("db-secret");
858
- });
859
-
860
- test("outputs error from orchestrator", async () => {
861
- mockGetAppByProviderAndClientId = () => ({
862
- id: "app-1",
863
- clientId: "x",
864
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
865
- providerKey: "integration:google",
866
- createdAt: 0,
867
- updatedAt: 0,
868
- });
869
- mockOrchestrateOAuthConnect = async () => ({
870
- success: false,
871
- error: "Something went wrong",
872
- });
873
-
874
- const { exitCode, stdout } = await runCli([
875
- "connections",
876
- "connect",
877
- "integration:google",
878
- "--client-id",
879
- "x",
880
- "--json",
881
- ]);
882
- expect(exitCode).toBe(1);
883
- const parsed = JSON.parse(stdout);
884
- expect(parsed.ok).toBe(false);
885
- expect(parsed.error).toBe("Something went wrong");
886
- });
887
-
888
- test("succeeds when callbackTransport is null (loopback default)", async () => {
889
- // Provider row has callbackTransport: null — orchestrator should default
890
- // to loopback and not require a public ingress URL.
891
- mockGetMostRecentAppByProvider = () => ({
892
- id: "app-loopback",
893
- clientId: "loopback-client",
894
- clientSecretCredentialPath: "oauth_app/app-loopback/client_secret",
895
- providerKey: "integration:test-loopback",
896
- createdAt: 0,
897
- updatedAt: 0,
898
- });
899
-
900
- let capturedOpts: Record<string, unknown> | undefined;
901
- mockOrchestrateOAuthConnect = async (opts) => {
902
- capturedOpts = opts;
903
- return {
904
- success: true,
905
- deferred: true,
906
- authUrl: "https://example.com/auth?loopback",
907
- state: "state-loopback",
908
- service: "integration:test-loopback",
909
- };
910
- };
911
-
912
- const { exitCode, stdout } = await runCli([
913
- "connections",
914
- "connect",
915
- "integration:test-loopback",
916
- "--json",
917
- ]);
918
- expect(exitCode).toBe(0);
919
- const parsed = JSON.parse(stdout);
920
- expect(parsed.ok).toBe(true);
921
- expect(parsed.deferred).toBe(true);
922
- expect(capturedOpts).toBeDefined();
923
- expect(capturedOpts!.clientId).toBe("loopback-client");
924
- });
925
-
926
- test("returns ingress URL error when callbackTransport is explicitly gateway", async () => {
927
- // Provider row has callbackTransport: "gateway" — orchestrator should
928
- // require a public ingress URL, which is not configured in the test env.
929
- mockGetMostRecentAppByProvider = () => ({
930
- id: "app-gateway",
931
- clientId: "gateway-client",
932
- clientSecretCredentialPath: "oauth_app/app-gateway/client_secret",
933
- providerKey: "integration:test-gateway",
934
- createdAt: 0,
935
- updatedAt: 0,
936
- });
937
-
938
- mockOrchestrateOAuthConnect = async () => ({
939
- success: false,
940
- error:
941
- "oauth2_connect from a non-interactive session requires a public ingress URL. Configure ingress.publicBaseUrl first.",
942
- });
943
-
944
- const { exitCode, stdout } = await runCli([
945
- "connections",
946
- "connect",
947
- "integration:test-gateway",
948
- "--json",
949
- ]);
950
- expect(exitCode).toBe(1);
951
- const parsed = JSON.parse(stdout);
952
- expect(parsed.ok).toBe(false);
953
- expect(parsed.error).toContain("requires a public ingress URL");
954
- });
955
-
956
- test("fails when client_secret is required but missing", async () => {
957
- mockGetAppByProviderAndClientId = () => ({
958
- id: "app-1",
959
- clientId: "test-id",
960
- clientSecretCredentialPath: "oauth_app/app-1/client_secret",
961
- providerKey: "integration:google",
962
- createdAt: 0,
963
- updatedAt: 0,
964
- });
965
- mockGetProviderBehavior = () => ({
966
- setup: {
967
- requiresClientSecret: true,
968
- displayName: "Test",
969
- dashboardUrl: "https://example.com",
970
- appType: "app",
971
- },
972
- });
973
-
974
- const { exitCode, stdout } = await runCli([
975
- "connections",
976
- "connect",
977
- "integration:google",
978
- "--client-id",
979
- "test-id",
980
- "--json",
981
- ]);
982
- expect(exitCode).toBe(1);
983
- const parsed = JSON.parse(stdout);
984
- expect(parsed.ok).toBe(false);
985
- expect(parsed.error).toContain("client_secret");
986
- expect(parsed.error).toContain("apps upsert");
987
- });
988
- });
989
-
990
553
  // ---------------------------------------------------------------------------
991
554
  // apps upsert --client-secret-credential-path
992
555
  // ---------------------------------------------------------------------------
@@ -994,11 +557,6 @@ describe("assistant oauth connections connect <provider-key>", () => {
994
557
  describe("assistant oauth apps upsert --client-secret-credential-path", () => {
995
558
  beforeEach(() => {
996
559
  mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
997
- secureKeyStore = new Map();
998
- metadataStore = [];
999
- disconnectOAuthProviderCalls = [];
1000
- disconnectOAuthProviderResult = "not-found";
1001
- idCounter = 0;
1002
560
  mockUpsertAppCalls = [];
1003
561
  mockUpsertAppResult = {
1004
562
  id: "app-upsert-1",
@@ -1207,14 +765,13 @@ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
1207
765
  // ping
1208
766
  // ---------------------------------------------------------------------------
1209
767
 
1210
- describe("assistant oauth connections ping <provider-key>", () => {
768
+ describe("assistant oauth ping <provider-key>", () => {
1211
769
  beforeEach(() => {
1212
770
  mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1213
- secureKeyStore = new Map();
1214
- metadataStore = [];
1215
- disconnectOAuthProviderCalls = [];
1216
- disconnectOAuthProviderResult = "not-found";
1217
- idCounter = 0;
771
+ // Reset resolveOAuthConnection to default (unconfigured)
772
+ mockResolveOAuthConnection = async () => {
773
+ throw new Error("resolveOAuthConnection not configured in test");
774
+ };
1218
775
  });
1219
776
 
1220
777
  test("returns ok when ping endpoint returns 200", async () => {
@@ -1229,29 +786,27 @@ describe("assistant oauth connections ping <provider-key>", () => {
1229
786
  createdAt: Date.now(),
1230
787
  updatedAt: Date.now(),
1231
788
  });
1232
- const originalFetch = globalThis.fetch;
1233
- globalThis.fetch = (async () =>
1234
- new Response("{}", { status: 200 })) as unknown as typeof fetch;
1235
- try {
1236
- const { exitCode, stdout } = await runCli([
1237
- "connections",
1238
- "ping",
1239
- "integration:google",
1240
- "--json",
1241
- ]);
1242
- expect(exitCode).toBe(0);
1243
- const parsed = JSON.parse(stdout);
1244
- expect(parsed.ok).toBe(true);
1245
- expect(parsed.status).toBe(200);
1246
- } finally {
1247
- globalThis.fetch = originalFetch;
1248
- }
789
+ mockResolveOAuthConnection = async () => ({
790
+ id: "conn-1",
791
+ providerKey: "integration:google",
792
+ accountInfo: null,
793
+ request: async () => ({ status: 200, headers: {}, body: {} }),
794
+ withToken: async (fn) => fn("mock-access-token-xyz"),
795
+ });
796
+ const { exitCode, stdout } = await runCli([
797
+ "ping",
798
+ "integration:google",
799
+ "--json",
800
+ ]);
801
+ expect(exitCode).toBe(0);
802
+ const parsed = JSON.parse(stdout);
803
+ expect(parsed.ok).toBe(true);
804
+ expect(parsed.status).toBe(200);
1249
805
  });
1250
806
 
1251
807
  test("exits 1 when provider not found", async () => {
1252
808
  mockGetProvider = () => undefined;
1253
809
  const { exitCode, stdout } = await runCli([
1254
- "connections",
1255
810
  "ping",
1256
811
  "integration:unknown",
1257
812
  "--json",
@@ -1259,7 +814,7 @@ describe("assistant oauth connections ping <provider-key>", () => {
1259
814
  expect(exitCode).toBe(1);
1260
815
  const parsed = JSON.parse(stdout);
1261
816
  expect(parsed.ok).toBe(false);
1262
- expect(parsed.error).toContain("Provider not found");
817
+ expect(parsed.error).toContain("Unknown provider");
1263
818
  });
1264
819
 
1265
820
  test("exits 1 when no ping URL configured", async () => {
@@ -1274,12 +829,7 @@ describe("assistant oauth connections ping <provider-key>", () => {
1274
829
  createdAt: Date.now(),
1275
830
  updatedAt: Date.now(),
1276
831
  });
1277
- const { exitCode, stdout } = await runCli([
1278
- "connections",
1279
- "ping",
1280
- "telegram",
1281
- "--json",
1282
- ]);
832
+ const { exitCode, stdout } = await runCli(["ping", "telegram", "--json"]);
1283
833
  expect(exitCode).toBe(1);
1284
834
  const parsed = JSON.parse(stdout);
1285
835
  expect(parsed.ok).toBe(false);
@@ -1298,30 +848,25 @@ describe("assistant oauth connections ping <provider-key>", () => {
1298
848
  createdAt: Date.now(),
1299
849
  updatedAt: Date.now(),
1300
850
  });
1301
- const originalFetch = globalThis.fetch;
1302
- // Use 403 (not 401) — 401 now throws inside withValidToken for retry
1303
- globalThis.fetch = (async () =>
1304
- new Response("Forbidden", { status: 403 })) as unknown as typeof fetch;
1305
- try {
1306
- const { exitCode, stdout } = await runCli([
1307
- "connections",
1308
- "ping",
1309
- "integration:google",
1310
- "--json",
1311
- ]);
1312
- expect(exitCode).toBe(1);
1313
- const parsed = JSON.parse(stdout);
1314
- expect(parsed.ok).toBe(false);
1315
- expect(parsed.status).toBe(403);
1316
- } finally {
1317
- globalThis.fetch = originalFetch;
1318
- }
851
+ mockResolveOAuthConnection = async () => ({
852
+ id: "conn-1",
853
+ providerKey: "integration:google",
854
+ accountInfo: null,
855
+ request: async () => ({ status: 403, headers: {}, body: "Forbidden" }),
856
+ withToken: async (fn) => fn("mock-access-token-xyz"),
857
+ });
858
+ const { exitCode, stdout } = await runCli([
859
+ "ping",
860
+ "integration:google",
861
+ "--json",
862
+ ]);
863
+ expect(exitCode).toBe(1);
864
+ const parsed = JSON.parse(stdout);
865
+ expect(parsed.ok).toBe(false);
866
+ expect(parsed.status).toBe(403);
1319
867
  });
1320
868
 
1321
- test("exits 1 when no token exists", async () => {
1322
- mockWithValidToken = async () => {
1323
- throw new Error('No access token found for "integration:google".');
1324
- };
869
+ test("exits 1 when no connection can be resolved", async () => {
1325
870
  mockGetProvider = () => ({
1326
871
  providerKey: "integration:google",
1327
872
  pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
@@ -1333,8 +878,10 @@ describe("assistant oauth connections ping <provider-key>", () => {
1333
878
  createdAt: Date.now(),
1334
879
  updatedAt: Date.now(),
1335
880
  });
881
+ mockResolveOAuthConnection = async () => {
882
+ throw new Error('No access token found for "integration:google".');
883
+ };
1336
884
  const { exitCode, stdout } = await runCli([
1337
- "connections",
1338
885
  "ping",
1339
886
  "integration:google",
1340
887
  "--json",