@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -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
+ });
@@ -0,0 +1,220 @@
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 brokerGetFn = mock(
9
+ async (
10
+ _account: string,
11
+ ): Promise<{ found: boolean; value?: string } | null> => ({
12
+ found: true,
13
+ value: "secret",
14
+ }),
15
+ );
16
+ const brokerDelFn = mock(async (_account: string): Promise<boolean> => true);
17
+ const brokerListFn = mock(async (): Promise<string[]> => []);
18
+ const createBrokerClientFn = mock(() => ({
19
+ isAvailable: isAvailableFn,
20
+ get: brokerGetFn,
21
+ del: brokerDelFn,
22
+ list: brokerListFn,
23
+ }));
24
+
25
+ const setKeyFn = mock(
26
+ (_account: string, _value: string): boolean => true,
27
+ );
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Mock modules — before importing module under test
31
+ //
32
+ // The logger is mocked with a silent Proxy to suppress pino output in tests.
33
+ // The broker client and encrypted store are mocked to control migration
34
+ // behavior without touching real keychain or filesystem state.
35
+ // ---------------------------------------------------------------------------
36
+
37
+ mock.module("../util/logger.js", () => ({
38
+ getLogger: () =>
39
+ new Proxy({} as Record<string, unknown>, {
40
+ get: () => () => {},
41
+ }),
42
+ }));
43
+
44
+ mock.module("../security/keychain-broker-client.js", () => ({
45
+ createBrokerClient: createBrokerClientFn,
46
+ }));
47
+
48
+ mock.module("../security/encrypted-store.js", () => ({
49
+ setKey: setKeyFn,
50
+ }));
51
+
52
+ // Import after mocking
53
+ import { migrateCredentialsFromKeychainMigration } from "../workspace/migrations/016-migrate-credentials-from-keychain.js";
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Helpers
57
+ // ---------------------------------------------------------------------------
58
+
59
+ const WORKSPACE_DIR = "/mock-home/.vellum/workspace";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Tests
63
+ // ---------------------------------------------------------------------------
64
+
65
+ describe("016-migrate-credentials-from-keychain migration", () => {
66
+ beforeEach(() => {
67
+ isAvailableFn.mockClear();
68
+ brokerGetFn.mockClear();
69
+ brokerDelFn.mockClear();
70
+ brokerListFn.mockClear();
71
+ createBrokerClientFn.mockClear();
72
+ setKeyFn.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
+ brokerGetFn.mockResolvedValue({ found: true, value: "secret" });
80
+ brokerDelFn.mockResolvedValue(true);
81
+ brokerListFn.mockResolvedValue([]);
82
+ setKeyFn.mockReturnValue(true);
83
+ });
84
+
85
+ test("has correct migration id", () => {
86
+ expect(migrateCredentialsFromKeychainMigration.id).toBe(
87
+ "016-migrate-credentials-from-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 migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
95
+
96
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
97
+ expect(brokerListFn).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 migrateCredentialsFromKeychainMigration.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 migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
112
+
113
+ expect(createBrokerClientFn).not.toHaveBeenCalled();
114
+ expect(brokerListFn).not.toHaveBeenCalled();
115
+ });
116
+
117
+ test(
118
+ "throws when broker is not available (skips checkpoint for retry)",
119
+ async () => {
120
+ isAvailableFn.mockReturnValue(false);
121
+
122
+ // Throwing skips the checkpoint so the migration retries on next startup
123
+ await expect(
124
+ migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR),
125
+ ).rejects.toThrow("Keychain broker not available after waiting");
126
+
127
+ // Should not proceed to list or migrate keys
128
+ expect(brokerListFn).not.toHaveBeenCalled();
129
+ expect(setKeyFn).not.toHaveBeenCalled();
130
+ },
131
+ { timeout: 10_000 },
132
+ );
133
+
134
+ test("no-ops when keychain has no accounts", async () => {
135
+ brokerListFn.mockResolvedValue([]);
136
+
137
+ await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
138
+
139
+ expect(setKeyFn).not.toHaveBeenCalled();
140
+ expect(brokerDelFn).not.toHaveBeenCalled();
141
+ });
142
+
143
+ test("copies credentials from keychain to encrypted store and deletes from keychain", async () => {
144
+ brokerListFn.mockResolvedValue(["account-a", "account-b"]);
145
+ brokerGetFn.mockImplementation(async (account: string) => {
146
+ if (account === "account-a") return { found: true, value: "secret-a" };
147
+ if (account === "account-b") return { found: true, value: "secret-b" };
148
+ return null;
149
+ });
150
+ setKeyFn.mockReturnValue(true);
151
+
152
+ await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
153
+
154
+ // Should have written each key to encrypted store
155
+ expect(setKeyFn).toHaveBeenCalledTimes(2);
156
+ expect(setKeyFn).toHaveBeenCalledWith("account-a", "secret-a");
157
+ expect(setKeyFn).toHaveBeenCalledWith("account-b", "secret-b");
158
+
159
+ // Should have deleted each key from keychain after successful migration
160
+ expect(brokerDelFn).toHaveBeenCalledTimes(2);
161
+ expect(brokerDelFn).toHaveBeenCalledWith("account-a");
162
+ expect(brokerDelFn).toHaveBeenCalledWith("account-b");
163
+ });
164
+
165
+ test("skips key when broker.get returns null", async () => {
166
+ brokerListFn.mockResolvedValue(["ghost-key", "real-key"]);
167
+ brokerGetFn.mockImplementation(async (account: string) => {
168
+ if (account === "ghost-key") return null;
169
+ if (account === "real-key") return { found: true, value: "real-secret" };
170
+ return null;
171
+ });
172
+
173
+ await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
174
+
175
+ // ghost-key should not be written or deleted
176
+ expect(setKeyFn).not.toHaveBeenCalledWith(
177
+ "ghost-key",
178
+ expect.anything(),
179
+ );
180
+ expect(brokerDelFn).not.toHaveBeenCalledWith("ghost-key");
181
+
182
+ // real-key should be migrated
183
+ expect(setKeyFn).toHaveBeenCalledWith("real-key", "real-secret");
184
+ expect(brokerDelFn).toHaveBeenCalledWith("real-key");
185
+ });
186
+
187
+ test("skips key when broker.get returns not found", async () => {
188
+ brokerListFn.mockResolvedValue(["missing-key"]);
189
+ brokerGetFn.mockResolvedValue({ found: false });
190
+
191
+ await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
192
+
193
+ expect(setKeyFn).not.toHaveBeenCalled();
194
+ expect(brokerDelFn).not.toHaveBeenCalled();
195
+ });
196
+
197
+ test("skips key when setKey fails and does not delete from keychain", async () => {
198
+ brokerListFn.mockResolvedValue(["fail-key", "ok-key"]);
199
+ brokerGetFn.mockImplementation(async (account: string) => {
200
+ if (account === "fail-key")
201
+ return { found: true, value: "fail-secret" };
202
+ if (account === "ok-key") return { found: true, value: "ok-secret" };
203
+ return null;
204
+ });
205
+ setKeyFn.mockImplementation((account: string) => {
206
+ if (account === "fail-key") return false;
207
+ return true;
208
+ });
209
+
210
+ await migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR);
211
+
212
+ // fail-key should NOT have been deleted from keychain (setKey failed)
213
+ expect(brokerDelFn).not.toHaveBeenCalledWith("fail-key");
214
+
215
+ // ok-key should have been migrated and deleted
216
+ expect(setKeyFn).toHaveBeenCalledWith("ok-key", "ok-secret");
217
+ expect(brokerDelFn).toHaveBeenCalledWith("ok-key");
218
+ expect(brokerDelFn).toHaveBeenCalledTimes(1);
219
+ });
220
+ });