@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -30,6 +30,42 @@ let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
30
30
  "not-found";
31
31
  let idCounter = 0;
32
32
 
33
+ // App upsert mock state
34
+ let mockUpsertAppCalls: Array<{
35
+ provider: string;
36
+ clientId: string;
37
+ clientSecretOpts?: {
38
+ clientSecretValue?: string;
39
+ clientSecretCredentialPath?: string;
40
+ };
41
+ }> = [];
42
+ let mockUpsertAppResult: Record<string, unknown> = {
43
+ id: "app-upsert-1",
44
+ providerKey: "integration:test",
45
+ clientId: "test-client-id",
46
+ createdAt: 1700000000000,
47
+ updatedAt: 1700000000000,
48
+ };
49
+
50
+ // Connect mock state
51
+ let mockOrchestrateOAuthConnect: (
52
+ opts: Record<string, unknown>,
53
+ ) => Promise<Record<string, unknown>>;
54
+ let mockGetAppByProviderAndClientId: (
55
+ providerKey: string,
56
+ clientId: string,
57
+ ) => Record<string, unknown> | undefined = () => undefined;
58
+ let mockGetMostRecentAppByProvider: (
59
+ providerKey: string,
60
+ ) => Record<string, unknown> | undefined = () => undefined;
61
+ let mockGetProvider: (
62
+ providerKey: string,
63
+ ) => Record<string, unknown> | undefined = () => undefined;
64
+ let mockGetProviderBehavior: (
65
+ providerKey: string,
66
+ ) => Record<string, unknown> | undefined = () => undefined;
67
+ let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
68
+
33
69
  function nextUUID(): string {
34
70
  idCounter += 1;
35
71
  return `00000000-0000-0000-0000-${String(idCounter).padStart(12, "0")}`;
@@ -72,13 +108,25 @@ mock.module("../oauth/oauth-store.js", () => ({
72
108
  listConnections: () => [],
73
109
  deleteConnection: () => false,
74
110
  // Stubs required by apps.ts and providers.ts (transitively loaded via oauth/index.ts)
75
- upsertApp: async () => ({}),
111
+ upsertApp: async (
112
+ provider: string,
113
+ clientId: string,
114
+ clientSecretOpts?: {
115
+ clientSecretValue?: string;
116
+ clientSecretCredentialPath?: string;
117
+ },
118
+ ) => {
119
+ mockUpsertAppCalls.push({ provider, clientId, clientSecretOpts });
120
+ return mockUpsertAppResult;
121
+ },
76
122
  getApp: () => undefined,
77
- getAppByProviderAndClientId: () => undefined,
78
- getMostRecentAppByProvider: () => undefined,
123
+ getAppByProviderAndClientId: (providerKey: string, clientId: string) =>
124
+ mockGetAppByProviderAndClientId(providerKey, clientId),
125
+ getMostRecentAppByProvider: (providerKey: string) =>
126
+ mockGetMostRecentAppByProvider(providerKey),
79
127
  listApps: () => [],
80
128
  deleteApp: async () => false,
81
- getProvider: () => undefined,
129
+ getProvider: (providerKey: string) => mockGetProvider(providerKey),
82
130
  listProviders: () => mockListProviders(),
83
131
  registerProvider: () => ({}),
84
132
  seedProviders: () => {},
@@ -89,7 +137,7 @@ mock.module("../oauth/oauth-store.js", () => ({
89
137
 
90
138
  // Stub out transitive dependencies that token-manager would normally pull in
91
139
  mock.module("../security/secure-keys.js", () => ({
92
- getSecureKey: () => undefined,
140
+ getSecureKey: (account: string) => mockGetSecureKey(account),
93
141
  setSecureKey: () => true,
94
142
  getSecureKeyAsync: async () => undefined,
95
143
  setSecureKeyAsync: async () => true,
@@ -129,6 +177,25 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
129
177
  },
130
178
  }));
131
179
 
180
+ // ---------------------------------------------------------------------------
181
+ // Mock connect-orchestrator
182
+ // ---------------------------------------------------------------------------
183
+
184
+ mock.module("../oauth/connect-orchestrator.js", () => ({
185
+ orchestrateOAuthConnect: (opts: Record<string, unknown>) =>
186
+ mockOrchestrateOAuthConnect(opts),
187
+ }));
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Mock provider-behaviors
191
+ // ---------------------------------------------------------------------------
192
+
193
+ mock.module("../oauth/provider-behaviors.js", () => ({
194
+ resolveService: (service: string) => service,
195
+ getProviderBehavior: (providerKey: string) =>
196
+ mockGetProviderBehavior(providerKey),
197
+ }));
198
+
132
199
  mock.module("../util/logger.js", () => ({
133
200
  getLogger: () => ({
134
201
  info: () => {},
@@ -559,4 +626,504 @@ describe("assistant oauth providers list", () => {
559
626
  const parsed = JSON.parse(stdout);
560
627
  expect(parsed).toHaveLength(0);
561
628
  });
629
+
630
+ test("trims whitespace around commas in --provider-key", async () => {
631
+ const { exitCode, stdout } = await runCli([
632
+ "providers",
633
+ "list",
634
+ "--provider-key",
635
+ "gmail, google",
636
+ "--json",
637
+ ]);
638
+ expect(exitCode).toBe(0);
639
+ const parsed = JSON.parse(stdout);
640
+ expect(parsed).toHaveLength(2);
641
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
642
+ expect(keys).toContain("integration:gmail");
643
+ expect(keys).toContain("integration:google-calendar");
644
+ });
645
+
646
+ test("ignores empty segments from extra commas in --provider-key", async () => {
647
+ const { exitCode, stdout } = await runCli([
648
+ "providers",
649
+ "list",
650
+ "--provider-key",
651
+ "gmail,,google",
652
+ "--json",
653
+ ]);
654
+ expect(exitCode).toBe(0);
655
+ const parsed = JSON.parse(stdout);
656
+ expect(parsed).toHaveLength(2);
657
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
658
+ expect(keys).toContain("integration:gmail");
659
+ expect(keys).toContain("integration:google-calendar");
660
+ });
661
+ });
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // connect
665
+ // ---------------------------------------------------------------------------
666
+
667
+ describe("assistant oauth connections connect <provider-key>", () => {
668
+ beforeEach(() => {
669
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
670
+ secureKeyStore = new Map();
671
+ metadataStore = [];
672
+ disconnectOAuthProviderCalls = [];
673
+ disconnectOAuthProviderResult = "not-found";
674
+ idCounter = 0;
675
+ mockOrchestrateOAuthConnect = async () => ({
676
+ success: true,
677
+ deferred: false,
678
+ grantedScopes: [],
679
+ });
680
+ mockGetAppByProviderAndClientId = () => undefined;
681
+ mockGetMostRecentAppByProvider = () => undefined;
682
+ mockGetProvider = () => undefined;
683
+ mockGetProviderBehavior = () => undefined;
684
+ mockGetSecureKey = () => undefined;
685
+ });
686
+
687
+ test("completes interactive flow and prints success (human mode)", async () => {
688
+ mockOrchestrateOAuthConnect = async () => ({
689
+ success: true,
690
+ deferred: false,
691
+ grantedScopes: ["read"],
692
+ accountInfo: "user@example.com",
693
+ });
694
+
695
+ const { exitCode, stdout } = await runCli([
696
+ "connections",
697
+ "connect",
698
+ "integration:gmail",
699
+ "--client-id",
700
+ "test-id",
701
+ ]);
702
+ expect(exitCode).toBe(0);
703
+ expect(stdout).toContain("Connected");
704
+ });
705
+
706
+ test("completes interactive flow and returns JSON with --json flag", async () => {
707
+ mockOrchestrateOAuthConnect = async () => ({
708
+ success: true,
709
+ deferred: false,
710
+ grantedScopes: ["read"],
711
+ accountInfo: "user@example.com",
712
+ });
713
+
714
+ const { exitCode, stdout } = await runCli([
715
+ "connections",
716
+ "connect",
717
+ "integration:gmail",
718
+ "--client-id",
719
+ "test-id",
720
+ "--json",
721
+ ]);
722
+ expect(exitCode).toBe(0);
723
+ const parsed = JSON.parse(stdout);
724
+ expect(parsed).toEqual({
725
+ ok: true,
726
+ grantedScopes: ["read"],
727
+ accountInfo: "user@example.com",
728
+ });
729
+ });
730
+
731
+ test("returns auth URL in default (non-interactive) mode (JSON)", async () => {
732
+ mockOrchestrateOAuthConnect = async () => ({
733
+ success: true,
734
+ deferred: true,
735
+ authUrl: "https://example.com/auth",
736
+ state: "abc",
737
+ service: "integration:gmail",
738
+ });
739
+
740
+ const { exitCode, stdout } = await runCli([
741
+ "connections",
742
+ "connect",
743
+ "integration:gmail",
744
+ "--client-id",
745
+ "test-id",
746
+ "--json",
747
+ ]);
748
+ expect(exitCode).toBe(0);
749
+ const parsed = JSON.parse(stdout);
750
+ expect(parsed.ok).toBe(true);
751
+ expect(parsed.deferred).toBe(true);
752
+ expect(parsed.authUrl).toBe("https://example.com/auth");
753
+ });
754
+
755
+ test("fails when no client_id available", async () => {
756
+ mockGetMostRecentAppByProvider = () => undefined;
757
+
758
+ const { exitCode, stdout } = await runCli([
759
+ "connections",
760
+ "connect",
761
+ "integration:gmail",
762
+ "--json",
763
+ ]);
764
+ expect(exitCode).toBe(1);
765
+ const parsed = JSON.parse(stdout);
766
+ expect(parsed.ok).toBe(false);
767
+ expect(parsed.error).toContain("client_id");
768
+ });
769
+
770
+ test("resolves client_id from DB when not provided", async () => {
771
+ mockGetMostRecentAppByProvider = () => ({
772
+ id: "app-1",
773
+ clientId: "db-client-id",
774
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
775
+ providerKey: "integration:gmail",
776
+ createdAt: 0,
777
+ updatedAt: 0,
778
+ });
779
+
780
+ let capturedClientId: string | undefined;
781
+ mockOrchestrateOAuthConnect = async (opts) => {
782
+ capturedClientId = opts.clientId as string;
783
+ return {
784
+ success: true,
785
+ deferred: false,
786
+ grantedScopes: [],
787
+ };
788
+ };
789
+
790
+ await runCli(["connections", "connect", "integration:gmail"]);
791
+ expect(capturedClientId).toBe("db-client-id");
792
+ });
793
+
794
+ test("resolves client_secret from secure store when not provided", async () => {
795
+ mockGetMostRecentAppByProvider = () => ({
796
+ id: "app-1",
797
+ clientId: "db-client-id",
798
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
799
+ providerKey: "integration:gmail",
800
+ createdAt: 0,
801
+ updatedAt: 0,
802
+ });
803
+
804
+ mockGetSecureKey = (account: string) =>
805
+ account === "oauth_app/app-1/client_secret" ? "db-secret" : undefined;
806
+
807
+ let capturedOpts: Record<string, unknown> | undefined;
808
+ mockOrchestrateOAuthConnect = async (opts) => {
809
+ capturedOpts = opts;
810
+ return {
811
+ success: true,
812
+ deferred: false,
813
+ grantedScopes: [],
814
+ };
815
+ };
816
+
817
+ await runCli(["connections", "connect", "integration:gmail"]);
818
+ expect(capturedOpts).toBeDefined();
819
+ expect(capturedOpts!.clientId).toBe("db-client-id");
820
+ expect(capturedOpts!.clientSecret).toBe("db-secret");
821
+ });
822
+
823
+ test("outputs error from orchestrator", async () => {
824
+ mockOrchestrateOAuthConnect = async () => ({
825
+ success: false,
826
+ error: "Something went wrong",
827
+ });
828
+
829
+ const { exitCode, stdout } = await runCli([
830
+ "connections",
831
+ "connect",
832
+ "integration:gmail",
833
+ "--client-id",
834
+ "x",
835
+ "--json",
836
+ ]);
837
+ expect(exitCode).toBe(1);
838
+ const parsed = JSON.parse(stdout);
839
+ expect(parsed.ok).toBe(false);
840
+ expect(parsed.error).toBe("Something went wrong");
841
+ });
842
+
843
+ test("fails when client_secret is required but missing", async () => {
844
+ mockGetProviderBehavior = () => ({
845
+ setup: {
846
+ requiresClientSecret: true,
847
+ displayName: "Test",
848
+ dashboardUrl: "https://example.com",
849
+ appType: "app",
850
+ },
851
+ });
852
+
853
+ const { exitCode, stdout } = await runCli([
854
+ "connections",
855
+ "connect",
856
+ "integration:gmail",
857
+ "--client-id",
858
+ "test-id",
859
+ "--json",
860
+ ]);
861
+ expect(exitCode).toBe(1);
862
+ const parsed = JSON.parse(stdout);
863
+ expect(parsed.ok).toBe(false);
864
+ expect(parsed.error).toContain("client_secret");
865
+ expect(parsed.error).toContain("apps upsert");
866
+ });
867
+ });
868
+
869
+ // ---------------------------------------------------------------------------
870
+ // apps upsert --client-secret-credential-path
871
+ // ---------------------------------------------------------------------------
872
+
873
+ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
874
+ beforeEach(() => {
875
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
876
+ secureKeyStore = new Map();
877
+ metadataStore = [];
878
+ disconnectOAuthProviderCalls = [];
879
+ disconnectOAuthProviderResult = "not-found";
880
+ idCounter = 0;
881
+ mockUpsertAppCalls = [];
882
+ mockUpsertAppResult = {
883
+ id: "app-upsert-1",
884
+ providerKey: "integration:gmail",
885
+ clientId: "abc123",
886
+ createdAt: 1700000000000,
887
+ updatedAt: 1700000000000,
888
+ };
889
+ mockOrchestrateOAuthConnect = async () => ({
890
+ success: true,
891
+ deferred: false,
892
+ grantedScopes: [],
893
+ });
894
+ mockGetAppByProviderAndClientId = () => undefined;
895
+ mockGetMostRecentAppByProvider = () => undefined;
896
+ mockGetProvider = () => undefined;
897
+ mockGetProviderBehavior = () => undefined;
898
+ mockGetSecureKey = () => undefined;
899
+ });
900
+
901
+ test("upsert with --client-secret-credential-path passes path to upsertApp", async () => {
902
+ const { exitCode, stdout } = await runCli([
903
+ "apps",
904
+ "upsert",
905
+ "--provider",
906
+ "integration:gmail",
907
+ "--client-id",
908
+ "abc123",
909
+ "--client-secret-credential-path",
910
+ "custom/path",
911
+ "--json",
912
+ ]);
913
+ expect(exitCode).toBe(0);
914
+ expect(mockUpsertAppCalls).toHaveLength(1);
915
+ expect(mockUpsertAppCalls[0]).toEqual({
916
+ provider: "integration:gmail",
917
+ clientId: "abc123",
918
+ clientSecretOpts: { clientSecretCredentialPath: "custom/path" },
919
+ });
920
+ const parsed = JSON.parse(stdout);
921
+ expect(parsed.id).toBe("app-upsert-1");
922
+ });
923
+
924
+ test("upsert with both --client-secret and --client-secret-credential-path returns error", async () => {
925
+ const { exitCode, stdout } = await runCli([
926
+ "apps",
927
+ "upsert",
928
+ "--provider",
929
+ "integration:gmail",
930
+ "--client-id",
931
+ "abc123",
932
+ "--client-secret",
933
+ "s3cret",
934
+ "--client-secret-credential-path",
935
+ "custom/path",
936
+ "--json",
937
+ ]);
938
+ expect(exitCode).toBe(1);
939
+ const parsed = JSON.parse(stdout);
940
+ expect(parsed.ok).toBe(false);
941
+ expect(parsed.error).toContain(
942
+ "Cannot provide both --client-secret and --client-secret-credential-path",
943
+ );
944
+ // upsertApp should NOT have been called
945
+ expect(mockUpsertAppCalls).toHaveLength(0);
946
+ });
947
+
948
+ test("upsert with --client-secret passes clientSecretValue to upsertApp", async () => {
949
+ const { exitCode } = await runCli([
950
+ "apps",
951
+ "upsert",
952
+ "--provider",
953
+ "integration:gmail",
954
+ "--client-id",
955
+ "abc123",
956
+ "--client-secret",
957
+ "s3cret",
958
+ "--json",
959
+ ]);
960
+ expect(exitCode).toBe(0);
961
+ expect(mockUpsertAppCalls).toHaveLength(1);
962
+ expect(mockUpsertAppCalls[0]).toEqual({
963
+ provider: "integration:gmail",
964
+ clientId: "abc123",
965
+ clientSecretOpts: { clientSecretValue: "s3cret" },
966
+ });
967
+ });
968
+
969
+ test("upsert without any secret option passes undefined", async () => {
970
+ const { exitCode } = await runCli([
971
+ "apps",
972
+ "upsert",
973
+ "--provider",
974
+ "integration:gmail",
975
+ "--client-id",
976
+ "abc123",
977
+ "--json",
978
+ ]);
979
+ expect(exitCode).toBe(0);
980
+ expect(mockUpsertAppCalls).toHaveLength(1);
981
+ expect(mockUpsertAppCalls[0]).toEqual({
982
+ provider: "integration:gmail",
983
+ clientId: "abc123",
984
+ clientSecretOpts: undefined,
985
+ });
986
+ });
987
+ });
988
+
989
+ // ---------------------------------------------------------------------------
990
+ // ping
991
+ // ---------------------------------------------------------------------------
992
+
993
+ describe("assistant oauth connections ping <provider-key>", () => {
994
+ beforeEach(() => {
995
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
996
+ secureKeyStore = new Map();
997
+ metadataStore = [];
998
+ disconnectOAuthProviderCalls = [];
999
+ disconnectOAuthProviderResult = "not-found";
1000
+ idCounter = 0;
1001
+ });
1002
+
1003
+ test("returns ok when ping endpoint returns 200", async () => {
1004
+ mockGetProvider = () => ({
1005
+ providerKey: "integration:gmail",
1006
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1007
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1008
+ tokenUrl: "https://oauth2.googleapis.com/token",
1009
+ defaultScopes: "[]",
1010
+ scopePolicy: "{}",
1011
+ extraParams: null,
1012
+ createdAt: Date.now(),
1013
+ updatedAt: Date.now(),
1014
+ });
1015
+ const originalFetch = globalThis.fetch;
1016
+ globalThis.fetch = (async () =>
1017
+ new Response("{}", { status: 200 })) as unknown as typeof fetch;
1018
+ try {
1019
+ const { exitCode, stdout } = await runCli([
1020
+ "connections",
1021
+ "ping",
1022
+ "integration:gmail",
1023
+ "--json",
1024
+ ]);
1025
+ expect(exitCode).toBe(0);
1026
+ const parsed = JSON.parse(stdout);
1027
+ expect(parsed.ok).toBe(true);
1028
+ expect(parsed.status).toBe(200);
1029
+ } finally {
1030
+ globalThis.fetch = originalFetch;
1031
+ }
1032
+ });
1033
+
1034
+ test("exits 1 when provider not found", async () => {
1035
+ mockGetProvider = () => undefined;
1036
+ const { exitCode, stdout } = await runCli([
1037
+ "connections",
1038
+ "ping",
1039
+ "integration:unknown",
1040
+ "--json",
1041
+ ]);
1042
+ expect(exitCode).toBe(1);
1043
+ const parsed = JSON.parse(stdout);
1044
+ expect(parsed.ok).toBe(false);
1045
+ expect(parsed.error).toContain("Provider not found");
1046
+ });
1047
+
1048
+ test("exits 1 when no ping URL configured", async () => {
1049
+ mockGetProvider = () => ({
1050
+ providerKey: "telegram",
1051
+ pingUrl: null,
1052
+ authUrl: "urn:manual-token",
1053
+ tokenUrl: "urn:manual-token",
1054
+ defaultScopes: "[]",
1055
+ scopePolicy: "{}",
1056
+ extraParams: null,
1057
+ createdAt: Date.now(),
1058
+ updatedAt: Date.now(),
1059
+ });
1060
+ const { exitCode, stdout } = await runCli([
1061
+ "connections",
1062
+ "ping",
1063
+ "telegram",
1064
+ "--json",
1065
+ ]);
1066
+ expect(exitCode).toBe(1);
1067
+ const parsed = JSON.parse(stdout);
1068
+ expect(parsed.ok).toBe(false);
1069
+ expect(parsed.error).toContain("No ping URL configured");
1070
+ });
1071
+
1072
+ test("exits 1 when ping endpoint returns non-2xx", async () => {
1073
+ mockGetProvider = () => ({
1074
+ providerKey: "integration:gmail",
1075
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1076
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1077
+ tokenUrl: "https://oauth2.googleapis.com/token",
1078
+ defaultScopes: "[]",
1079
+ scopePolicy: "{}",
1080
+ extraParams: null,
1081
+ createdAt: Date.now(),
1082
+ updatedAt: Date.now(),
1083
+ });
1084
+ const originalFetch = globalThis.fetch;
1085
+ globalThis.fetch = (async () =>
1086
+ new Response("Unauthorized", { status: 401 })) as unknown as typeof fetch;
1087
+ try {
1088
+ const { exitCode, stdout } = await runCli([
1089
+ "connections",
1090
+ "ping",
1091
+ "integration:gmail",
1092
+ "--json",
1093
+ ]);
1094
+ expect(exitCode).toBe(1);
1095
+ const parsed = JSON.parse(stdout);
1096
+ expect(parsed.ok).toBe(false);
1097
+ expect(parsed.status).toBe(401);
1098
+ } finally {
1099
+ globalThis.fetch = originalFetch;
1100
+ }
1101
+ });
1102
+
1103
+ test("exits 1 when no token exists", async () => {
1104
+ mockWithValidToken = async () => {
1105
+ throw new Error('No access token found for "integration:gmail".');
1106
+ };
1107
+ mockGetProvider = () => ({
1108
+ providerKey: "integration:gmail",
1109
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1110
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1111
+ tokenUrl: "https://oauth2.googleapis.com/token",
1112
+ defaultScopes: "[]",
1113
+ scopePolicy: "{}",
1114
+ extraParams: null,
1115
+ createdAt: Date.now(),
1116
+ updatedAt: Date.now(),
1117
+ });
1118
+ const { exitCode, stdout } = await runCli([
1119
+ "connections",
1120
+ "ping",
1121
+ "integration:gmail",
1122
+ "--json",
1123
+ ]);
1124
+ expect(exitCode).toBe(1);
1125
+ const parsed = JSON.parse(stdout);
1126
+ expect(parsed.ok).toBe(false);
1127
+ expect(parsed.error).toContain("No access token");
1128
+ });
562
1129
  });