@vellumai/assistant 0.4.35 → 0.4.37

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/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +5 -2
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +814 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +494 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +134 -245
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +175 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -2,10 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
4
4
  import type { AssistantConfig } from "../config/schema.js";
5
- import {
6
- isSkillFeatureEnabled,
7
- resolveSkillStates,
8
- } from "../config/skill-state.js";
5
+ import { resolveSkillStates, skillFlagKey } from "../config/skill-state.js";
9
6
  import type { SkillSummary } from "../config/skills.js";
10
7
 
11
8
  const DECLARED_FLAG_KEY = "feature_flags.hatch-new-assistant.enabled";
@@ -55,27 +52,33 @@ function makeSkill(
55
52
  }
56
53
 
57
54
  // ---------------------------------------------------------------------------
58
- // isSkillFeatureEnabled (legacy wrapper backward compat)
55
+ // isAssistantFeatureFlagEnabled with skillFlagKey (canonical path)
59
56
  // ---------------------------------------------------------------------------
60
57
 
61
- describe("isSkillFeatureEnabled", () => {
58
+ describe("isAssistantFeatureFlagEnabled with skillFlagKey", () => {
62
59
  test("returns false when no flag overrides (registry default is false)", () => {
63
60
  const config = makeConfig();
64
- expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(false);
61
+ expect(
62
+ isAssistantFeatureFlagEnabled(skillFlagKey(DECLARED_SKILL_ID), config),
63
+ ).toBe(false);
65
64
  });
66
65
 
67
66
  test("returns true when skill key is explicitly true", () => {
68
67
  const config = makeConfig({
69
68
  assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: true },
70
69
  });
71
- expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(true);
70
+ expect(
71
+ isAssistantFeatureFlagEnabled(skillFlagKey(DECLARED_SKILL_ID), config),
72
+ ).toBe(true);
72
73
  });
73
74
 
74
75
  test("returns false when skill key is explicitly false", () => {
75
76
  const config = makeConfig({
76
77
  assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
77
78
  });
78
- expect(isSkillFeatureEnabled(DECLARED_SKILL_ID, config)).toBe(false);
79
+ expect(
80
+ isAssistantFeatureFlagEnabled(skillFlagKey(DECLARED_SKILL_ID), config),
81
+ ).toBe(false);
79
82
  });
80
83
  });
81
84
 
@@ -51,18 +51,39 @@ const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
51
51
  migrateToDataLayout: () => {},
52
52
  removeSocketFile: () => {},
53
53
  };
54
- mock.module("../util/platform.js", () => platformOverrides);
54
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
55
+ const realPlatform = require("../util/platform.js");
56
+ mock.module("../util/platform.js", () => ({
57
+ ...realPlatform,
58
+ ...platformOverrides,
59
+ }));
60
+
61
+ const noopLogger = new Proxy({} as Record<string, unknown>, {
62
+ get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
63
+ });
55
64
 
65
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
66
+ const realLogger = require("../util/logger.js");
56
67
  mock.module("../util/logger.js", () => ({
57
- getLogger: () =>
58
- new Proxy({} as Record<string, unknown>, {
59
- get: () => () => {},
60
- }),
68
+ ...realLogger,
69
+ getLogger: () => noopLogger,
70
+ getCliLogger: () => noopLogger,
61
71
  isDebug: () => false,
72
+ truncateForLog: (value: string) => value,
73
+ initLogger: () => {},
74
+ pruneOldLogFiles: () => 0,
62
75
  }));
63
76
 
64
77
  mock.module("../config/loader.js", () => ({
65
78
  getConfig: () => currentConfig,
79
+ loadConfig: () => currentConfig,
80
+ loadRawConfig: () => ({}),
81
+ saveConfig: () => {},
82
+ saveRawConfig: () => {},
83
+ invalidateConfigCache: () => {},
84
+ getNestedValue: () => undefined,
85
+ setNestedValue: () => {},
86
+ syncConfigToLockfile: () => {},
66
87
  }));
67
88
 
68
89
  await import("../tools/skills/load.js");
