@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,579 @@
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 mockListConnections: (
14
+ providerKey: string,
15
+ ) => Array<Record<string, unknown>> = () => [];
16
+
17
+ let mockIsManagedMode: (key: string) => boolean = () => false;
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
+ // ---------------------------------------------------------------------------
28
+ // Mocks
29
+ // ---------------------------------------------------------------------------
30
+
31
+ mock.module("../../../../config/loader.js", () => ({
32
+ getConfig: () => ({ services: {} }),
33
+ API_KEY_PROVIDERS: [],
34
+ }));
35
+
36
+ mock.module("../../../../oauth/oauth-store.js", () => ({
37
+ getProvider: (key: string) => mockGetProvider(key),
38
+ listConnections: (providerKey: string) => mockListConnections(providerKey),
39
+ getConnection: () => undefined,
40
+ getConnectionByProvider: () => undefined,
41
+ getActiveConnection: () => undefined,
42
+ listActiveConnectionsByProvider: () => [],
43
+ disconnectOAuthProvider: async () => "not-found" as const,
44
+ upsertApp: async () => ({}),
45
+ getApp: () => undefined,
46
+ getAppByProviderAndClientId: () => undefined,
47
+ getMostRecentAppByProvider: () => undefined,
48
+ listApps: () => [],
49
+ deleteApp: async () => false,
50
+ listProviders: () => [],
51
+ registerProvider: () => ({}),
52
+ seedProviders: () => {},
53
+ isProviderConnected: () => false,
54
+ createConnection: () => ({}),
55
+ updateConnection: () => ({}),
56
+ deleteConnection: () => false,
57
+ }));
58
+
59
+ mock.module("../../../../oauth/provider-behaviors.js", () => ({
60
+ resolveService: (service: string) => {
61
+ const aliases: Record<string, string> = {
62
+ gmail: "integration:google",
63
+ google: "integration:google",
64
+ slack: "integration:slack",
65
+ };
66
+ if (aliases[service]) return aliases[service];
67
+ if (!service.includes(":")) return `integration:${service}`;
68
+ return service;
69
+ },
70
+ getProviderBehavior: () => undefined,
71
+ }));
72
+
73
+ mock.module("../../../../oauth/connect-orchestrator.js", () => ({
74
+ orchestrateOAuthConnect: async () => ({
75
+ success: true,
76
+ deferred: false,
77
+ grantedScopes: [],
78
+ }),
79
+ }));
80
+
81
+ mock.module("../../../../platform/client.js", () => ({
82
+ VellumPlatformClient: {
83
+ create: async () => mockPlatformClientResult,
84
+ },
85
+ }));
86
+
87
+ mock.module("../../../../util/browser.js", () => ({
88
+ openInBrowser: () => {},
89
+ }));
90
+
91
+ mock.module("../../../../util/logger.js", () => ({
92
+ getLogger: () => ({
93
+ info: () => {},
94
+ warn: () => {},
95
+ error: () => {},
96
+ debug: () => {},
97
+ }),
98
+ getCliLogger: () => ({
99
+ info: () => {},
100
+ warn: () => {},
101
+ error: () => {},
102
+ debug: () => {},
103
+ }),
104
+ }));
105
+
106
+ mock.module("../../../lib/daemon-credential-client.js", () => ({
107
+ getSecureKeyViaDaemon: async () => undefined,
108
+ deleteSecureKeyViaDaemon: async () => "not-found" as const,
109
+ }));
110
+
111
+ // Mock shared.js helpers to control managed vs BYO mode routing
112
+ mock.module("../shared.js", () => ({
113
+ resolveService: (service: string) => {
114
+ const aliases: Record<string, string> = {
115
+ gmail: "integration:google",
116
+ google: "integration:google",
117
+ slack: "integration:slack",
118
+ };
119
+ if (aliases[service]) return aliases[service];
120
+ if (!service.includes(":")) return `integration:${service}`;
121
+ return service;
122
+ },
123
+ isManagedMode: (key: string) => mockIsManagedMode(key),
124
+ requirePlatformClient: async (_cmd: Command) => {
125
+ if (
126
+ !mockPlatformClientResult ||
127
+ !(mockPlatformClientResult as Record<string, unknown>).platformAssistantId
128
+ ) {
129
+ process.exitCode = 1;
130
+ process.stdout.write(
131
+ JSON.stringify({
132
+ ok: false,
133
+ error:
134
+ "Platform prerequisites not met (not logged in or missing assistant ID)",
135
+ }) + "\n",
136
+ );
137
+ return null;
138
+ }
139
+ return {
140
+ platformAssistantId: (mockPlatformClientResult as Record<string, unknown>)
141
+ .platformAssistantId,
142
+ fetch: async (): Promise<Response> => {
143
+ const idx = mockPlatformFetchCallIndex++;
144
+ const result = mockPlatformFetchResults[idx] ?? {
145
+ ok: false,
146
+ status: 500,
147
+ body: "mock not configured",
148
+ };
149
+ return {
150
+ ok: result.ok,
151
+ status: result.status,
152
+ json: async () => result.body,
153
+ text: async () =>
154
+ typeof result.body === "string"
155
+ ? result.body
156
+ : JSON.stringify(result.body),
157
+ } as unknown as Response;
158
+ },
159
+ };
160
+ },
161
+ fetchActiveConnections: async (): Promise<Array<
162
+ Record<string, unknown>
163
+ > | null> => {
164
+ const idx = mockPlatformFetchCallIndex++;
165
+ const result = mockPlatformFetchResults[idx];
166
+ if (!result) return [];
167
+ if (!result.ok) return null;
168
+ return result.body as Array<Record<string, unknown>>;
169
+ },
170
+ toBareProvider: (provider: string): string =>
171
+ provider.startsWith("integration:")
172
+ ? provider.slice("integration:".length)
173
+ : provider,
174
+ }));
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Import module under test (after mocks are registered)
178
+ // ---------------------------------------------------------------------------
179
+
180
+ const { registerStatusCommand } = await import("../status.js");
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Test helper
184
+ // ---------------------------------------------------------------------------
185
+
186
+ async function runCommand(
187
+ args: string[],
188
+ ): Promise<{ stdout: string; exitCode: number }> {
189
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
190
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
191
+ const stdoutChunks: string[] = [];
192
+
193
+ process.stdout.write = ((chunk: unknown) => {
194
+ stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
195
+ return true;
196
+ }) as typeof process.stdout.write;
197
+
198
+ process.stderr.write = (() => true) as typeof process.stderr.write;
199
+
200
+ process.exitCode = 0;
201
+
202
+ try {
203
+ const program = new Command();
204
+ program.exitOverride();
205
+ program.option("--json", "JSON output");
206
+ program.configureOutput({
207
+ writeErr: () => {},
208
+ writeOut: (str: string) => stdoutChunks.push(str),
209
+ });
210
+ registerStatusCommand(program);
211
+ await program.parseAsync(["node", "assistant", ...args]);
212
+ } catch {
213
+ if (process.exitCode === 0) process.exitCode = 1;
214
+ } finally {
215
+ process.stdout.write = originalStdoutWrite;
216
+ process.stderr.write = originalStderrWrite;
217
+ }
218
+
219
+ const exitCode = process.exitCode ?? 0;
220
+ process.exitCode = 0;
221
+
222
+ return { exitCode, stdout: stdoutChunks.join("") };
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Tests
227
+ // ---------------------------------------------------------------------------
228
+
229
+ describe("assistant oauth status", () => {
230
+ beforeEach(() => {
231
+ mockGetProvider = () => undefined;
232
+ mockListConnections = () => [];
233
+ mockIsManagedMode = () => false;
234
+ mockPlatformClientResult = null;
235
+ mockPlatformFetchResults = [];
236
+ mockPlatformFetchCallIndex = 0;
237
+ process.exitCode = 0;
238
+ });
239
+
240
+ // -------------------------------------------------------------------------
241
+ // Unknown provider
242
+ // -------------------------------------------------------------------------
243
+
244
+ test("unknown provider returns error", async () => {
245
+ mockGetProvider = () => undefined;
246
+
247
+ const { exitCode, stdout } = await runCommand([
248
+ "status",
249
+ "nonexistent",
250
+ "--json",
251
+ ]);
252
+ expect(exitCode).toBe(1);
253
+ const parsed = JSON.parse(stdout);
254
+ expect(parsed.ok).toBe(false);
255
+ expect(parsed.error).toContain("Unknown provider");
256
+ expect(parsed.error).toContain("providers list");
257
+ });
258
+
259
+ // =========================================================================
260
+ // Managed mode tests
261
+ // =========================================================================
262
+
263
+ describe("managed mode", () => {
264
+ beforeEach(() => {
265
+ mockGetProvider = () => ({
266
+ providerKey: "integration:google",
267
+ managedServiceConfigKey: "google-oauth",
268
+ });
269
+ mockIsManagedMode = () => true;
270
+ mockPlatformClientResult = { platformAssistantId: "asst-123" };
271
+ });
272
+
273
+ test("shows platform connections with account labels", async () => {
274
+ mockPlatformFetchResults = [
275
+ {
276
+ ok: true,
277
+ status: 200,
278
+ body: [
279
+ {
280
+ id: "conn-1",
281
+ account_label: "user@gmail.com",
282
+ scopes_granted: ["email", "calendar"],
283
+ status: "ACTIVE",
284
+ },
285
+ {
286
+ id: "conn-2",
287
+ account_label: "work@company.com",
288
+ scopes_granted: ["email"],
289
+ status: "ACTIVE",
290
+ },
291
+ ],
292
+ },
293
+ ];
294
+
295
+ const { exitCode, stdout } = await runCommand([
296
+ "status",
297
+ "google",
298
+ "--json",
299
+ ]);
300
+ expect(exitCode).toBe(0);
301
+ const parsed = JSON.parse(stdout);
302
+ expect(parsed.ok).toBe(true);
303
+ expect(parsed.provider).toBe("integration:google");
304
+ expect(parsed.mode).toBe("managed");
305
+ expect(parsed.connections).toHaveLength(2);
306
+
307
+ // Verify connection structure
308
+ const conn1 = parsed.connections[0];
309
+ expect(conn1.id).toBe("conn-1");
310
+ expect(conn1.account).toBe("user@gmail.com");
311
+ expect(conn1.grantedScopes).toEqual(["email", "calendar"]);
312
+ expect(conn1.status).toBe("ACTIVE");
313
+
314
+ const conn2 = parsed.connections[1];
315
+ expect(conn2.id).toBe("conn-2");
316
+ expect(conn2.account).toBe("work@company.com");
317
+ });
318
+
319
+ test("no connections: empty connections array in JSON", async () => {
320
+ mockPlatformFetchResults = [{ ok: true, status: 200, body: [] }];
321
+
322
+ const { exitCode, stdout } = await runCommand([
323
+ "status",
324
+ "google",
325
+ "--json",
326
+ ]);
327
+ expect(exitCode).toBe(0);
328
+ const parsed = JSON.parse(stdout);
329
+ expect(parsed.ok).toBe(true);
330
+ expect(parsed.provider).toBe("integration:google");
331
+ expect(parsed.mode).toBe("managed");
332
+ expect(parsed.connections).toEqual([]);
333
+ });
334
+
335
+ test("no connections: human output hints at connect command", async () => {
336
+ mockPlatformFetchResults = [{ ok: true, status: 200, body: [] }];
337
+
338
+ // Run without --json to test human output path
339
+ const { exitCode } = await runCommand(["status", "google"]);
340
+ // Should succeed (info message printed via logger, which is mocked)
341
+ expect(exitCode).toBe(0);
342
+ });
343
+
344
+ test("JSON output structure matches contract", async () => {
345
+ mockPlatformFetchResults = [
346
+ {
347
+ ok: true,
348
+ status: 200,
349
+ body: [
350
+ {
351
+ id: "conn-abc",
352
+ account_label: null,
353
+ scopes_granted: [],
354
+ status: "ACTIVE",
355
+ },
356
+ ],
357
+ },
358
+ ];
359
+
360
+ const { exitCode, stdout } = await runCommand([
361
+ "status",
362
+ "google",
363
+ "--json",
364
+ ]);
365
+ expect(exitCode).toBe(0);
366
+ const parsed = JSON.parse(stdout);
367
+
368
+ // Required top-level fields
369
+ expect(parsed).toHaveProperty("ok", true);
370
+ expect(parsed).toHaveProperty("provider");
371
+ expect(parsed).toHaveProperty("mode", "managed");
372
+ expect(parsed).toHaveProperty("connections");
373
+ expect(Array.isArray(parsed.connections)).toBe(true);
374
+
375
+ // Required per-connection fields
376
+ const conn = parsed.connections[0];
377
+ expect(conn).toHaveProperty("id");
378
+ expect(conn).toHaveProperty("account");
379
+ expect(conn).toHaveProperty("grantedScopes");
380
+ expect(conn).toHaveProperty("status");
381
+ });
382
+ });
383
+
384
+ // =========================================================================
385
+ // BYO mode tests
386
+ // =========================================================================
387
+
388
+ describe("BYO mode", () => {
389
+ beforeEach(() => {
390
+ mockGetProvider = () => ({
391
+ providerKey: "integration:google",
392
+ managedServiceConfigKey: null,
393
+ });
394
+ mockIsManagedMode = () => false;
395
+ });
396
+
397
+ test("shows local connections with expiry and refresh info", async () => {
398
+ const expiresAt = Date.now() + 3600_000; // 1 hour from now
399
+ mockListConnections = () => [
400
+ {
401
+ id: "conn-local-1",
402
+ providerKey: "integration:google",
403
+ accountInfo: "localuser@gmail.com",
404
+ grantedScopes: '["email","profile"]',
405
+ expiresAt,
406
+ hasRefreshToken: 1,
407
+ status: "active",
408
+ },
409
+ ];
410
+
411
+ const { exitCode, stdout } = await runCommand([
412
+ "status",
413
+ "google",
414
+ "--json",
415
+ ]);
416
+ expect(exitCode).toBe(0);
417
+ const parsed = JSON.parse(stdout);
418
+ expect(parsed.ok).toBe(true);
419
+ expect(parsed.provider).toBe("integration:google");
420
+ expect(parsed.mode).toBe("byo");
421
+ expect(parsed.connections).toHaveLength(1);
422
+
423
+ const conn = parsed.connections[0];
424
+ expect(conn.id).toBe("conn-local-1");
425
+ expect(conn.account).toBe("localuser@gmail.com");
426
+ expect(conn.grantedScopes).toEqual(["email", "profile"]);
427
+ expect(conn.expiresAt).toBeTruthy();
428
+ expect(conn.hasRefreshToken).toBe(true);
429
+ expect(conn.status).toBe("active");
430
+ });
431
+
432
+ test("shows connection with no refresh token", async () => {
433
+ mockListConnections = () => [
434
+ {
435
+ id: "conn-local-2",
436
+ providerKey: "integration:google",
437
+ accountInfo: null,
438
+ grantedScopes: "[]",
439
+ expiresAt: null,
440
+ hasRefreshToken: 0,
441
+ status: "active",
442
+ },
443
+ ];
444
+
445
+ const { exitCode, stdout } = await runCommand([
446
+ "status",
447
+ "google",
448
+ "--json",
449
+ ]);
450
+ expect(exitCode).toBe(0);
451
+ const parsed = JSON.parse(stdout);
452
+ expect(parsed.ok).toBe(true);
453
+ const conn = parsed.connections[0];
454
+ expect(conn.account).toBeNull();
455
+ expect(conn.expiresAt).toBeNull();
456
+ expect(conn.hasRefreshToken).toBe(false);
457
+ });
458
+
459
+ test("filters to only active connections", async () => {
460
+ mockListConnections = () => [
461
+ {
462
+ id: "conn-active",
463
+ providerKey: "integration:google",
464
+ accountInfo: "user@gmail.com",
465
+ grantedScopes: "[]",
466
+ expiresAt: null,
467
+ hasRefreshToken: 0,
468
+ status: "active",
469
+ },
470
+ {
471
+ id: "conn-revoked",
472
+ providerKey: "integration:google",
473
+ accountInfo: "old@gmail.com",
474
+ grantedScopes: "[]",
475
+ expiresAt: null,
476
+ hasRefreshToken: 0,
477
+ status: "revoked",
478
+ },
479
+ ];
480
+
481
+ const { exitCode, stdout } = await runCommand([
482
+ "status",
483
+ "google",
484
+ "--json",
485
+ ]);
486
+ expect(exitCode).toBe(0);
487
+ const parsed = JSON.parse(stdout);
488
+ expect(parsed.connections).toHaveLength(1);
489
+ expect(parsed.connections[0].id).toBe("conn-active");
490
+ });
491
+
492
+ test("no connections: empty array in JSON output", async () => {
493
+ mockListConnections = () => [];
494
+
495
+ const { exitCode, stdout } = await runCommand([
496
+ "status",
497
+ "google",
498
+ "--json",
499
+ ]);
500
+ expect(exitCode).toBe(0);
501
+ const parsed = JSON.parse(stdout);
502
+ expect(parsed.ok).toBe(true);
503
+ expect(parsed.provider).toBe("integration:google");
504
+ expect(parsed.mode).toBe("byo");
505
+ expect(parsed.connections).toEqual([]);
506
+ });
507
+
508
+ test("no connections: human output hints at connect command", async () => {
509
+ mockListConnections = () => [];
510
+
511
+ // Run without --json — the human output path logs via getCliLogger
512
+ const { exitCode } = await runCommand(["status", "google"]);
513
+ expect(exitCode).toBe(0);
514
+ });
515
+
516
+ test("JSON output structure matches contract", async () => {
517
+ mockListConnections = () => [
518
+ {
519
+ id: "conn-structure",
520
+ providerKey: "integration:google",
521
+ accountInfo: "check@gmail.com",
522
+ grantedScopes: '["scope1"]',
523
+ expiresAt: Date.now() + 60_000,
524
+ hasRefreshToken: 1,
525
+ status: "active",
526
+ },
527
+ ];
528
+
529
+ const { exitCode, stdout } = await runCommand([
530
+ "status",
531
+ "google",
532
+ "--json",
533
+ ]);
534
+ expect(exitCode).toBe(0);
535
+ const parsed = JSON.parse(stdout);
536
+
537
+ // Required top-level fields
538
+ expect(parsed).toHaveProperty("ok", true);
539
+ expect(parsed).toHaveProperty("provider");
540
+ expect(parsed).toHaveProperty("mode", "byo");
541
+ expect(parsed).toHaveProperty("connections");
542
+ expect(Array.isArray(parsed.connections)).toBe(true);
543
+
544
+ // Required per-connection fields for BYO
545
+ const conn = parsed.connections[0];
546
+ expect(conn).toHaveProperty("id");
547
+ expect(conn).toHaveProperty("account");
548
+ expect(conn).toHaveProperty("grantedScopes");
549
+ expect(conn).toHaveProperty("expiresAt");
550
+ expect(conn).toHaveProperty("hasRefreshToken");
551
+ expect(conn).toHaveProperty("status");
552
+ });
553
+
554
+ test("handles malformed grantedScopes JSON gracefully", async () => {
555
+ mockListConnections = () => [
556
+ {
557
+ id: "conn-bad-scopes",
558
+ providerKey: "integration:google",
559
+ accountInfo: null,
560
+ grantedScopes: "not-valid-json",
561
+ expiresAt: null,
562
+ hasRefreshToken: 0,
563
+ status: "active",
564
+ },
565
+ ];
566
+
567
+ const { exitCode, stdout } = await runCommand([
568
+ "status",
569
+ "google",
570
+ "--json",
571
+ ]);
572
+ expect(exitCode).toBe(0);
573
+ const parsed = JSON.parse(stdout);
574
+ expect(parsed.ok).toBe(true);
575
+ // Should default to empty array on parse failure
576
+ expect(parsed.connections[0].grantedScopes).toEqual([]);
577
+ });
578
+ });
579
+ });