@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
@@ -0,0 +1,672 @@
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 mockListActiveConnectionsByProvider: (
14
+ providerKey: string,
15
+ ) => Array<Record<string, unknown>> = () => [];
16
+
17
+ let mockGetManagedServiceConfigKey: (key: string) => string | null = () => null;
18
+
19
+ let mockPlatformClientResult: Record<string, unknown> | null = null;
20
+ let mockPlatformFetchResults: Array<{
21
+ ok: boolean;
22
+ status: number;
23
+ body: unknown;
24
+ }> = [];
25
+ let mockPlatformFetchCallIndex = 0;
26
+
27
+ let mockRawConfig: Record<string, unknown> = {};
28
+ let mockSaveRawConfigCalls: Array<Record<string, unknown>> = [];
29
+ let mockSetNestedValueCalls: Array<{
30
+ obj: Record<string, unknown>;
31
+ path: string;
32
+ value: unknown;
33
+ }> = [];
34
+
35
+ let mockConfigServices: Record<string, unknown> = {};
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Mocks
39
+ // ---------------------------------------------------------------------------
40
+
41
+ mock.module("../../../../config/loader.js", () => ({
42
+ getConfig: () => ({ services: mockConfigServices }),
43
+ loadRawConfig: () => mockRawConfig,
44
+ saveRawConfig: (config: Record<string, unknown>) => {
45
+ mockSaveRawConfigCalls.push(structuredClone(config));
46
+ },
47
+ setNestedValue: (
48
+ obj: Record<string, unknown>,
49
+ path: string,
50
+ value: unknown,
51
+ ) => {
52
+ mockSetNestedValueCalls.push({ obj, path, value });
53
+ // Actually set the value so the mock raw config is mutated
54
+ const keys = path.split(".");
55
+ let current: Record<string, unknown> = obj;
56
+ for (let i = 0; i < keys.length - 1; i++) {
57
+ const key = keys[i];
58
+ if (current[key] == null || typeof current[key] !== "object") {
59
+ current[key] = {};
60
+ }
61
+ current = current[key] as Record<string, unknown>;
62
+ }
63
+ current[keys[keys.length - 1]] = value;
64
+ },
65
+ API_KEY_PROVIDERS: [],
66
+ }));
67
+
68
+ mock.module("../../../../oauth/oauth-store.js", () => ({
69
+ getProvider: (key: string) => mockGetProvider(key),
70
+ listActiveConnectionsByProvider: (providerKey: string) =>
71
+ mockListActiveConnectionsByProvider(providerKey),
72
+ listConnections: () => [],
73
+ getConnection: () => undefined,
74
+ getConnectionByProvider: () => undefined,
75
+ getActiveConnection: () => undefined,
76
+ disconnectOAuthProvider: async () => "not-found" as const,
77
+ upsertApp: async () => ({}),
78
+ getApp: () => undefined,
79
+ getAppByProviderAndClientId: () => undefined,
80
+ getMostRecentAppByProvider: () => undefined,
81
+ listApps: () => [],
82
+ deleteApp: async () => false,
83
+ listProviders: () => [],
84
+ registerProvider: () => ({}),
85
+ seedProviders: () => {},
86
+ isProviderConnected: () => false,
87
+ createConnection: () => ({}),
88
+ updateConnection: () => ({}),
89
+ deleteConnection: () => false,
90
+ }));
91
+
92
+ mock.module("../../../../oauth/provider-behaviors.js", () => ({
93
+ resolveService: (service: string) => {
94
+ const aliases: Record<string, string> = {
95
+ gmail: "integration:google",
96
+ google: "integration:google",
97
+ slack: "integration:slack",
98
+ };
99
+ if (aliases[service]) return aliases[service];
100
+ if (!service.includes(":")) return `integration:${service}`;
101
+ return service;
102
+ },
103
+ getProviderBehavior: () => undefined,
104
+ }));
105
+
106
+ mock.module("../../../../platform/client.js", () => ({
107
+ VellumPlatformClient: {
108
+ create: async () => mockPlatformClientResult,
109
+ },
110
+ }));
111
+
112
+ mock.module("../../../../util/logger.js", () => ({
113
+ getLogger: () => ({
114
+ info: () => {},
115
+ warn: () => {},
116
+ error: () => {},
117
+ debug: () => {},
118
+ }),
119
+ getCliLogger: () => ({
120
+ info: () => {},
121
+ warn: () => {},
122
+ error: () => {},
123
+ debug: () => {},
124
+ }),
125
+ }));
126
+
127
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
128
+ getSecureKeyViaDaemon: async () => undefined,
129
+ deleteSecureKeyViaDaemon: async () => "not-found" as const,
130
+ }));
131
+
132
+ // Mock shared.js helpers
133
+ mock.module("../shared.js", () => ({
134
+ resolveService: (service: string) => {
135
+ const aliases: Record<string, string> = {
136
+ gmail: "integration:google",
137
+ google: "integration:google",
138
+ slack: "integration:slack",
139
+ };
140
+ if (aliases[service]) return aliases[service];
141
+ if (!service.includes(":")) return `integration:${service}`;
142
+ return service;
143
+ },
144
+ isManagedMode: () => false,
145
+ getManagedServiceConfigKey: (key: string) =>
146
+ mockGetManagedServiceConfigKey(key),
147
+ requirePlatformClient: async (_cmd: Command) => {
148
+ if (
149
+ !mockPlatformClientResult ||
150
+ !(mockPlatformClientResult as Record<string, unknown>).platformAssistantId
151
+ ) {
152
+ process.exitCode = 1;
153
+ process.stdout.write(
154
+ JSON.stringify({
155
+ ok: false,
156
+ error:
157
+ "Platform prerequisites not met (not logged in or missing assistant ID)",
158
+ }) + "\n",
159
+ );
160
+ return null;
161
+ }
162
+ return {
163
+ platformAssistantId: (mockPlatformClientResult as Record<string, unknown>)
164
+ .platformAssistantId,
165
+ fetch: async (): Promise<Response> => {
166
+ const idx = mockPlatformFetchCallIndex++;
167
+ const result = mockPlatformFetchResults[idx] ?? {
168
+ ok: false,
169
+ status: 500,
170
+ body: "mock not configured",
171
+ };
172
+ return {
173
+ ok: result.ok,
174
+ status: result.status,
175
+ json: async () => result.body,
176
+ text: async () =>
177
+ typeof result.body === "string"
178
+ ? result.body
179
+ : JSON.stringify(result.body),
180
+ } as unknown as Response;
181
+ },
182
+ };
183
+ },
184
+ fetchActiveConnections: async (
185
+ _client: Record<string, unknown>,
186
+ _provider: string,
187
+ _cmd: Command,
188
+ _options?: { silent?: boolean },
189
+ ): Promise<Array<Record<string, unknown>> | null> => {
190
+ const idx = mockPlatformFetchCallIndex++;
191
+ const result = mockPlatformFetchResults[idx];
192
+ if (!result) return [];
193
+ if (!result.ok) return null;
194
+ return result.body as Array<Record<string, unknown>>;
195
+ },
196
+ toBareProvider: (provider: string): string =>
197
+ provider.startsWith("integration:")
198
+ ? provider.slice("integration:".length)
199
+ : provider,
200
+ }));
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Import module under test (after mocks are registered)
204
+ // ---------------------------------------------------------------------------
205
+
206
+ const { registerModeCommand } = await import("../mode.js");
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Test helper
210
+ // ---------------------------------------------------------------------------
211
+
212
+ async function runCommand(
213
+ args: string[],
214
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
215
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
216
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
217
+ const stdoutChunks: string[] = [];
218
+ const stderrChunks: string[] = [];
219
+
220
+ process.stdout.write = ((chunk: unknown) => {
221
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
222
+ return true;
223
+ }) as typeof process.stdout.write;
224
+
225
+ process.stderr.write = ((chunk: unknown) => {
226
+ stderrChunks.push(typeof chunk === "string" ? chunk : String(chunk));
227
+ return true;
228
+ }) as typeof process.stderr.write;
229
+
230
+ process.exitCode = 0;
231
+
232
+ try {
233
+ const program = new Command();
234
+ program.exitOverride();
235
+ program.option("--json", "JSON output");
236
+ program.configureOutput({
237
+ writeErr: () => {},
238
+ writeOut: (str: string) => stdoutChunks.push(str),
239
+ });
240
+ registerModeCommand(program);
241
+ await program.parseAsync(["node", "assistant", ...args]);
242
+ } catch {
243
+ if (process.exitCode === 0) process.exitCode = 1;
244
+ } finally {
245
+ process.stdout.write = originalStdoutWrite;
246
+ process.stderr.write = originalStderrWrite;
247
+ }
248
+
249
+ const exitCode = process.exitCode ?? 0;
250
+ process.exitCode = 0;
251
+
252
+ return {
253
+ exitCode,
254
+ stdout: stdoutChunks.join(""),
255
+ stderr: stderrChunks.join(""),
256
+ };
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Tests
261
+ // ---------------------------------------------------------------------------
262
+
263
+ describe("assistant oauth mode", () => {
264
+ beforeEach(() => {
265
+ mockGetProvider = () => undefined;
266
+ mockListActiveConnectionsByProvider = () => [];
267
+ mockGetManagedServiceConfigKey = () => null;
268
+ mockPlatformClientResult = null;
269
+ mockPlatformFetchResults = [];
270
+ mockPlatformFetchCallIndex = 0;
271
+ mockRawConfig = {};
272
+ mockSaveRawConfigCalls = [];
273
+ mockSetNestedValueCalls = [];
274
+ mockConfigServices = {};
275
+ process.exitCode = 0;
276
+ });
277
+
278
+ // =========================================================================
279
+ // Get mode
280
+ // =========================================================================
281
+
282
+ describe("get mode", () => {
283
+ test("unknown provider returns error", async () => {
284
+ mockGetProvider = () => undefined;
285
+
286
+ const { exitCode, stdout } = await runCommand([
287
+ "mode",
288
+ "nonexistent",
289
+ "--json",
290
+ ]);
291
+ expect(exitCode).toBe(1);
292
+ const parsed = JSON.parse(stdout);
293
+ expect(parsed.ok).toBe(false);
294
+ expect(parsed.error).toContain("Unknown provider");
295
+ expect(parsed.error).toContain("providers list");
296
+ });
297
+
298
+ test("provider alias resolution: gmail resolves to integration:google", async () => {
299
+ let capturedProviderKey: string | undefined;
300
+
301
+ mockGetProvider = (key: string) => {
302
+ capturedProviderKey = key;
303
+ return {
304
+ providerKey: key,
305
+ managedServiceConfigKey: "google-oauth",
306
+ };
307
+ };
308
+ mockGetManagedServiceConfigKey = () => "google-oauth";
309
+ mockConfigServices = {
310
+ "google-oauth": { mode: "managed" },
311
+ };
312
+
313
+ await runCommand(["mode", "gmail", "--json"]);
314
+ expect(capturedProviderKey).toBe("integration:google");
315
+ });
316
+
317
+ test("provider without managedServiceConfigKey returns your-own with managedModeSupported: false", async () => {
318
+ mockGetProvider = () => ({
319
+ providerKey: "integration:slack",
320
+ managedServiceConfigKey: null,
321
+ });
322
+ mockGetManagedServiceConfigKey = () => null;
323
+
324
+ const { exitCode, stdout } = await runCommand([
325
+ "mode",
326
+ "slack",
327
+ "--json",
328
+ ]);
329
+ expect(exitCode).toBe(0);
330
+ const parsed = JSON.parse(stdout);
331
+ expect(parsed.ok).toBe(true);
332
+ expect(parsed.provider).toBe("integration:slack");
333
+ expect(parsed.mode).toBe("your-own");
334
+ expect(parsed.managedModeSupported).toBe(false);
335
+ });
336
+
337
+ test("provider in managed mode returns mode: managed with managedModeSupported: true", async () => {
338
+ mockGetProvider = () => ({
339
+ providerKey: "integration:google",
340
+ managedServiceConfigKey: "google-oauth",
341
+ });
342
+ mockGetManagedServiceConfigKey = () => "google-oauth";
343
+ mockConfigServices = {
344
+ "google-oauth": { mode: "managed" },
345
+ };
346
+
347
+ const { exitCode, stdout } = await runCommand([
348
+ "mode",
349
+ "google",
350
+ "--json",
351
+ ]);
352
+ expect(exitCode).toBe(0);
353
+ const parsed = JSON.parse(stdout);
354
+ expect(parsed.ok).toBe(true);
355
+ expect(parsed.provider).toBe("integration:google");
356
+ expect(parsed.mode).toBe("managed");
357
+ expect(parsed.managedModeSupported).toBe(true);
358
+ });
359
+
360
+ test("provider in your-own mode returns mode: your-own with managedModeSupported: true", async () => {
361
+ mockGetProvider = () => ({
362
+ providerKey: "integration:google",
363
+ managedServiceConfigKey: "google-oauth",
364
+ });
365
+ mockGetManagedServiceConfigKey = () => "google-oauth";
366
+ mockConfigServices = {
367
+ "google-oauth": { mode: "your-own" },
368
+ };
369
+
370
+ const { exitCode, stdout } = await runCommand([
371
+ "mode",
372
+ "google",
373
+ "--json",
374
+ ]);
375
+ expect(exitCode).toBe(0);
376
+ const parsed = JSON.parse(stdout);
377
+ expect(parsed.ok).toBe(true);
378
+ expect(parsed.provider).toBe("integration:google");
379
+ expect(parsed.mode).toBe("your-own");
380
+ expect(parsed.managedModeSupported).toBe(true);
381
+ });
382
+ });
383
+
384
+ // =========================================================================
385
+ // Set mode
386
+ // =========================================================================
387
+
388
+ describe("set mode", () => {
389
+ test("invalid mode value returns error listing valid values", async () => {
390
+ mockGetProvider = () => ({
391
+ providerKey: "integration:google",
392
+ managedServiceConfigKey: "google-oauth",
393
+ });
394
+ mockGetManagedServiceConfigKey = () => "google-oauth";
395
+
396
+ const { exitCode, stdout } = await runCommand([
397
+ "mode",
398
+ "google",
399
+ "--set",
400
+ "invalid",
401
+ "--json",
402
+ ]);
403
+ expect(exitCode).toBe(1);
404
+ const parsed = JSON.parse(stdout);
405
+ expect(parsed.ok).toBe(false);
406
+ expect(parsed.error).toContain("invalid");
407
+ expect(parsed.error).toContain("managed");
408
+ expect(parsed.error).toContain("your-own");
409
+ });
410
+
411
+ test("provider without managedServiceConfigKey returns error about managed mode not available when --set managed", async () => {
412
+ mockGetProvider = () => ({
413
+ providerKey: "integration:slack",
414
+ managedServiceConfigKey: null,
415
+ });
416
+ mockGetManagedServiceConfigKey = () => null;
417
+
418
+ const { exitCode, stdout } = await runCommand([
419
+ "mode",
420
+ "slack",
421
+ "--set",
422
+ "managed",
423
+ "--json",
424
+ ]);
425
+ expect(exitCode).toBe(1);
426
+ const parsed = JSON.parse(stdout);
427
+ expect(parsed.ok).toBe(false);
428
+ expect(parsed.error).toContain("Managed mode is not available");
429
+ expect(parsed.error).toContain("integration:slack");
430
+ });
431
+
432
+ test("provider without managedServiceConfigKey treats --set your-own as successful no-op", async () => {
433
+ mockGetProvider = () => ({
434
+ providerKey: "integration:slack",
435
+ managedServiceConfigKey: null,
436
+ });
437
+ mockGetManagedServiceConfigKey = () => null;
438
+
439
+ const { exitCode, stdout } = await runCommand([
440
+ "mode",
441
+ "slack",
442
+ "--set",
443
+ "your-own",
444
+ "--json",
445
+ ]);
446
+ expect(exitCode).toBe(0);
447
+ const parsed = JSON.parse(stdout);
448
+ expect(parsed.ok).toBe(true);
449
+ expect(parsed.provider).toBe("integration:slack");
450
+ expect(parsed.mode).toBe("your-own");
451
+ expect(parsed.changed).toBe(false);
452
+ expect(parsed.managedModeSupported).toBe(false);
453
+ });
454
+
455
+ test("set to same mode returns changed: false", async () => {
456
+ mockGetProvider = () => ({
457
+ providerKey: "integration:google",
458
+ managedServiceConfigKey: "google-oauth",
459
+ });
460
+ mockGetManagedServiceConfigKey = () => "google-oauth";
461
+ mockConfigServices = {
462
+ "google-oauth": { mode: "managed" },
463
+ };
464
+
465
+ const { exitCode, stdout } = await runCommand([
466
+ "mode",
467
+ "google",
468
+ "--set",
469
+ "managed",
470
+ "--json",
471
+ ]);
472
+ expect(exitCode).toBe(0);
473
+ const parsed = JSON.parse(stdout);
474
+ expect(parsed.ok).toBe(true);
475
+ expect(parsed.provider).toBe("integration:google");
476
+ expect(parsed.mode).toBe("managed");
477
+ expect(parsed.changed).toBe(false);
478
+ expect(parsed.managedModeSupported).toBe(true);
479
+ });
480
+
481
+ test("switch managed -> your-own with active managed connections and no BYO connections includes hint", async () => {
482
+ mockGetProvider = () => ({
483
+ providerKey: "integration:google",
484
+ managedServiceConfigKey: "google-oauth",
485
+ });
486
+ mockGetManagedServiceConfigKey = () => "google-oauth";
487
+ mockConfigServices = {
488
+ "google-oauth": { mode: "managed" },
489
+ };
490
+ mockRawConfig = { services: { "google-oauth": { mode: "managed" } } };
491
+
492
+ // Platform has active connections (old mode = managed)
493
+ mockPlatformClientResult = { platformAssistantId: "asst-123" };
494
+ mockPlatformFetchResults = [
495
+ {
496
+ ok: true,
497
+ status: 200,
498
+ body: [{ id: "conn-1", account_label: "user@gmail.com" }],
499
+ },
500
+ ];
501
+
502
+ // No BYO connections (new mode = your-own)
503
+ mockListActiveConnectionsByProvider = () => [];
504
+
505
+ const { exitCode, stdout } = await runCommand([
506
+ "mode",
507
+ "google",
508
+ "--set",
509
+ "your-own",
510
+ "--json",
511
+ ]);
512
+ expect(exitCode).toBe(0);
513
+ const parsed = JSON.parse(stdout);
514
+ expect(parsed.ok).toBe(true);
515
+ expect(parsed.provider).toBe("integration:google");
516
+ expect(parsed.mode).toBe("your-own");
517
+ expect(parsed.changed).toBe(true);
518
+ expect(parsed.managedModeSupported).toBe(true);
519
+ expect(parsed.hint).toContain("No active connections");
520
+ expect(parsed.hint).toContain("your-own");
521
+ expect(parsed.hint).toContain("connect");
522
+ });
523
+
524
+ test("switch your-own -> managed with active BYO connections and no managed connections includes hint", async () => {
525
+ mockGetProvider = () => ({
526
+ providerKey: "integration:google",
527
+ managedServiceConfigKey: "google-oauth",
528
+ });
529
+ mockGetManagedServiceConfigKey = () => "google-oauth";
530
+ mockConfigServices = {
531
+ "google-oauth": { mode: "your-own" },
532
+ };
533
+ mockRawConfig = { services: { "google-oauth": { mode: "your-own" } } };
534
+
535
+ // BYO has active connections (old mode = your-own)
536
+ mockListActiveConnectionsByProvider = () => [
537
+ {
538
+ id: "conn-local-1",
539
+ providerKey: "integration:google",
540
+ status: "active",
541
+ },
542
+ ];
543
+
544
+ // Platform has no connections (new mode = managed)
545
+ mockPlatformClientResult = { platformAssistantId: "asst-123" };
546
+ mockPlatformFetchResults = [{ ok: true, status: 200, body: [] }];
547
+
548
+ const { exitCode, stdout } = await runCommand([
549
+ "mode",
550
+ "google",
551
+ "--set",
552
+ "managed",
553
+ "--json",
554
+ ]);
555
+ expect(exitCode).toBe(0);
556
+ const parsed = JSON.parse(stdout);
557
+ expect(parsed.ok).toBe(true);
558
+ expect(parsed.provider).toBe("integration:google");
559
+ expect(parsed.mode).toBe("managed");
560
+ expect(parsed.changed).toBe(true);
561
+ expect(parsed.managedModeSupported).toBe(true);
562
+ expect(parsed.hint).toContain("No active connections");
563
+ expect(parsed.hint).toContain("managed");
564
+ expect(parsed.hint).toContain("connect");
565
+ });
566
+
567
+ test("switch mode with connections on both sides has no hint", async () => {
568
+ mockGetProvider = () => ({
569
+ providerKey: "integration:google",
570
+ managedServiceConfigKey: "google-oauth",
571
+ });
572
+ mockGetManagedServiceConfigKey = () => "google-oauth";
573
+ mockConfigServices = {
574
+ "google-oauth": { mode: "managed" },
575
+ };
576
+ mockRawConfig = { services: { "google-oauth": { mode: "managed" } } };
577
+
578
+ // Platform has active connections (old mode = managed)
579
+ mockPlatformClientResult = { platformAssistantId: "asst-123" };
580
+ mockPlatformFetchResults = [
581
+ {
582
+ ok: true,
583
+ status: 200,
584
+ body: [{ id: "conn-1", account_label: "user@gmail.com" }],
585
+ },
586
+ ];
587
+
588
+ // BYO also has connections (new mode = your-own)
589
+ mockListActiveConnectionsByProvider = () => [
590
+ {
591
+ id: "conn-local-1",
592
+ providerKey: "integration:google",
593
+ status: "active",
594
+ },
595
+ ];
596
+
597
+ const { exitCode, stdout } = await runCommand([
598
+ "mode",
599
+ "google",
600
+ "--set",
601
+ "your-own",
602
+ "--json",
603
+ ]);
604
+ expect(exitCode).toBe(0);
605
+ const parsed = JSON.parse(stdout);
606
+ expect(parsed.ok).toBe(true);
607
+ expect(parsed.changed).toBe(true);
608
+ expect(parsed.managedModeSupported).toBe(true);
609
+ expect(parsed.hint).toBeUndefined();
610
+ });
611
+
612
+ test("switch mode with no connections on either side has no hint", async () => {
613
+ mockGetProvider = () => ({
614
+ providerKey: "integration:google",
615
+ managedServiceConfigKey: "google-oauth",
616
+ });
617
+ mockGetManagedServiceConfigKey = () => "google-oauth";
618
+ mockConfigServices = {
619
+ "google-oauth": { mode: "managed" },
620
+ };
621
+ mockRawConfig = { services: { "google-oauth": { mode: "managed" } } };
622
+
623
+ // No platform connections
624
+ mockPlatformClientResult = { platformAssistantId: "asst-123" };
625
+ mockPlatformFetchResults = [{ ok: true, status: 200, body: [] }];
626
+
627
+ // No BYO connections
628
+ mockListActiveConnectionsByProvider = () => [];
629
+
630
+ const { exitCode, stdout } = await runCommand([
631
+ "mode",
632
+ "google",
633
+ "--set",
634
+ "your-own",
635
+ "--json",
636
+ ]);
637
+ expect(exitCode).toBe(0);
638
+ const parsed = JSON.parse(stdout);
639
+ expect(parsed.ok).toBe(true);
640
+ expect(parsed.changed).toBe(true);
641
+ expect(parsed.managedModeSupported).toBe(true);
642
+ expect(parsed.hint).toBeUndefined();
643
+ });
644
+
645
+ test("saveRawConfig is called with the correct nested path", async () => {
646
+ mockGetProvider = () => ({
647
+ providerKey: "integration:google",
648
+ managedServiceConfigKey: "google-oauth",
649
+ });
650
+ mockGetManagedServiceConfigKey = () => "google-oauth";
651
+ mockConfigServices = {
652
+ "google-oauth": { mode: "managed" },
653
+ };
654
+ mockRawConfig = { services: { "google-oauth": { mode: "managed" } } };
655
+
656
+ // No platform client — skip connection checking
657
+ mockPlatformClientResult = null;
658
+ mockListActiveConnectionsByProvider = () => [];
659
+
660
+ await runCommand(["mode", "google", "--set", "your-own", "--json"]);
661
+
662
+ // Verify setNestedValue was called with correct path and value
663
+ expect(mockSetNestedValueCalls.length).toBeGreaterThanOrEqual(1);
664
+ const setCall = mockSetNestedValueCalls[0];
665
+ expect(setCall.path).toBe("services.google-oauth.mode");
666
+ expect(setCall.value).toBe("your-own");
667
+
668
+ // Verify saveRawConfig was called
669
+ expect(mockSaveRawConfigCalls.length).toBe(1);
670
+ });
671
+ });
672
+ });