@vellumai/assistant 0.5.5 → 0.5.7

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 (382) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +4 -5
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +1 -2
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +3 -3
  26. package/src/__tests__/context-window-manager.test.ts +78 -0
  27. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  28. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  29. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  30. package/src/__tests__/conversation-title-service.test.ts +117 -1
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  33. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  34. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  35. package/src/__tests__/credentials-cli.test.ts +78 -0
  36. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  37. package/src/__tests__/docker-signing-key-bootstrap.test.ts +98 -0
  38. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  39. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  40. package/src/__tests__/host-shell-tool.test.ts +6 -7
  41. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  42. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  43. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  44. package/src/__tests__/intent-routing.test.ts +0 -13
  45. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  46. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  47. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  48. package/src/__tests__/memory-regressions.test.ts +8 -30
  49. package/src/__tests__/migration-export-http.test.ts +2 -2
  50. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  51. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  52. package/src/__tests__/migration-validate-http.test.ts +2 -2
  53. package/src/__tests__/non-member-access-request.test.ts +0 -5
  54. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  55. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  56. package/src/__tests__/permission-types.test.ts +1 -0
  57. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  58. package/src/__tests__/qdrant-manager.test.ts +28 -2
  59. package/src/__tests__/registry.test.ts +0 -6
  60. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  62. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  63. package/src/__tests__/secure-keys.test.ts +83 -263
  64. package/src/__tests__/shell-identity.test.ts +96 -6
  65. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  66. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  67. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  68. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  69. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  70. package/src/__tests__/skill-load-tool.test.ts +0 -2
  71. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  72. package/src/__tests__/skills.test.ts +0 -2
  73. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  74. package/src/__tests__/suggestion-routes.test.ts +1 -32
  75. package/src/__tests__/system-prompt.test.ts +0 -1
  76. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  77. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  78. package/src/__tests__/tool-executor.test.ts +4 -0
  79. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  80. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  81. package/src/__tests__/update-bulletin.test.ts +0 -2
  82. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  83. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  84. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  85. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  86. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  87. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  88. package/src/calls/audio-store.test.ts +97 -0
  89. package/src/calls/audio-store.ts +205 -0
  90. package/src/calls/call-controller.ts +85 -7
  91. package/src/calls/call-domain.ts +3 -0
  92. package/src/calls/call-store.ts +10 -3
  93. package/src/calls/fish-audio-client.ts +117 -0
  94. package/src/calls/relay-server.ts +27 -0
  95. package/src/calls/twilio-routes.ts +2 -1
  96. package/src/calls/types.ts +1 -0
  97. package/src/calls/voice-ingress-preflight.ts +0 -42
  98. package/src/calls/voice-quality.ts +26 -5
  99. package/src/calls/voice-session-bridge.ts +6 -12
  100. package/src/cli/commands/config.ts +1 -4
  101. package/src/cli/commands/conversations.ts +0 -18
  102. package/src/cli/commands/credentials.ts +34 -4
  103. package/src/cli/commands/oauth/index.ts +7 -0
  104. package/src/cli/commands/oauth/platform.ts +179 -0
  105. package/src/cli/commands/platform.ts +3 -3
  106. package/src/config/assistant-feature-flags.ts +186 -5
  107. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  108. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  109. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  110. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  111. package/src/config/bundled-tool-registry.ts +1 -11
  112. package/src/config/env-registry.ts +1 -1
  113. package/src/config/env.ts +16 -16
  114. package/src/config/feature-flag-registry.json +48 -16
  115. package/src/config/loader.ts +98 -31
  116. package/src/config/schema.ts +4 -25
  117. package/src/config/schemas/calls.ts +13 -0
  118. package/src/config/schemas/fish-audio.ts +39 -0
  119. package/src/config/schemas/memory.ts +0 -4
  120. package/src/config/schemas/platform.ts +1 -1
  121. package/src/config/schemas/security.ts +4 -4
  122. package/src/config/types.ts +0 -1
  123. package/src/contacts/contact-store.ts +39 -0
  124. package/src/contacts/types.ts +2 -0
  125. package/src/context/window-manager.ts +53 -2
  126. package/src/credential-execution/approval-bridge.ts +1 -0
  127. package/src/credential-execution/executable-discovery.ts +28 -4
  128. package/src/credential-execution/feature-gates.ts +16 -0
  129. package/src/credential-execution/process-manager.ts +38 -0
  130. package/src/daemon/assistant-attachments.ts +9 -0
  131. package/src/daemon/config-watcher.ts +6 -4
  132. package/src/daemon/conversation-agent-loop.ts +0 -60
  133. package/src/daemon/conversation-memory.ts +0 -117
  134. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  135. package/src/daemon/conversation-tool-setup.ts +0 -105
  136. package/src/daemon/conversation.ts +10 -1
  137. package/src/daemon/handlers/config-vercel.ts +92 -0
  138. package/src/daemon/handlers/conversations.ts +0 -11
  139. package/src/daemon/handlers/skills.ts +2 -15
  140. package/src/daemon/install-symlink.ts +195 -0
  141. package/src/daemon/lifecycle.ts +229 -96
  142. package/src/daemon/message-types/conversations.ts +3 -4
  143. package/src/daemon/message-types/diagnostics.ts +3 -22
  144. package/src/daemon/message-types/messages.ts +0 -2
  145. package/src/daemon/message-types/upgrades.ts +8 -0
  146. package/src/daemon/server.ts +30 -92
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/followups/followup-store.ts +5 -2
  149. package/src/inbound/platform-callback-registration.ts +3 -3
  150. package/src/instrument.ts +8 -5
  151. package/src/memory/conversation-crud.ts +0 -236
  152. package/src/memory/conversation-title-service.ts +76 -11
  153. package/src/memory/db-init.ts +15 -11
  154. package/src/memory/indexer.ts +15 -106
  155. package/src/memory/items-extractor.ts +15 -1
  156. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  157. package/src/memory/job-handlers/embedding.ts +0 -79
  158. package/src/memory/job-utils.ts +1 -1
  159. package/src/memory/jobs-store.ts +30 -13
  160. package/src/memory/jobs-worker.ts +31 -27
  161. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  162. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  163. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  164. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  165. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  166. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  167. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  168. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  169. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  170. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  171. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  172. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  173. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  174. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  175. package/src/memory/migrations/116-messages-fts.ts +106 -1
  176. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  177. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  178. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  179. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  180. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  181. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  182. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  183. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  184. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  185. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  186. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  187. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  188. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  189. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  190. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  191. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  192. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  193. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  194. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  195. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  196. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  197. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  198. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  199. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  200. package/src/memory/migrations/index.ts +5 -3
  201. package/src/memory/migrations/registry.ts +90 -0
  202. package/src/memory/migrations/validate-migration-state.ts +137 -11
  203. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  204. package/src/memory/qdrant-client.ts +4 -6
  205. package/src/memory/qdrant-manager.ts +64 -7
  206. package/src/memory/schema/calls.ts +1 -0
  207. package/src/memory/schema/contacts.ts +1 -0
  208. package/src/memory/schema/conversations.ts +0 -3
  209. package/src/memory/schema/index.ts +0 -2
  210. package/src/messaging/draft-store.ts +2 -2
  211. package/src/notifications/decision-engine.ts +4 -1
  212. package/src/oauth/connection-resolver.ts +6 -4
  213. package/src/permissions/checker.ts +0 -38
  214. package/src/permissions/defaults.ts +3 -3
  215. package/src/permissions/shell-identity.ts +76 -22
  216. package/src/permissions/trust-client.ts +2 -13
  217. package/src/permissions/trust-store.ts +8 -3
  218. package/src/permissions/types.ts +4 -2
  219. package/src/platform/client.ts +35 -7
  220. package/src/prompts/persona-resolver.ts +138 -0
  221. package/src/prompts/system-prompt.ts +36 -4
  222. package/src/prompts/templates/users/default.md +1 -0
  223. package/src/providers/registry.ts +27 -40
  224. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  225. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  226. package/src/runtime/auth/external-assistant-id.ts +13 -59
  227. package/src/runtime/auth/route-policy.ts +29 -1
  228. package/src/runtime/auth/token-service.ts +53 -15
  229. package/src/runtime/channel-readiness-service.ts +1 -16
  230. package/src/runtime/http-server.ts +29 -2
  231. package/src/runtime/middleware/error-handler.ts +1 -9
  232. package/src/runtime/routes/audio-routes.ts +40 -0
  233. package/src/runtime/routes/btw-routes.ts +0 -17
  234. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  235. package/src/runtime/routes/conversation-query-routes.ts +106 -2
  236. package/src/runtime/routes/conversation-routes.ts +4 -43
  237. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  238. package/src/runtime/routes/identity-routes.ts +18 -29
  239. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  240. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  241. package/src/runtime/routes/integrations/vercel.ts +89 -0
  242. package/src/runtime/routes/log-export-routes.ts +5 -0
  243. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  244. package/src/runtime/routes/memory-item-routes.ts +144 -4
  245. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  246. package/src/runtime/routes/migration-routes.ts +17 -1
  247. package/src/runtime/routes/notification-routes.ts +58 -0
  248. package/src/runtime/routes/schedule-routes.ts +65 -0
  249. package/src/runtime/routes/settings-routes.ts +41 -1
  250. package/src/runtime/routes/tts-routes.ts +86 -0
  251. package/src/runtime/routes/upgrade-broadcast-routes.ts +175 -0
  252. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  253. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  254. package/src/runtime/routes/workspace-routes.ts +1 -1
  255. package/src/runtime/routes/workspace-utils.ts +86 -2
  256. package/src/schedule/schedule-store.ts +0 -21
  257. package/src/security/ces-credential-client.ts +59 -22
  258. package/src/security/ces-rpc-credential-backend.ts +85 -0
  259. package/src/security/credential-backend.ts +12 -88
  260. package/src/security/keychain-broker-client.ts +10 -2
  261. package/src/security/secure-keys.ts +94 -113
  262. package/src/skills/catalog-install.ts +13 -7
  263. package/src/skills/inline-command-render.ts +5 -1
  264. package/src/skills/inline-command-runner.ts +30 -2
  265. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  266. package/src/tools/calls/call-start.ts +1 -0
  267. package/src/tools/executor.ts +0 -4
  268. package/src/tools/memory/handlers.ts +1 -129
  269. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  270. package/src/tools/network/web-fetch.ts +3 -1
  271. package/src/tools/permission-checker.ts +18 -0
  272. package/src/tools/skills/execute.ts +1 -1
  273. package/src/tools/skills/load.ts +9 -2
  274. package/src/tools/types.ts +0 -8
  275. package/src/util/errors.ts +0 -12
  276. package/src/util/platform.ts +8 -55
  277. package/src/util/xml.ts +8 -0
  278. package/src/workspace/git-service.ts +5 -2
  279. package/src/workspace/heartbeat-service.ts +5 -24
  280. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  281. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  282. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  283. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  284. package/src/workspace/migrations/006-services-config.ts +49 -0
  285. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  286. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  287. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  288. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  289. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  290. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  291. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  292. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  293. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  294. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  295. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  296. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  297. package/src/workspace/migrations/registry.ts +8 -0
  298. package/src/workspace/migrations/runner.ts +106 -2
  299. package/src/workspace/migrations/types.ts +4 -0
  300. package/src/__tests__/archive-recall.test.ts +0 -560
  301. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  302. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  303. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  304. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  305. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  306. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  307. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  308. package/src/__tests__/diagnostics-export.test.ts +0 -288
  309. package/src/__tests__/local-gateway-health.test.ts +0 -209
  310. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  311. package/src/__tests__/memory-brief-time.test.ts +0 -285
  312. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  313. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  314. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  315. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  316. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  317. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  318. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  319. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  320. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  321. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  322. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  323. package/src/__tests__/memory-reducer.test.ts +0 -704
  324. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  325. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  326. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  327. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  328. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  329. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  330. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  331. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  332. package/src/__tests__/swarm-recursion.test.ts +0 -197
  333. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  334. package/src/__tests__/swarm-tool.test.ts +0 -185
  335. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  336. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  337. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  338. package/src/commands/cc-command-registry.ts +0 -248
  339. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  340. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  341. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  342. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  343. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  344. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  345. package/src/config/schemas/memory-simplified.ts +0 -101
  346. package/src/config/schemas/swarm.ts +0 -82
  347. package/src/logfire.ts +0 -135
  348. package/src/memory/archive-recall.ts +0 -516
  349. package/src/memory/archive-store.ts +0 -400
  350. package/src/memory/brief-formatting.ts +0 -33
  351. package/src/memory/brief-open-loops.ts +0 -266
  352. package/src/memory/brief-time.ts +0 -162
  353. package/src/memory/brief.ts +0 -75
  354. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  355. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  356. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  357. package/src/memory/migrations/186-memory-archive.ts +0 -109
  358. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  359. package/src/memory/reducer-scheduler.ts +0 -242
  360. package/src/memory/reducer-store.ts +0 -271
  361. package/src/memory/reducer-types.ts +0 -106
  362. package/src/memory/reducer.ts +0 -467
  363. package/src/memory/schema/memory-archive.ts +0 -121
  364. package/src/memory/schema/memory-brief.ts +0 -55
  365. package/src/runtime/local-gateway-health.ts +0 -275
  366. package/src/security/secret-ingress.ts +0 -68
  367. package/src/swarm/backend-claude-code.ts +0 -225
  368. package/src/swarm/checkpoint.ts +0 -137
  369. package/src/swarm/graph-utils.ts +0 -53
  370. package/src/swarm/index.ts +0 -55
  371. package/src/swarm/limits.ts +0 -66
  372. package/src/swarm/orchestrator.ts +0 -424
  373. package/src/swarm/plan-validator.ts +0 -117
  374. package/src/swarm/router-planner.ts +0 -162
  375. package/src/swarm/router-prompts.ts +0 -39
  376. package/src/swarm/synthesizer.ts +0 -81
  377. package/src/swarm/types.ts +0 -72
  378. package/src/swarm/worker-backend.ts +0 -131
  379. package/src/swarm/worker-prompts.ts +0 -80
  380. package/src/swarm/worker-runner.ts +0 -170
  381. package/src/tools/claude-code/claude-code.ts +0 -610
  382. package/src/tools/swarm/delegate.ts +0 -205