@@ -69,7 +69,6 @@ mock.module("../config/skills.js", () => ({
69
69
  // only needs skillFlagKey and doesn't exercise resolveSkillStates.
70
70
  mock.module("../config/skill-state.js", () => ({
71
71
  skillFlagKey: (id: string) => `feature_flags.${id}.enabled`,
72
- isSkillFeatureEnabled: () => true,
73
72
  resolveSkillStates: () => [],
74
73
  }));
75
74
 
@@ -12,33 +12,63 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
12
12
 
13
13
  const TEST_DIR = join(tmpdir(), `vellum-skills-test-${crypto.randomUUID()}`);
14
14
 
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
16
+ const realPlatform = require("../util/platform.js");
15
17
  mock.module("../util/platform.js", () => ({
18
+ ...realPlatform,
16
19
  getRootDir: () => TEST_DIR,
17
20
  getDataDir: () => TEST_DIR,
21
+ getIpcBlobDir: () => join(TEST_DIR, "ipc-blobs"),
22
+ getSandboxRootDir: () => join(TEST_DIR, "sandbox"),
23
+ getSandboxWorkingDir: () => TEST_DIR,
24
+ getInterfacesDir: () => join(TEST_DIR, "interfaces"),
18
25
  ensureDataDir: () => {},
19
26
  getSocketPath: () => join(TEST_DIR, "vellum.sock"),
20
27
  getPidPath: () => join(TEST_DIR, "vellum.pid"),
21
28
  getDbPath: () => join(TEST_DIR, "data", "assistant.db"),
22
29
  getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
30
+ getHistoryPath: () => join(TEST_DIR, "history"),
23
31
  isMacOS: () => process.platform === "darwin",
24
32
  isLinux: () => process.platform === "linux",
25
33
  isWindows: () => process.platform === "win32",
26
34
  getPlatformName: () => process.platform,
35
+ getClipboardCommand: () => null,
27
36
  getWorkspaceConfigPath: () => join(TEST_DIR, "config.json"),
28
37
  getWorkspaceSkillsDir: () => join(TEST_DIR, "skills"),
38
+ getWorkspaceHooksDir: () => join(TEST_DIR, "hooks"),
39
+ getHooksDir: () => join(TEST_DIR, "hooks"),
29
40
  getWorkspaceDir: () => TEST_DIR,
30
41
  getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
31
42
  migrateToDataLayout: () => {},
32
43
  migrateToWorkspaceLayout: () => {},
44
+ migratePath: () => {},
45
+ readSessionToken: () => null,
46
+ removeSocketFile: () => {},
47
+ normalizeAssistantId: (id: string) => id,
48
+ readLockfile: () => null,
49
+ writeLockfile: () => {},
33
50
  }));
34
51
 
52
+ const noopLogger = {
53
+ info: () => {},
54
+ warn: () => {},
55
+ error: () => {},
56
+ debug: () => {},
57
+ trace: () => {},
58
+ fatal: () => {},
59
+ child: () => noopLogger,
60
+ };
61
+
62
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
63
+ const realLogger = require("../util/logger.js");
35
64
  mock.module("../util/logger.js", () => ({
36
- getLogger: () =>
37
- new Proxy({} as Record<string, unknown>, {
38
- get: () => () => {},
39
- }),
65
+ ...realLogger,
66
+ getLogger: () => noopLogger,
67
+ getCliLogger: () => noopLogger,
40
68
  isDebug: () => false,
41
69
  truncateForLog: (v: string) => v,
70
+ initLogger: () => {},
71
+ pruneOldLogFiles: () => 0,
42
72
  }));
43
73
 
44
74
  const { loadSkillCatalog, loadSkillBySelector, resolveSkillSelector } =
@@ -61,9 +61,9 @@ mock.module("../security/secure-keys.js", () => ({
61
61
  deleteSecureKey: (account: string) => {
62
62
  if (account in secureKeyStore) {
63
63
  delete secureKeyStore[account];
64
- return true;
64
+ return "deleted";
65
65
  }
66
- return false;
66
+ return "not-found";
67
67
  },
68
68
  listSecureKeys: () => Object.keys(secureKeyStore),
69
69
  getBackendType: () => "encrypted",
@@ -14,7 +14,10 @@ const TEST_DIR = join(tmpdir(), `vellum-sysprompt-test-${crypto.randomUUID()}`);
14
14
 
15
15
  import { mock } from "bun:test";
16
16
 
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const realPlatform = require("../util/platform.js");
17
19
  mock.module("../util/platform.js", () => ({
20
+ ...realPlatform,
18
21
  getRootDir: () => TEST_DIR,
19
22
  getDataDir: () => TEST_DIR,
20
23
  getWorkspaceDir: () => TEST_DIR,
@@ -38,19 +41,27 @@ mock.module("../util/platform.js", () => ({
38
41
  isWindows: () => process.platform === "win32",
39
42
  getPlatformName: () => process.platform,
40
43
  getClipboardCommand: () => null,
44
+ readSessionToken: () => null,
41
45
  removeSocketFile: () => {},
42
46
  migratePath: () => {},
43
47
  migrateToWorkspaceLayout: () => {},
44
48
  migrateToDataLayout: () => {},
45
49
  }));
46
50
 
51
+ const noopLogger = new Proxy({} as Record<string, unknown>, {
52
+ get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
53
+ });
54
+
55
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
56
+ const realLogger = require("../util/logger.js");
47
57
  mock.module("../util/logger.js", () => ({
48
- getLogger: () =>
49
- new Proxy({} as Record<string, unknown>, {
50
- get: () => () => {},
51
- }),
58
+ ...realLogger,
59
+ getLogger: () => noopLogger,
60
+ getCliLogger: () => noopLogger,
52
61
  isDebug: () => false,
53
62
  truncateForLog: (v: string) => v,
63
+ initLogger: () => {},
64
+ pruneOldLogFiles: () => 0,
54
65
  }));
55
66
 
56
67
  mock.module("../config/loader.js", () => ({
@@ -59,9 +70,20 @@ mock.module("../config/loader.js", () => ({
59
70
 
60
71
  sandbox: { enabled: true },
61
72
  }),
73
+ loadConfig: () => ({}),
74
+ loadRawConfig: () => ({}),
75
+ saveConfig: () => {},
76
+ saveRawConfig: () => {},
77
+ invalidateConfigCache: () => {},
78
+ getNestedValue: () => undefined,
79
+ setNestedValue: () => {},
80
+ syncConfigToLockfile: () => {},
62
81
  }));
63
82
 
83
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
84
+ const realUserReference = require("../config/user-reference.js");
64
85
  mock.module("../config/user-reference.js", () => ({
86
+ ...realUserReference,
65
87
  resolveUserReference: () => "John",
66
88
  resolveUserPronouns: () => null,
67
89
  }));
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Tests for `ensureTelegramBotUsernameResolved()`.
3
+ *
4
+ * This function fills the bot-username gap when the token was configured
5
+ * without a `getMe` call (e.g. via `credential set` or ingress secret
6
+ * redirect). Each branch is exercised in isolation by controlling the
7
+ * mutable mock variables below.
8
+ */
9
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Mutable mock state — tests toggle these before each call
13
+ // ---------------------------------------------------------------------------
14
+
15
+ let mockMetadata: { accountInfo?: string } | undefined;
16
+ let mockSecureKey: string | undefined;
17
+ let mockUpsertCalls: Array<{
18
+ service: string;
19
+ key: string;
20
+ patch: Record<string, unknown>;
21
+ }> = [];
22
+
23
+ mock.module("../tools/credentials/metadata-store.js", () => ({
24
+ getCredentialMetadata: (_service: string, _key: string) => mockMetadata,
25
+ upsertCredentialMetadata: (
26
+ service: string,
27
+ key: string,
28
+ patch: Record<string, unknown>,
29
+ ) => {
30
+ mockUpsertCalls.push({ service, key, patch });
31
+ },
32
+ }));
33
+
34
+ mock.module("../security/secure-keys.js", () => ({
35
+ getSecureKey: (_keyId: string) => mockSecureKey,
36
+ }));
37
+
38
+ // Suppress logger output during tests
39
+ mock.module("../util/logger.js", () => ({
40
+ getLogger: () =>
41
+ new Proxy({} as Record<string, unknown>, {
42
+ get: () => () => {},
43
+ }),
44
+ }));
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Global fetch mock — swapped per test
48
+ // ---------------------------------------------------------------------------
49
+
50
+ let mockFetchResponse: {
51
+ ok: boolean;
52
+ status: number;
53
+ json: () => Promise<unknown>;
54
+ };
55
+ let mockFetchThrows: Error | undefined;
56
+ let fetchCallCount = 0;
57
+
58
+ beforeEach(() => {
59
+ mockMetadata = undefined;
60
+ mockSecureKey = undefined;
61
+ mockUpsertCalls = [];
62
+ mockFetchThrows = undefined;
63
+ mockFetchResponse = {
64
+ ok: true,
65
+ status: 200,
66
+ json: async () => ({ ok: true, result: { username: "ResolvedBot" } }),
67
+ };
68
+ fetchCallCount = 0;
69
+
70
+ globalThis.fetch = (async (..._args: unknown[]) => {
71
+ fetchCallCount++;
72
+ if (mockFetchThrows) throw mockFetchThrows;
73
+ return mockFetchResponse;
74
+ }) as typeof globalThis.fetch;
75
+ });
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Import under test — AFTER mocks are registered
79
+ // ---------------------------------------------------------------------------
80
+
81
+ const { ensureTelegramBotUsernameResolved } =
82
+ await import("../runtime/channel-invite-transports/telegram.js");
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Tests
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("ensureTelegramBotUsernameResolved", () => {
89
+ test("(a) early-returns when accountInfo is already cached", async () => {
90
+ mockMetadata = { accountInfo: "CachedBot" };
91
+ mockSecureKey = "some-token";
92
+
93
+ await ensureTelegramBotUsernameResolved();
94
+
95
+ expect(fetchCallCount).toBe(0);
96
+ expect(mockUpsertCalls).toHaveLength(0);
97
+ });
98
+
99
+ test("(b) fetches getMe and caches username on success", async () => {
100
+ mockMetadata = undefined;
101
+ mockSecureKey = "bot-token-123";
102
+ mockFetchResponse = {
103
+ ok: true,
104
+ status: 200,
105
+ json: async () => ({ ok: true, result: { username: "MyNewBot" } }),
106
+ };
107
+
108
+ await ensureTelegramBotUsernameResolved();
109
+
110
+ expect(fetchCallCount).toBe(1);
111
+ expect(mockUpsertCalls).toEqual([
112
+ {
113
+ service: "telegram",
114
+ key: "bot_token",
115
+ patch: { accountInfo: "MyNewBot" },
116
+ },
117
+ ]);
118
+ });
119
+
120
+ test("(c) handles non-200 response gracefully without caching", async () => {
121
+ mockMetadata = undefined;
122
+ mockSecureKey = "bot-token-123";
123
+ mockFetchResponse = {
124
+ ok: false,
125
+ status: 401,
126
+ json: async () => ({ ok: false, description: "Unauthorized" }),
127
+ };
128
+
129
+ await ensureTelegramBotUsernameResolved();
130
+
131
+ expect(fetchCallCount).toBe(1);
132
+ expect(mockUpsertCalls).toHaveLength(0);
133
+ });
134
+
135
+ test("(d) handles missing username in response gracefully", async () => {
136
+ mockMetadata = undefined;
137
+ mockSecureKey = "bot-token-123";
138
+ mockFetchResponse = {
139
+ ok: true,
140
+ status: 200,
141
+ json: async () => ({ ok: true, result: {} }),
142
+ };
143
+
144
+ await ensureTelegramBotUsernameResolved();
145
+
146
+ expect(fetchCallCount).toBe(1);
147
+ expect(mockUpsertCalls).toHaveLength(0);
148
+ });
149
+
150
+ test("(e) handles network errors (fetch throws) gracefully", async () => {
151
+ mockMetadata = undefined;
152
+ mockSecureKey = "bot-token-123";
153
+ mockFetchThrows = new Error("ECONNREFUSED");
154
+
155
+ await ensureTelegramBotUsernameResolved();
156
+
157
+ expect(fetchCallCount).toBe(1);
158
+ expect(mockUpsertCalls).toHaveLength(0);
159
+ });
160
+
161
+ test("(f) no-ops when no bot token is configured", async () => {
162
+ mockMetadata = undefined;
163
+ mockSecureKey = undefined;
164
+
165
+ await ensureTelegramBotUsernameResolved();
166
+
167
+ expect(fetchCallCount).toBe(0);
168
+ expect(mockUpsertCalls).toHaveLength(0);
169
+ });
170
+
171
+ test("treats whitespace-only accountInfo as uncached", async () => {
172
+ mockMetadata = { accountInfo: " " };
173
+ mockSecureKey = "bot-token-123";
174
+ mockFetchResponse = {
175
+ ok: true,
176
+ status: 200,
177
+ json: async () => ({ ok: true, result: { username: "FreshBot" } }),
178
+ };
179
+
180
+ await ensureTelegramBotUsernameResolved();
181
+
182
+ expect(fetchCallCount).toBe(1);
183
+ expect(mockUpsertCalls).toEqual([
184
+ {
185
+ service: "telegram",
186
+ key: "bot_token",
187
+ patch: { accountInfo: "FreshBot" },
188
+ },
189
+ ]);
190
+ });
191
+
192
+ test("treats empty-string accountInfo as uncached", async () => {
193
+ mockMetadata = { accountInfo: "" };
194
+ mockSecureKey = "bot-token-456";
195
+ mockFetchResponse = {
196
+ ok: true,
197
+ status: 200,
198
+ json: async () => ({ ok: true, result: { username: "AnotherBot" } }),
199
+ };
200
+
201
+ await ensureTelegramBotUsernameResolved();
202
+
203
+ expect(fetchCallCount).toBe(1);
204
+ expect(mockUpsertCalls).toEqual([
205
+ {
206
+ service: "telegram",
207
+ key: "bot_token",
208
+ patch: { accountInfo: "AnotherBot" },
209
+ },
210
+ ]);
211
+ });
212
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Tests for the Telegram channel invite adapter.
3
+ *
4
+ * Covers `buildShareLink`, `extractInboundToken`, and
5
+ * `resolveChannelHandle` on the real production adapter.
6
+ */
7
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
8
+
9
+ import type { ChannelId } from "../channels/types.js";
10
+ import { telegramInviteAdapter } from "../runtime/channel-invite-transports/telegram.js";
11
+
12
+ // Mock credential metadata so tests don't depend on local persisted state.
13
+ mock.module("../tools/credentials/metadata-store.js", () => ({
14
+ getCredentialMetadata: () => undefined,
15
+ }));
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const CHANNEL: ChannelId = "telegram" as ChannelId;
22
+
23
+ function setEnv(key: string, value: string | undefined) {
24
+ if (value === undefined) {
25
+ delete process.env[key];
26
+ } else {
27
+ process.env[key] = value;
28
+ }
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // buildShareLink
33
+ // ---------------------------------------------------------------------------
34
+
35
+ describe("telegramInviteAdapter.buildShareLink", () => {
36
+ let originalBotUsername: string | undefined;
37
+
38
+ beforeEach(() => {
39
+ originalBotUsername = process.env.TELEGRAM_BOT_USERNAME;
40
+ });
41
+
42
+ afterEach(() => {
43
+ setEnv("TELEGRAM_BOT_USERNAME", originalBotUsername);
44
+ });
45
+
46
+ test("builds a deep link with iv_ prefix", () => {
47
+ setEnv("TELEGRAM_BOT_USERNAME", "TestBot");
48
+
49
+ const link = telegramInviteAdapter.buildShareLink!({
50
+ rawToken: "abc123",
51
+ sourceChannel: CHANNEL,
52
+ });
53
+
54
+ expect(link.url).toBe("https://t.me/TestBot?start=iv_abc123");
55
+ expect(link.displayText).toContain("https://t.me/TestBot?start=iv_abc123");
56
+ });
57
+
58
+ test("throws when bot username is not configured", () => {
59
+ setEnv("TELEGRAM_BOT_USERNAME", undefined);
60
+
61
+ expect(() =>
62
+ telegramInviteAdapter.buildShareLink!({
63
+ rawToken: "abc123",
64
+ sourceChannel: CHANNEL,
65
+ }),
66
+ ).toThrow(/bot username/i);
67
+ });
68
+ });
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // extractInboundToken
72
+ // ---------------------------------------------------------------------------
73
+
74
+ describe("telegramInviteAdapter.extractInboundToken", () => {
75
+ test("extracts token from structured commandIntent", () => {
76
+ const token = telegramInviteAdapter.extractInboundToken!({
77
+ commandIntent: { type: "start", payload: "iv_tok123" },
78
+ content: "/start iv_tok123",
79
+ });
80
+
81
+ expect(token).toBe("tok123");
82
+ });
83
+
84
+ test("returns undefined for non-invite commandIntent payload", () => {
85
+ const token = telegramInviteAdapter.extractInboundToken!({
86
+ commandIntent: { type: "start", payload: "gv_guardian_token" },
87
+ content: "/start gv_guardian_token",
88
+ });
89
+
90
+ expect(token).toBeUndefined();
91
+ });
92
+
93
+ test("returns undefined for commandIntent with empty payload after prefix", () => {
94
+ const token = telegramInviteAdapter.extractInboundToken!({
95
+ commandIntent: { type: "start", payload: "iv_" },
96
+ content: "/start iv_",
97
+ });
98
+
99
+ expect(token).toBeUndefined();
100
+ });
101
+
102
+ test("falls back to raw content parsing when no commandIntent", () => {
103
+ const token = telegramInviteAdapter.extractInboundToken!({
104
+ content: "/start iv_fallback_token",
105
+ });
106
+
107
+ expect(token).toBe("fallback_token");
108
+ });
109
+
110
+ test("returns undefined for non-invite raw content", () => {
111
+ const token = telegramInviteAdapter.extractInboundToken!({
112
+ content: "/start gv_something",
113
+ });
114
+
115
+ expect(token).toBeUndefined();
116
+ });
117
+
118
+ test("returns undefined for plain text content", () => {
119
+ const token = telegramInviteAdapter.extractInboundToken!({
120
+ content: "hello world",
121
+ });
122
+
123
+ expect(token).toBeUndefined();
124
+ });
125
+
126
+ test("returns undefined for commandIntent with non-start type", () => {
127
+ const token = telegramInviteAdapter.extractInboundToken!({
128
+ commandIntent: { type: "help", payload: "iv_tok" },
129
+ content: "/help iv_tok",
130
+ });
131
+
132
+ expect(token).toBeUndefined();
133
+ });
134
+ });
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // resolveChannelHandle
138
+ // ---------------------------------------------------------------------------
139
+
140
+ describe("telegramInviteAdapter.resolveChannelHandle", () => {
141
+ let originalBotUsername: string | undefined;
142
+
143
+ beforeEach(() => {
144
+ originalBotUsername = process.env.TELEGRAM_BOT_USERNAME;
145
+ });
146
+
147
+ afterEach(() => {
148
+ setEnv("TELEGRAM_BOT_USERNAME", originalBotUsername);
149
+ });
150
+
151
+ test("returns @-prefixed bot username from env", () => {
152
+ setEnv("TELEGRAM_BOT_USERNAME", "MyBot");
153
+
154
+ const handle = telegramInviteAdapter.resolveChannelHandle!();
155
+ expect(handle).toBe("@MyBot");
156
+ });
157
+
158
+ test("returns undefined when bot username is not configured", () => {
159
+ setEnv("TELEGRAM_BOT_USERNAME", undefined);
160
+
161
+ const handle = telegramInviteAdapter.resolveChannelHandle!();
162
+ expect(handle).toBeUndefined();
163
+ });
164
+ });
@@ -63,7 +63,7 @@ mock.module("../config/loader.js", () => ({
63
63
 
64
64
  provider: "mock-provider",
65
65
  timeouts: { permissionTimeoutSec: 5, toolExecutionTimeoutSec: 120 },
66
- permissions: { mode: "legacy" },
66
+ permissions: { mode: "workspace" },
67
67
  skills: { load: { extraDirs: [] } },
68
68
  secretDetection: { enabled: true, entropyThreshold: 4.0, action: "warn" },
69
69
  sandbox: { enabled: false },
@@ -6,7 +6,10 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
6
6
 
7
7
  const testDir = mkdtempSync(join(tmpdir(), "permsim-handler-test-"));
8
8
 
9
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
10
+ const realPlatform = require("../util/platform.js");
9
11
  mock.module("../util/platform.js", () => ({
12
+ ...realPlatform,
10
13
  getRootDir: () => testDir,
11
14
  getDataDir: () => join(testDir, "data"),
12
15
  getWorkspaceSkillsDir: () => join(testDir, "skills"),
@@ -21,7 +24,10 @@ mock.module("../util/platform.js", () => ({
21
24
  getIpcBlobDir: () => join(testDir, "ipc-blobs"),
22
25
  }));
23
26
 
27
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
28
+ const realLogger = require("../util/logger.js");
24
29
  mock.module("../util/logger.js", () => ({
30
+ ...realLogger,
25
31
  getLogger: () => ({
26
32
  info: () => {},
27
33
  warn: () => {},
@@ -39,7 +45,7 @@ mock.module("../util/logger.js", () => ({
39
45
  }));
40
46
 
41
47
  const testConfig: Record<string, any> = {
42
- permissions: { mode: "legacy" as "legacy" | "strict" | "workspace" },
48
+ permissions: { mode: "workspace" as "strict" | "workspace" },
43
49
  skills: { load: { extraDirs: [] as string[] } },
44
50
  sandbox: { enabled: true },
45
51
  };
@@ -111,7 +117,7 @@ describe("tool_permission_simulate handler", () => {
111
117
  beforeEach(() => {
112
118
  clearAllRules();
113
119
  clearCache();
114
- testConfig.permissions.mode = "legacy";
120
+ testConfig.permissions.mode = "workspace";
115
121
  });
116
122
 
117
123
  test("validation: returns error when toolName is missing", async () => {