@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -0,0 +1,574 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import { Command } from "commander";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mock state
7
+ // ---------------------------------------------------------------------------
8
+
9
+ let mockGetProvider: (
10
+ key: string,
11
+ ) => Record<string, unknown> | undefined = () => undefined;
12
+
13
+ let mockDeleteProviderResult = false;
14
+
15
+ let mockListAppsResult: Array<Record<string, unknown>> = [];
16
+
17
+ let mockListConnectionsResult: Array<Record<string, unknown>> = [];
18
+
19
+ let mockDeleteAppCalls: string[] = [];
20
+ let mockDeleteAppResult = true;
21
+
22
+ let mockDisconnectCalls: Array<{
23
+ providerKey: string;
24
+ clientId: string | undefined;
25
+ connectionId: string | undefined;
26
+ }> = [];
27
+ let mockDisconnectResult: "disconnected" | "not-found" | "error" =
28
+ "disconnected";
29
+
30
+ let mockDeleteConnectionCalls: Array<string | number> = [];
31
+
32
+ let mockSeededProviderKeys = new Set<string>(["google", "slack", "github"]);
33
+
34
+ let mockLogInfoCalls: string[] = [];
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Mocks
38
+ // ---------------------------------------------------------------------------
39
+
40
+ mock.module("../../../../config/loader.js", () => ({
41
+ getConfig: () => ({ services: {} }),
42
+ loadConfig: () => ({ services: {} }),
43
+ API_KEY_PROVIDERS: [],
44
+ }));
45
+
46
+ mock.module("../../../../oauth/oauth-store.js", () => ({
47
+ getProvider: (key: string) => mockGetProvider(key),
48
+ deleteProvider: () => mockDeleteProviderResult,
49
+ listApps: () => mockListAppsResult,
50
+ listConnections: (providerKey?: string) => {
51
+ if (providerKey) {
52
+ return mockListConnectionsResult.filter(
53
+ (c) => c.providerKey === providerKey,
54
+ );
55
+ }
56
+ return mockListConnectionsResult;
57
+ },
58
+ deleteApp: async (id: string) => {
59
+ mockDeleteAppCalls.push(id);
60
+ return mockDeleteAppResult;
61
+ },
62
+ disconnectOAuthProvider: async (
63
+ providerKey: string,
64
+ clientId?: string,
65
+ connectionId?: string,
66
+ ) => {
67
+ mockDisconnectCalls.push({ providerKey, clientId, connectionId });
68
+ return mockDisconnectResult;
69
+ },
70
+ listProviders: () => [],
71
+ registerProvider: () => ({}),
72
+ seedProviders: () => {},
73
+ upsertApp: async () => ({}),
74
+ getApp: () => undefined,
75
+ getAppByProviderAndClientId: () => undefined,
76
+ getMostRecentAppByProvider: () => undefined,
77
+ createConnection: () => ({}),
78
+ updateConnection: () => ({}),
79
+ getConnection: () => undefined,
80
+ getActiveConnection: () => undefined,
81
+ getConnectionByProvider: () => undefined,
82
+ listActiveConnectionsByProvider: () => [],
83
+ isProviderConnected: () => false,
84
+ deleteConnection: (id: string | number) => {
85
+ mockDeleteConnectionCalls.push(id);
86
+ return true;
87
+ },
88
+ }));
89
+
90
+ mock.module("../../../../oauth/seed-providers.js", () => ({
91
+ SEEDED_PROVIDER_KEYS: mockSeededProviderKeys,
92
+ seedOAuthProviders: () => {},
93
+ }));
94
+
95
+ mock.module("../../../../inbound/public-ingress-urls.js", () => ({
96
+ getOAuthCallbackUrl: () => null,
97
+ }));
98
+
99
+ mock.module("../../../../util/logger.js", () => ({
100
+ getLogger: () => ({
101
+ info: () => {},
102
+ warn: () => {},
103
+ error: () => {},
104
+ debug: () => {},
105
+ }),
106
+ getCliLogger: () => ({
107
+ info: (msg: string) => {
108
+ mockLogInfoCalls.push(msg);
109
+ },
110
+ warn: () => {},
111
+ error: () => {},
112
+ debug: () => {},
113
+ }),
114
+ }));
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Import module under test (after mocks are registered)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ const { registerProviderCommands } = await import("../providers.js");
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Test helper
124
+ // ---------------------------------------------------------------------------
125
+
126
+ async function runCommand(
127
+ args: string[],
128
+ ): Promise<{ stdout: string; exitCode: number }> {
129
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
130
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
131
+ const stdoutChunks: string[] = [];
132
+
133
+ process.stdout.write = ((chunk: unknown) => {
134
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
135
+ return true;
136
+ }) as typeof process.stdout.write;
137
+
138
+ process.stderr.write = (() => true) as typeof process.stderr.write;
139
+
140
+ process.exitCode = 0;
141
+
142
+ try {
143
+ const program = new Command();
144
+ program.exitOverride();
145
+ program.option("--json", "JSON output");
146
+ program.configureOutput({
147
+ writeErr: () => {},
148
+ writeOut: (str: string) => stdoutChunks.push(str),
149
+ });
150
+ registerProviderCommands(program);
151
+ await program.parseAsync(["node", "assistant", ...args]);
152
+ } catch {
153
+ if (process.exitCode === 0) process.exitCode = 1;
154
+ } finally {
155
+ process.stdout.write = originalStdoutWrite;
156
+ process.stderr.write = originalStderrWrite;
157
+ }
158
+
159
+ const exitCode = process.exitCode ?? 0;
160
+ process.exitCode = 0;
161
+
162
+ return { exitCode, stdout: stdoutChunks.join("") };
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Tests
167
+ // ---------------------------------------------------------------------------
168
+
169
+ describe("assistant oauth providers delete", () => {
170
+ beforeEach(() => {
171
+ mockGetProvider = () => undefined;
172
+ mockDeleteProviderResult = true;
173
+ mockListAppsResult = [];
174
+ mockListConnectionsResult = [];
175
+ mockDeleteAppCalls = [];
176
+ mockDeleteAppResult = true;
177
+ mockDisconnectCalls = [];
178
+ mockDisconnectResult = "disconnected";
179
+ mockDeleteConnectionCalls = [];
180
+ mockSeededProviderKeys = new Set(["google", "slack", "github"]);
181
+ mockLogInfoCalls = [];
182
+ process.exitCode = 0;
183
+ });
184
+
185
+ // -------------------------------------------------------------------------
186
+ // Provider not found
187
+ // -------------------------------------------------------------------------
188
+
189
+ test("provider not found returns exit code 1 with actionable error", async () => {
190
+ mockGetProvider = () => undefined;
191
+
192
+ const { exitCode, stdout } = await runCommand([
193
+ "providers",
194
+ "delete",
195
+ "nonexistent",
196
+ "--json",
197
+ ]);
198
+ expect(exitCode).toBe(1);
199
+ const parsed = JSON.parse(stdout);
200
+ expect(parsed.ok).toBe(false);
201
+ expect(parsed.error).toContain("not found");
202
+ expect(parsed.error).toContain("nonexistent");
203
+ expect(parsed.error).toContain("providers list");
204
+ });
205
+
206
+ // -------------------------------------------------------------------------
207
+ // Provider with dependents, no --force
208
+ // -------------------------------------------------------------------------
209
+
210
+ test("provider with dependents and no --force returns exit code 1 with counts", async () => {
211
+ mockGetProvider = (key) =>
212
+ key === "custom-api"
213
+ ? { providerKey: "custom-api", authUrl: "https://example.com/auth" }
214
+ : undefined;
215
+
216
+ mockListAppsResult = [
217
+ {
218
+ id: "app-1",
219
+ providerKey: "custom-api",
220
+ clientId: "c1",
221
+ createdAt: Date.now(),
222
+ updatedAt: Date.now(),
223
+ },
224
+ {
225
+ id: "app-2",
226
+ providerKey: "custom-api",
227
+ clientId: "c2",
228
+ createdAt: Date.now(),
229
+ updatedAt: Date.now(),
230
+ },
231
+ ];
232
+
233
+ mockListConnectionsResult = [
234
+ {
235
+ id: "conn-1",
236
+ providerKey: "custom-api",
237
+ oauthAppId: "app-1",
238
+ status: "active",
239
+ },
240
+ {
241
+ id: "conn-2",
242
+ providerKey: "custom-api",
243
+ oauthAppId: "app-1",
244
+ status: "active",
245
+ },
246
+ {
247
+ id: "conn-3",
248
+ providerKey: "custom-api",
249
+ oauthAppId: "app-2",
250
+ status: "active",
251
+ },
252
+ ];
253
+
254
+ const { exitCode, stdout } = await runCommand([
255
+ "providers",
256
+ "delete",
257
+ "custom-api",
258
+ "--json",
259
+ ]);
260
+ expect(exitCode).toBe(1);
261
+ const parsed = JSON.parse(stdout);
262
+ expect(parsed.ok).toBe(false);
263
+ expect(parsed.error).toContain("2 app(s)");
264
+ expect(parsed.error).toContain("3 connection(s)");
265
+ expect(parsed.error).toContain("--force");
266
+ });
267
+
268
+ // -------------------------------------------------------------------------
269
+ // Provider with dependents, --force
270
+ // -------------------------------------------------------------------------
271
+
272
+ test("provider with dependents and --force cascades deletion and returns summary", async () => {
273
+ mockGetProvider = (key) =>
274
+ key === "custom-api"
275
+ ? { providerKey: "custom-api", authUrl: "https://example.com/auth" }
276
+ : undefined;
277
+
278
+ mockListAppsResult = [
279
+ {
280
+ id: "app-1",
281
+ providerKey: "custom-api",
282
+ clientId: "c1",
283
+ clientSecretCredentialPath: "cred/app-1",
284
+ createdAt: Date.now(),
285
+ updatedAt: Date.now(),
286
+ },
287
+ {
288
+ id: "app-other",
289
+ providerKey: "other-provider",
290
+ clientId: "c3",
291
+ clientSecretCredentialPath: "cred/app-other",
292
+ createdAt: Date.now(),
293
+ updatedAt: Date.now(),
294
+ },
295
+ ];
296
+
297
+ mockListConnectionsResult = [
298
+ {
299
+ id: "conn-1",
300
+ providerKey: "custom-api",
301
+ oauthAppId: "app-1",
302
+ status: "active",
303
+ },
304
+ {
305
+ id: "conn-2",
306
+ providerKey: "custom-api",
307
+ oauthAppId: "app-1",
308
+ status: "active",
309
+ },
310
+ ];
311
+
312
+ const { exitCode, stdout } = await runCommand([
313
+ "providers",
314
+ "delete",
315
+ "custom-api",
316
+ "--force",
317
+ "--json",
318
+ ]);
319
+ expect(exitCode).toBe(0);
320
+ const parsed = JSON.parse(stdout);
321
+ expect(parsed.ok).toBe(true);
322
+ expect(parsed.deleted.provider).toBe(1);
323
+ expect(parsed.deleted.apps).toBe(1); // Only custom-api apps, not other-provider
324
+ expect(parsed.deleted.connections).toBe(2);
325
+
326
+ // Verify disconnectOAuthProvider was called for each connection
327
+ expect(mockDisconnectCalls).toEqual([
328
+ {
329
+ providerKey: "custom-api",
330
+ clientId: undefined,
331
+ connectionId: "conn-1",
332
+ },
333
+ {
334
+ providerKey: "custom-api",
335
+ clientId: undefined,
336
+ connectionId: "conn-2",
337
+ },
338
+ ]);
339
+
340
+ // Verify only matching apps were deleted (not app-other)
341
+ expect(mockDeleteAppCalls).toEqual(["app-1"]);
342
+ });
343
+
344
+ // -------------------------------------------------------------------------
345
+ // Provider with no dependents, no --force
346
+ // -------------------------------------------------------------------------
347
+
348
+ test("provider with no dependents and no --force deletes cleanly", async () => {
349
+ mockGetProvider = (key) =>
350
+ key === "custom-api"
351
+ ? { providerKey: "custom-api", authUrl: "https://example.com/auth" }
352
+ : undefined;
353
+
354
+ mockListAppsResult = [];
355
+ mockListConnectionsResult = [];
356
+
357
+ const { exitCode, stdout } = await runCommand([
358
+ "providers",
359
+ "delete",
360
+ "custom-api",
361
+ "--json",
362
+ ]);
363
+ expect(exitCode).toBe(0);
364
+ const parsed = JSON.parse(stdout);
365
+ expect(parsed.ok).toBe(true);
366
+ expect(parsed.deleted.provider).toBe(1);
367
+ expect(parsed.deleted.apps).toBe(0);
368
+ expect(parsed.deleted.connections).toBe(0);
369
+
370
+ // No cascade deletes should have happened
371
+ expect(mockDisconnectCalls).toHaveLength(0);
372
+ expect(mockDeleteAppCalls).toHaveLength(0);
373
+ });
374
+
375
+ // -------------------------------------------------------------------------
376
+ // Built-in provider with --force logs warning
377
+ // -------------------------------------------------------------------------
378
+
379
+ test("built-in provider with --force succeeds and logs warning about re-creation", async () => {
380
+ mockGetProvider = (key) =>
381
+ key === "google"
382
+ ? { providerKey: "google", authUrl: "https://accounts.google.com" }
383
+ : undefined;
384
+
385
+ mockListAppsResult = [
386
+ {
387
+ id: "app-g",
388
+ providerKey: "google",
389
+ clientId: "goog-client",
390
+ clientSecretCredentialPath: "cred/app-g",
391
+ createdAt: Date.now(),
392
+ updatedAt: Date.now(),
393
+ },
394
+ ];
395
+
396
+ mockListConnectionsResult = [
397
+ {
398
+ id: "conn-g",
399
+ providerKey: "google",
400
+ oauthAppId: "app-g",
401
+ status: "active",
402
+ },
403
+ ];
404
+
405
+ const { exitCode, stdout } = await runCommand([
406
+ "providers",
407
+ "delete",
408
+ "google",
409
+ "--force",
410
+ "--json",
411
+ ]);
412
+ expect(exitCode).toBe(0);
413
+ const parsed = JSON.parse(stdout);
414
+ expect(parsed.ok).toBe(true);
415
+ expect(parsed.deleted.provider).toBe(1);
416
+ expect(parsed.deleted.apps).toBe(1);
417
+ expect(parsed.deleted.connections).toBe(1);
418
+
419
+ // Should have logged a warning about re-creation
420
+ const warningLogged = mockLogInfoCalls.some(
421
+ (msg) => msg.includes("built-in") && msg.includes("re-created"),
422
+ );
423
+ expect(warningLogged).toBe(true);
424
+ });
425
+
426
+ // -------------------------------------------------------------------------
427
+ // Built-in provider without --force and no dependents logs warning
428
+ // -------------------------------------------------------------------------
429
+
430
+ test("built-in provider without --force and no dependents logs warning and deletes", async () => {
431
+ mockGetProvider = (key) =>
432
+ key === "google"
433
+ ? { providerKey: "google", authUrl: "https://accounts.google.com" }
434
+ : undefined;
435
+
436
+ mockListAppsResult = [];
437
+ mockListConnectionsResult = [];
438
+
439
+ const { exitCode, stdout } = await runCommand([
440
+ "providers",
441
+ "delete",
442
+ "google",
443
+ "--json",
444
+ ]);
445
+ expect(exitCode).toBe(0);
446
+ const parsed = JSON.parse(stdout);
447
+ expect(parsed.ok).toBe(true);
448
+ expect(parsed.deleted.provider).toBe(1);
449
+
450
+ // Should have logged a warning about re-creation
451
+ const warningLogged = mockLogInfoCalls.some(
452
+ (msg) => msg.includes("built-in") && msg.includes("re-created"),
453
+ );
454
+ expect(warningLogged).toBe(true);
455
+ });
456
+
457
+ // -------------------------------------------------------------------------
458
+ // Token cleanup error is logged but does not abort cascade
459
+ // -------------------------------------------------------------------------
460
+
461
+ test("token cleanup error logs warning but continues cascade delete", async () => {
462
+ mockGetProvider = (key) =>
463
+ key === "custom-api"
464
+ ? { providerKey: "custom-api", authUrl: "https://example.com/auth" }
465
+ : undefined;
466
+
467
+ mockListAppsResult = [
468
+ {
469
+ id: "app-1",
470
+ providerKey: "custom-api",
471
+ clientId: "c1",
472
+ createdAt: Date.now(),
473
+ updatedAt: Date.now(),
474
+ },
475
+ ];
476
+
477
+ mockListConnectionsResult = [
478
+ {
479
+ id: "conn-1",
480
+ providerKey: "custom-api",
481
+ oauthAppId: "app-1",
482
+ status: "active",
483
+ },
484
+ ];
485
+
486
+ // Simulate token cleanup failure
487
+ mockDisconnectResult = "error";
488
+
489
+ const { exitCode, stdout } = await runCommand([
490
+ "providers",
491
+ "delete",
492
+ "custom-api",
493
+ "--force",
494
+ "--json",
495
+ ]);
496
+ // Should still succeed despite token cleanup error
497
+ expect(exitCode).toBe(0);
498
+ const parsed = JSON.parse(stdout);
499
+ expect(parsed.ok).toBe(true);
500
+ expect(parsed.deleted.provider).toBe(1);
501
+ expect(parsed.deleted.connections).toBe(1);
502
+
503
+ // Should have logged a warning about the token cleanup failure
504
+ const warningLogged = mockLogInfoCalls.some(
505
+ (msg) =>
506
+ msg.includes("failed to clean up tokens") && msg.includes("conn-1"),
507
+ );
508
+ expect(warningLogged).toBe(true);
509
+
510
+ // Should have called deleteConnection as a fallback
511
+ expect(mockDeleteConnectionCalls).toEqual(["conn-1"]);
512
+ });
513
+
514
+ // -------------------------------------------------------------------------
515
+ // Token cleanup error falls back to deleteConnection
516
+ // -------------------------------------------------------------------------
517
+
518
+ test("token cleanup error calls deleteConnection as fallback to avoid FK violation", async () => {
519
+ mockGetProvider = (key) =>
520
+ key === "custom-api"
521
+ ? { providerKey: "custom-api", authUrl: "https://example.com/auth" }
522
+ : undefined;
523
+
524
+ mockListAppsResult = [
525
+ {
526
+ id: "app-1",
527
+ providerKey: "custom-api",
528
+ clientId: "c1",
529
+ createdAt: Date.now(),
530
+ updatedAt: Date.now(),
531
+ },
532
+ ];
533
+
534
+ mockListConnectionsResult = [
535
+ {
536
+ id: "conn-1",
537
+ providerKey: "custom-api",
538
+ oauthAppId: "app-1",
539
+ status: "active",
540
+ },
541
+ {
542
+ id: "conn-2",
543
+ providerKey: "custom-api",
544
+ oauthAppId: "app-1",
545
+ status: "active",
546
+ },
547
+ ];
548
+
549
+ // Simulate token cleanup failure for all connections
550
+ mockDisconnectResult = "error";
551
+
552
+ const { exitCode, stdout } = await runCommand([
553
+ "providers",
554
+ "delete",
555
+ "custom-api",
556
+ "--force",
557
+ "--json",
558
+ ]);
559
+ expect(exitCode).toBe(0);
560
+ const parsed = JSON.parse(stdout);
561
+ expect(parsed.ok).toBe(true);
562
+
563
+ // Both disconnectOAuthProvider calls should have been made
564
+ expect(mockDisconnectCalls).toHaveLength(2);
565
+ expect(mockDisconnectCalls[0]!.connectionId).toBe("conn-1");
566
+ expect(mockDisconnectCalls[1]!.connectionId).toBe("conn-2");
567
+
568
+ // Both should have fallen back to deleteConnection
569
+ expect(mockDeleteConnectionCalls).toEqual(["conn-1", "conn-2"]);
570
+
571
+ // Apps should still have been deleted after connections were cleaned up
572
+ expect(mockDeleteAppCalls).toEqual(["app-1"]);
573
+ });
574
+ });