@@ -3,7 +3,7 @@
3
3
  * tools, even when conversation history contains old markers for those skills.
4
4
  */
5
5
  import * as realFs from "node:fs";
6
- import { beforeEach, describe, expect, mock, test } from "bun:test";
6
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
7
7
 
8
8
  import type { SkillSummary, SkillToolManifest } from "../config/skills.js";
9
9
  import { RiskLevel } from "../permissions/types.js";
@@ -38,23 +38,6 @@ mock.module("../config/loader.js", () => ({
38
38
  invalidateConfigCache: () => {},
39
39
  }));
40
40
 
41
- mock.module("../config/assistant-feature-flags.js", () => ({
42
- isAssistantFeatureFlagEnabled: (
43
- key: string,
44
- config: Record<string, unknown>,
45
- ) => {
46
- const vals = (
47
- config as {
48
- assistantFeatureFlagValues?: Record<string, boolean>;
49
- }
50
- ).assistantFeatureFlagValues;
51
- if (vals && typeof vals[key] === "boolean") return vals[key];
52
- return true; // default enabled
53
- },
54
- loadDefaultsRegistry: () => ({}),
55
- getAssistantFeatureFlagDefaults: () => ({}),
56
- }));
57
-
58
41
  mock.module("../config/skill-state.js", () => ({
59
42
  skillFlagKey: (skill: { featureFlag?: string }) =>
60
43
  skill.featureFlag
@@ -62,6 +45,24 @@ mock.module("../config/skill-state.js", () => ({
62
45
  : undefined,
63
46
  }));
64
47
 
48
+ // Mock assistant-feature-flags to avoid loading the real module (which
49
+ // triggers file I/O and env-registry imports that hang in test context).
50
+ let _mockOverrides: Record<string, boolean> = {};
51
+ mock.module("../config/assistant-feature-flags.js", () => ({
52
+ isAssistantFeatureFlagEnabled: (key: string, _config: unknown): boolean => {
53
+ const explicit = _mockOverrides[key];
54
+ if (typeof explicit === "boolean") return explicit;
55
+ return true; // undeclared flags default to enabled
56
+ },
57
+ clearFeatureFlagOverridesCache: () => {
58
+ _mockOverrides = {};
59
+ },
60
+ _setOverridesForTesting: (overrides: Record<string, boolean>) => {
61
+ _mockOverrides = { ...overrides };
62
+ },
63
+ getAssistantFeatureFlagDefaults: () => ({}),
64
+ }));
65
+
65
66
  mock.module("../skills/active-skill-tools.js", () => {
66
67
  const parseMarkers = (messages: Message[]) => {
67
68
  const skillLoadUseIds = new Set<string>();
@@ -216,6 +217,10 @@ mock.module("../util/logger.js", () => ({
216
217
 
217
218
  const { projectSkillTools, resetSkillToolProjection } =
218
219
  await import("../daemon/conversation-skill-tools.js");
220
+ const { _setOverridesForTesting } =
221
+ (await import("../config/assistant-feature-flags.js")) as {
222
+ _setOverridesForTesting: (o: Record<string, boolean>) => void;
223
+ };
219
224
 
220
225
  // ---------------------------------------------------------------------------
221
226
  // Helpers
@@ -289,9 +294,14 @@ describe("projectSkillTools feature flag enforcement", () => {
289
294
  mockUnregisteredSkillIds = [];
290
295
  mockSkillRefCount = new Map();
291
296
  currentConfig = {};
297
+ _setOverridesForTesting({});
292
298
  resetSkillToolProjection();
293
299
  });
294
300
 
301
+ afterEach(() => {
302
+ _setOverridesForTesting({});
303
+ });
304
+
295
305
  test("no skill tools projected for flag OFF skill even with old markers", () => {
296
306
  mockCatalog = [makeSkill(DECLARED_SKILL_ID, DECLARED_SKILL_ID)];
297
307
  mockManifests = {
@@ -302,10 +312,8 @@ describe("projectSkillTools feature flag enforcement", () => {
302
312
  const history = buildHistoryWithMarker(DECLARED_SKILL_ID);
303
313
  const prevActive = new Map<string, string>();
304
314
 
305
- // Feature flag is OFF
306
- currentConfig = {
307
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
308
- };
315
+ // Feature flag is OFF — use protected directory override
316
+ _setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
309
317
 
310
318
  const result = projectSkillTools(history, {
311
319
  previouslyActiveSkillIds: prevActive,
@@ -325,10 +333,8 @@ describe("projectSkillTools feature flag enforcement", () => {
325
333
  const history = buildHistoryWithMarker(DECLARED_SKILL_ID);
326
334
  const prevActive = new Map<string, string>();
327
335
 
328
- // Feature flag is ON
329
- currentConfig = {
330
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: true },
331
- };
336
+ // Feature flag is ON — use protected directory override
337
+ _setOverridesForTesting({ [DECLARED_FLAG_KEY]: true });
332
338
 
333
339
  const result = projectSkillTools(history, {
334
340
  previouslyActiveSkillIds: prevActive,
@@ -419,9 +425,7 @@ describe("projectSkillTools feature flag enforcement", () => {
419
425
  const prevActive = new Map<string, string>();
420
426
 
421
427
  // Declared skill is OFF, plain-skill is undeclared with no persisted override so remains ON.
422
- currentConfig = {
423
- assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
424
- };
428
+ _setOverridesForTesting({ [DECLARED_FLAG_KEY]: false });
425
429
 
426
430
  const result = projectSkillTools(history, {
427
431
  previouslyActiveSkillIds: prevActive,
@@ -40,8 +40,6 @@ mock.module("../util/platform.js", () => ({
40
40
  getWorkspacePromptPath: (file: string) => join(TEST_DIR, file),
41
41
  readSessionToken: () => null,
42
42
  normalizeAssistantId: (id: string) => id,
43
- readLockfile: () => null,
44
- writeLockfile: () => {},
45
43
  }));
46
44
 
47
45
  const noopLogger = {
@@ -38,10 +38,6 @@ mock.module("../util/logger.js", () => ({
38
38
  }),
39
39
  }));
40
40
 
41
- mock.module("../security/secret-ingress.js", () => ({
42
- checkIngressForSecrets: () => ({ blocked: false }),
43
- }));
44
-
45
41
  mock.module("../config/env.js", () => ({
46
42
  isHttpAuthDisabled: () => true,
47
43
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -2,8 +2,7 @@
2
2
  * Unit tests for the GET /v1/suggestion endpoint (handleGetSuggestion).
3
3
  *
4
4
  * Validates happy path, all null-return paths, caching, staleness check,
5
- * quote stripping, word-boundary truncation, empty response rejection,
6
- * and modelIntent verification.
5
+ * quote stripping, empty response rejection, and modelIntent verification.
7
6
  */
8
7
 
9
8
  import { describe, expect, mock, test } from "bun:test";
@@ -293,36 +292,6 @@ describe("GET /v1/suggestion", () => {
293
292
  expect(body.suggestion).toBe("Sure, let's go!");
294
293
  });
295
294
 
296
- test("truncates long suggestions at word boundary", async () => {
297
- // A 60-char string that will exceed the 50-char limit
298
- const longText =
299
- "This is a really long suggestion that goes well beyond fifty chars";
300
- const provider = makeMockProvider(longText);
301
- mockGetConfiguredProvider.mockImplementation(async () => provider);
302
- mockGetConversationByKey.mockImplementation(() => ({
303
- conversationId: "conv-test",
304
- }));
305
- mockGetMessages.mockImplementation(() => [
306
- {
307
- id: "msg-asst-1",
308
- conversationId: "conv-test",
309
- role: "assistant",
310
- content: JSON.stringify([{ type: "text", text: "Hello there" }]),
311
- createdAt: Date.now(),
312
- metadata: null,
313
- },
314
- ]);
315
-
316
- const url = makeUrl({ conversationKey: "test-key" });
317
- const deps = makeDeps();
318
- const res = await handleGetSuggestion(url, deps);
319
- const body = (await res.json()) as { suggestion: string };
320
-
321
- expect(body.suggestion.length).toBeLessThanOrEqual(50);
322
- // Should end at a word boundary (no partial words)
323
- expect(body.suggestion).not.toMatch(/\s$/);
324
- });
325
-
326
295
  test("rejects empty LLM response", async () => {
327
296
  const provider = makeMockProvider("");
328
297
  mockGetConfiguredProvider.mockImplementation(async () => provider);
@@ -86,7 +86,6 @@ mock.module("../config/loader.js", () => ({
86
86
  invalidateConfigCache: () => {},
87
87
  getNestedValue: () => undefined,
88
88
  setNestedValue: () => {},
89
- syncConfigToLockfile: () => {},
90
89
  }));
91
90
 
92
91
  // eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -32,6 +32,10 @@ const mockConfig = {
32
32
  action: "warn" as const,
33
33
  entropyThreshold: 4.0,
34
34
  },
35
+ permissions: {
36
+ mode: "workspace" as const,
37
+ dangerouslySkipPermissions: false,
38
+ },
35
39
  };
36
40
 
37
41
  let checkerDecision: "allow" | "prompt" | "deny" = "allow";
@@ -330,7 +330,7 @@ describe("ToolExecutor → real shell allowlist integration", () => {
330
330
  expect(patterns).toContain("action:git");
331
331
  });
332
332
 
333
- test("pipeline command produces only exact option", async () => {
333
+ test("pipeline command produces exact + action-key options", async () => {
334
334
  const { prompter, getAllowlist } = makeCapturingPrompter();
335
335
  const executor = new ToolExecutor(prompter);
336
336
 
@@ -343,9 +343,11 @@ describe("ToolExecutor → real shell allowlist integration", () => {
343
343
  const allowlist = getAllowlist();
344
344
  expect(allowlist).toBeDefined();
345
345
 
346
- // Pipelines are complex commands — only exact option, no action keys
347
- expect(allowlist!.length).toBe(1);
346
+ // Pipelines now produce exact option + action key options
347
+ expect(allowlist!.length).toBeGreaterThanOrEqual(2);
348
348
  expect(allowlist![0].pattern).toBe("cat file.txt | grep error");
349
349
  expect(allowlist![0].description).toContain("compound");
350
+ // Action keys from the first segment before the pipe
351
+ expect(allowlist!.some((o) => o.pattern.startsWith("action:"))).toBe(true);
350
352
  });
351
353
  });
@@ -50,6 +50,10 @@ const mockConfig = {
50
50
  action: "warn" as const,
51
51
  entropyThreshold: 4.0,
52
52
  },
53
+ permissions: {
54
+ mode: "workspace" as const,
55
+ dangerouslySkipPermissions: false,
56
+ },
53
57
  };
54
58
 
55
59
  let fakeToolResult: ToolExecutionResult = { content: "ok", isError: false };
@@ -41,11 +41,6 @@ mock.module("../util/logger.js", () => ({
41
41
  }),
42
42
  }));
43
43
 
44
- // Mock security check to always pass
45
- mock.module("../security/secret-ingress.js", () => ({
46
- checkIngressForSecrets: () => ({ blocked: false }),
47
- }));
48
-
49
44
  mock.module("../config/env.js", () => ({
50
45
  isHttpAuthDisabled: () => true,
51
46
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -36,10 +36,6 @@ mock.module("../util/logger.js", () => ({
36
36
  }),
37
37
  }));
38
38
 
39
- mock.module("../security/secret-ingress.js", () => ({
40
- checkIngressForSecrets: () => ({ blocked: false }),
41
- }));
42
-
43
39
  mock.module("../config/env.js", () => ({
44
40
  isHttpAuthDisabled: () => true,
45
41
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -66,8 +66,6 @@ mock.module("../util/platform.js", () => ({
66
66
  getSandboxWorkingDir: () => "",
67
67
  getInterfacesDir: () => "",
68
68
  getClipboardCommand: () => null,
69
- readLockfile: () => null,
70
- writeLockfile: () => {},
71
69
  readPlatformToken: () => null,
72
70
  readSessionToken: () => null,
73
71
  getTCPPort: () => 8765,
@@ -61,9 +61,7 @@ const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
61
61
  getSessionTokenPath: () => join(TEST_DIR, "session-token"),
62
62
  readSessionToken: () => null,
63
63
  getClipboardCommand: () => null,
64
- readLockfile: () => null,
65
64
  normalizeAssistantId: (id: unknown) => String(id),
66
- writeLockfile: () => {},
67
65
  getEmbeddingModelsDir: () => join(TEST_DIR, "embedding-models"),
68
66
  getTCPPort: () => 8765,
69
67
  isTCPEnabled: () => false,
@@ -141,7 +139,6 @@ interface TestConfig {
141
139
  permissions: { mode: "strict" | "workspace" };
142
140
  skills: { load: { extraDirs: string[] } };
143
141
  sandbox: { enabled: boolean };
144
- assistantFeatureFlagValues?: Record<string, boolean>;
145
142
  [key: string]: unknown;
146
143
  }
147
144
 
@@ -149,9 +146,6 @@ const testConfig: TestConfig = {
149
146
  permissions: { mode: "workspace" },
150
147
  skills: { load: { extraDirs: [] } },
151
148
  sandbox: { enabled: true },
152
- assistantFeatureFlagValues: {
153
- "feature_flags.inline-skill-commands.enabled": true,
154
- },
155
149
  };
156
150
 
157
151
  mock.module("../config/loader.js", () => ({
@@ -169,6 +163,8 @@ mock.module("../config/loader.js", () => ({
169
163
 
170
164
  await import("../tools/skills/load.js");
171
165
  const { getTool } = await import("../tools/registry.js");
166
+ const { _setOverridesForTesting } =
167
+ await import("../config/assistant-feature-flags.js");
172
168
 
173
169
  // ── Helpers ──────────────────────────────────────────────────────────────
174
170
 
@@ -228,16 +224,17 @@ describe("vellum-self-knowledge inline command expansion", () => {
228
224
  ) => mockRunInlineCommand(command, workingDir),
229
225
  }));
230
226
 
231
- // Enable the feature flag
232
- testConfig.assistantFeatureFlagValues = {
227
+ // Enable the feature flag via protected directory override
228
+ _setOverridesForTesting({
233
229
  "feature_flags.inline-skill-commands.enabled": true,
234
- };
230
+ });
235
231
  testConfig.skills = { load: { extraDirs: [] } };
236
232
 
237
233
  installSelfKnowledgeSkill();
238
234
  });
239
235
 
240
236
  afterEach(() => {
237
+ _setOverridesForTesting({});
241
238
  if (existsSync(TEST_DIR)) {
242
239
  rmSync(TEST_DIR, { recursive: true, force: true });
243
240
  }
@@ -70,12 +70,6 @@ mock.module("../config/loader.js", () => ({
70
70
  }),
71
71
  }));
72
72
 
73
- // ── Secret ingress mock ────────────────────────────────────────────
74
-
75
- mock.module("../security/secret-ingress.js", () => ({
76
- checkIngressForSecrets: () => ({ blocked: false }),
77
- }));
78
-
79
73
  // ── Assistant event hub mock ───────────────────────────────────────
80
74
 
81
75
  mock.module("../runtime/assistant-event-hub.js", () => ({
@@ -0,0 +1,252 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mock state
5
+ // ---------------------------------------------------------------------------
6
+
7
+ const isAvailableFn = mock((): boolean => true);
8
+ const brokerSetFn = mock(
9
+ async (
10
+ _account: string,
11
+ _value: string,
12
+ ): Promise<{ status: string; code?: string; message?: string }> => ({
13
+ status: "ok",
14
+ }),
15
+ );
16
+ const createBrokerClientFn = mock(() => ({
17
+ isAvailable: isAvailableFn,
18
+ set: brokerSetFn,
19
+ }));
20
+
21
+ const listKeysFn = mock((): string[] => []);
22
+ const getKeyFn = mock((_account: string): string | undefined => undefined);
23
+ const deleteKeyFn = mock(
24
+ (_account: string): "deleted" | "not-found" | "error" => "deleted",
25
+ );
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Mock modules — before importing module under test
29
+ //
30
+ // The logger is mocked with a silent Proxy to suppress pino output in tests.
31
+ // The broker client and encrypted store are mocked to control migration
32
+ // behavior without touching real keychain or filesystem state.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ mock.module("../util/logger.js", () => ({
36
+ getLogger: () =>
37
+ new Proxy({} as Record<string, unknown>, {
38
+ get: () => () => {},
39
+ }),
40
+ }));
41
+
42
+ mock.module("../security/keychain-broker-client.js", () => ({
43
+ createBrokerClient: createBrokerClientFn,
44
+ }));
45
+
46
+ mock.module("../security/encrypted-store.js", () => ({
47
+ listKeys: listKeysFn,
48
+ getKey: getKeyFn,
49
+ deleteKey: deleteKeyFn,
50
+ }));
51
+
52
+ // Import after mocking
53
+ import { migrateCredentialsToKeychainMigration } from "../workspace/migrations/015-migrate-credentials-to-keychain.js";
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const WORKSPACE_DIR = "/mock-home/.vellum/workspace";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Tests
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe("015-migrate-credentials-to-keychain migration", () => {
66
+ beforeEach(() => {
67
+ isAvailableFn.mockClear();
68
+ brokerSetFn.mockClear();
69
+ createBrokerClientFn.mockClear();
70
+ listKeysFn.mockClear();
71
+ getKeyFn.mockClear();
72
+ deleteKeyFn.mockClear();
73
+
74
+ // Defaults: mac production build
75
+ process.env.VELLUM_DESKTOP_APP = "1";
76
+ delete process.env.VELLUM_DEV;
77
+
78
+ isAvailableFn.mockReturnValue(true);
79
+ brokerSetFn.mockResolvedValue({ status: "ok" });
80
+ listKeysFn.mockReturnValue([]);
81
+ getKeyFn.mockReturnValue(undefined);
82
+ deleteKeyFn.mockReturnValue("deleted");
83
+ });
84
+
85
+ test("has correct migration id", () => {
86
+ expect(migrateCredentialsToKeychainMigration.id).toBe(
87
+ "015-migrate-credentials-to-keychain",
88
+ );
89
+ });
90
+
91
+ test("skips when VELLUM_DESKTOP_APP is not set", async () => {
92
+ delete process.env.VELLUM_DESKTOP_APP;
93
+
94
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
95
+
96
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
97
+ expect(listKeysFn).not.toHaveBeenCalled();
98
+ });
99
+
100
+ test("skips when VELLUM_DESKTOP_APP is not '1'", async () => {
101
+ process.env.VELLUM_DESKTOP_APP = "0";
102
+
103
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
104
+
105
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
106
+ });
107
+
108
+ test("skips when VELLUM_DEV=1", async () => {
109
+ process.env.VELLUM_DEV = "1";
110
+
111
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
112
+
113
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
114
+ expect(listKeysFn).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test(
118
+ "throws when broker is not available after max retry attempts",
119
+ async () => {
120
+ isAvailableFn.mockReturnValue(false);
121
+
122
+ await expect(
123
+ migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR),
124
+ ).rejects.toThrow(
125
+ "Keychain broker not available after waiting — credential migration will be retried on next startup",
126
+ );
127
+
128
+ // Should have retried isAvailable multiple times
129
+ expect(isAvailableFn.mock.calls.length).toBeGreaterThan(1);
130
+
131
+ // Should not proceed to list or migrate keys
132
+ expect(listKeysFn).not.toHaveBeenCalled();
133
+ expect(brokerSetFn).not.toHaveBeenCalled();
134
+ },
135
+ { timeout: 10_000 },
136
+ );
137
+
138
+ test("succeeds when broker becomes available after retry", async () => {
139
+ // Broker unavailable for first 3 calls, then available
140
+ let callCount = 0;
141
+ isAvailableFn.mockImplementation(() => {
142
+ callCount++;
143
+ return callCount > 3;
144
+ });
145
+ listKeysFn.mockReturnValue(["retry-key"]);
146
+ getKeyFn.mockReturnValue("retry-secret");
147
+ brokerSetFn.mockResolvedValue({ status: "ok" });
148
+
149
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
150
+
151
+ // Should have called isAvailable 4 times (3 false + 1 true)
152
+ expect(isAvailableFn).toHaveBeenCalledTimes(4);
153
+
154
+ // Should have proceeded with migration
155
+ expect(brokerSetFn).toHaveBeenCalledWith("retry-key", "retry-secret");
156
+ expect(deleteKeyFn).toHaveBeenCalledWith("retry-key");
157
+ });
158
+
159
+ test("no-ops when encrypted store has no keys", async () => {
160
+ listKeysFn.mockReturnValue([]);
161
+
162
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
163
+
164
+ expect(brokerSetFn).not.toHaveBeenCalled();
165
+ expect(deleteKeyFn).not.toHaveBeenCalled();
166
+ });
167
+
168
+ test("successfully migrates keys from encrypted store to keychain", async () => {
169
+ listKeysFn.mockReturnValue(["account-a", "account-b"]);
170
+ getKeyFn.mockImplementation((account: string) => {
171
+ if (account === "account-a") return "secret-a";
172
+ if (account === "account-b") return "secret-b";
173
+ return undefined;
174
+ });
175
+ brokerSetFn.mockResolvedValue({ status: "ok" });
176
+
177
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
178
+
179
+ // Should have called broker.set for each key
180
+ expect(brokerSetFn).toHaveBeenCalledTimes(2);
181
+ expect(brokerSetFn).toHaveBeenCalledWith("account-a", "secret-a");
182
+ expect(brokerSetFn).toHaveBeenCalledWith("account-b", "secret-b");
183
+
184
+ // Should have deleted each key from encrypted store after successful migration
185
+ expect(deleteKeyFn).toHaveBeenCalledTimes(2);
186
+ expect(deleteKeyFn).toHaveBeenCalledWith("account-a");
187
+ expect(deleteKeyFn).toHaveBeenCalledWith("account-b");
188
+ });
189
+
190
+ test("continues on individual key failure and migrates others", async () => {
191
+ listKeysFn.mockReturnValue(["fail-key", "ok-key"]);
192
+ getKeyFn.mockImplementation((account: string) => {
193
+ if (account === "fail-key") return "fail-secret";
194
+ if (account === "ok-key") return "ok-secret";
195
+ return undefined;
196
+ });
197
+ brokerSetFn.mockImplementation(async (account: string) => {
198
+ if (account === "fail-key") {
199
+ return {
200
+ status: "rejected" as const,
201
+ code: "UNKNOWN",
202
+ message: "broker rejected",
203
+ };
204
+ }
205
+ return { status: "ok" as const };
206
+ });
207
+
208
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
209
+
210
+ // fail-key should NOT have been deleted (broker rejected it)
211
+ expect(deleteKeyFn).not.toHaveBeenCalledWith("fail-key");
212
+
213
+ // ok-key should have been migrated and deleted
214
+ expect(brokerSetFn).toHaveBeenCalledWith("ok-key", "ok-secret");
215
+ expect(deleteKeyFn).toHaveBeenCalledWith("ok-key");
216
+ expect(deleteKeyFn).toHaveBeenCalledTimes(1);
217
+ });
218
+
219
+ test("handles getKey returning undefined for a listed key", async () => {
220
+ listKeysFn.mockReturnValue(["ghost-key", "real-key"]);
221
+ getKeyFn.mockImplementation((account: string) => {
222
+ if (account === "ghost-key") return undefined;
223
+ if (account === "real-key") return "real-secret";
224
+ return undefined;
225
+ });
226
+ brokerSetFn.mockResolvedValue({ status: "ok" });
227
+
228
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
229
+
230
+ // ghost-key should not be sent to broker or deleted
231
+ expect(brokerSetFn).not.toHaveBeenCalledWith(
232
+ "ghost-key",
233
+ expect.anything(),
234
+ );
235
+ expect(deleteKeyFn).not.toHaveBeenCalledWith("ghost-key");
236
+
237
+ // real-key should be migrated
238
+ expect(brokerSetFn).toHaveBeenCalledWith("real-key", "real-secret");
239
+ expect(deleteKeyFn).toHaveBeenCalledWith("real-key");
240
+ });
241
+
242
+ test("handles broker unreachable status for individual keys", async () => {
243
+ listKeysFn.mockReturnValue(["key-1"]);
244
+ getKeyFn.mockReturnValue("secret-1");
245
+ brokerSetFn.mockResolvedValue({ status: "unreachable" });
246
+
247
+ await migrateCredentialsToKeychainMigration.run(WORKSPACE_DIR);
248
+
249
+ // Should not delete when broker is unreachable
250
+ expect(deleteKeyFn).not.toHaveBeenCalled();
251
+ });
252
+ });