@vellumai/assistant 0.6.2 → 0.6.4

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 (895) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +41 -49
  4. package/bunfig.toml +3 -0
  5. package/docs/architecture/memory.md +1 -1
  6. package/docs/backup-troubleshooting.md +52 -0
  7. package/docs/browser-use-architecture-phase2.md +174 -0
  8. package/docs/stt-provider-onboarding.md +120 -0
  9. package/knip.json +12 -2
  10. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  11. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  12. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  13. package/openapi.yaml +1111 -86
  14. package/package.json +40 -42
  15. package/scripts/generate-openapi.ts +0 -2
  16. package/scripts/test.sh +73 -18
  17. package/src/__tests__/acp-session.test.ts +43 -0
  18. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  19. package/src/__tests__/agent-loop.test.ts +123 -0
  20. package/src/__tests__/anthropic-provider.test.ts +263 -10
  21. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  22. package/src/__tests__/app-executors.test.ts +1 -0
  23. package/src/__tests__/app-source-watcher.test.ts +37 -11
  24. package/src/__tests__/approval-routes-http.test.ts +178 -1
  25. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  26. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +240 -94
  28. package/src/__tests__/browser-manager.test.ts +40 -27
  29. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  30. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  31. package/src/__tests__/btw-routes.test.ts +7 -0
  32. package/src/__tests__/call-controller.test.ts +581 -20
  33. package/src/__tests__/catalog-files.test.ts +1000 -0
  34. package/src/__tests__/channel-approvals.test.ts +53 -0
  35. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  36. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  37. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  38. package/src/__tests__/checker.test.ts +157 -10
  39. package/src/__tests__/clawhub-files.test.ts +347 -0
  40. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  41. package/src/__tests__/config-analysis.test.ts +100 -0
  42. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  43. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  44. package/src/__tests__/config-schema.test.ts +1248 -224
  45. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  46. package/src/__tests__/config-watcher.test.ts +43 -8
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  48. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  49. package/src/__tests__/contacts-write.test.ts +197 -0
  50. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  51. package/src/__tests__/context-window-manager.test.ts +88 -0
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  54. package/src/__tests__/conversation-agent-loop.test.ts +99 -3
  55. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  56. package/src/__tests__/conversation-attachments.test.ts +80 -4
  57. package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
  58. package/src/__tests__/conversation-error.test.ts +70 -0
  59. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  60. package/src/__tests__/conversation-history-web-search.test.ts +12 -4
  61. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  62. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  63. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  64. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  65. package/src/__tests__/conversation-list-source.test.ts +145 -0
  66. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  67. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  68. package/src/__tests__/conversation-queue.test.ts +946 -62
  69. package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
  73. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  74. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  76. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  77. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  78. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  79. package/src/__tests__/conversation-store.test.ts +195 -0
  80. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  81. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  82. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  83. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  84. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  85. package/src/__tests__/credential-health-service.test.ts +352 -0
  86. package/src/__tests__/credential-security-invariants.test.ts +6 -3
  87. package/src/__tests__/credential-vault-unit.test.ts +383 -7
  88. package/src/__tests__/credential-vault.test.ts +152 -13
  89. package/src/__tests__/credentials-cli.test.ts +42 -18
  90. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  91. package/src/__tests__/date-context.test.ts +4 -4
  92. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  93. package/src/__tests__/device-id.test.ts +112 -0
  94. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  95. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  96. package/src/__tests__/email-html-renderer.test.ts +71 -0
  97. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  98. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  99. package/src/__tests__/emit-event-signal.test.ts +71 -0
  100. package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
  101. package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
  102. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -0
  104. package/src/__tests__/gemini-provider.test.ts +66 -2
  105. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  106. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  107. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  108. package/src/__tests__/gmail-preferences.test.ts +117 -0
  109. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  110. package/src/__tests__/headless-browser-interactions.test.ts +738 -359
  111. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  112. package/src/__tests__/headless-browser-navigate.test.ts +528 -49
  113. package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
  114. package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
  115. package/src/__tests__/heartbeat-service.test.ts +70 -17
  116. package/src/__tests__/home-state-routes.test.ts +162 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +145 -1
  118. package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
  119. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  120. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  121. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  122. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  123. package/src/__tests__/host-browser-routes.test.ts +198 -0
  124. package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
  125. package/src/__tests__/host-cu-proxy.test.ts +166 -1
  126. package/src/__tests__/host-file-proxy.test.ts +185 -1
  127. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  128. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  129. package/src/__tests__/host-shell-tool.test.ts +1 -11
  130. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  131. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  132. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  133. package/src/__tests__/integration-status.test.ts +6 -7
  134. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  135. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  136. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  137. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  138. package/src/__tests__/llm-usage-store.test.ts +363 -0
  139. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  140. package/src/__tests__/mcp-health-check.test.ts +10 -3
  141. package/src/__tests__/media-stream-output.test.ts +555 -0
  142. package/src/__tests__/media-stream-parser.test.ts +374 -0
  143. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  144. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  145. package/src/__tests__/media-turn-detector.test.ts +440 -0
  146. package/src/__tests__/message-queue.test.ts +125 -0
  147. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  148. package/src/__tests__/migration-export-http.test.ts +67 -8
  149. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  150. package/src/__tests__/migration-import-commit-http.test.ts +109 -7
  151. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  152. package/src/__tests__/migration-validate-http.test.ts +3 -3
  153. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  154. package/src/__tests__/model-intents.test.ts +2 -2
  155. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  156. package/src/__tests__/oauth-apps-routes.test.ts +18 -12
  157. package/src/__tests__/oauth-cli.test.ts +709 -60
  158. package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  160. package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
  161. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  162. package/src/__tests__/oauth-providers-routes.test.ts +52 -14
  163. package/src/__tests__/oauth-store.test.ts +1465 -176
  164. package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
  165. package/src/__tests__/onboarding-template-contract.test.ts +81 -70
  166. package/src/__tests__/openai-provider.test.ts +178 -2
  167. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  168. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  169. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  170. package/src/__tests__/outlook-categories.test.ts +1 -1
  171. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  172. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  173. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  174. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  175. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  176. package/src/__tests__/outlook-trash.test.ts +1 -1
  177. package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
  178. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  179. package/src/__tests__/permission-mode.test.ts +28 -56
  180. package/src/__tests__/persona-resolver.test.ts +251 -0
  181. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  182. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  183. package/src/__tests__/platform.test.ts +92 -1
  184. package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
  185. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  186. package/src/__tests__/pricing.test.ts +174 -0
  187. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  188. package/src/__tests__/qdrant-manager.test.ts +29 -8
  189. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  190. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  191. package/src/__tests__/relay-server.test.ts +423 -5
  192. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  193. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  194. package/src/__tests__/schedule-routes.test.ts +162 -0
  195. package/src/__tests__/search-skills-unified.test.ts +118 -0
  196. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  197. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  198. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  199. package/src/__tests__/secure-keys.test.ts +107 -0
  200. package/src/__tests__/send-endpoint-busy.test.ts +8 -1
  201. package/src/__tests__/sequence-store.test.ts +1 -1
  202. package/src/__tests__/server-history-render.test.ts +49 -0
  203. package/src/__tests__/set-permission-mode.test.ts +13 -250
  204. package/src/__tests__/settings-routes.test.ts +201 -0
  205. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
  208. package/src/__tests__/skills.test.ts +5 -2
  209. package/src/__tests__/skillssh-files.test.ts +446 -0
  210. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  211. package/src/__tests__/slack-channel-config.test.ts +576 -16
  212. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  213. package/src/__tests__/stt-stream-session.test.ts +535 -0
  214. package/src/__tests__/subagent-detail.test.ts +44 -2
  215. package/src/__tests__/subagent-disposal.test.ts +1 -0
  216. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  217. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  218. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  219. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  220. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  221. package/src/__tests__/subagent-tools.test.ts +1 -0
  222. package/src/__tests__/subagent-types.test.ts +1 -0
  223. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  224. package/src/__tests__/system-prompt.test.ts +184 -27
  225. package/src/__tests__/task-scheduler.test.ts +32 -6
  226. package/src/__tests__/telegram-config.test.ts +10 -13
  227. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  228. package/src/__tests__/terminal-tools.test.ts +25 -5
  229. package/src/__tests__/test-preload.ts +18 -0
  230. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  231. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  232. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  233. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  234. package/src/__tests__/tool-executor.test.ts +33 -24
  235. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  236. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  237. package/src/__tests__/top-level-renderer.test.ts +73 -1
  238. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  239. package/src/__tests__/trust-store.test.ts +7 -1
  240. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  241. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  242. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  243. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  244. package/src/__tests__/twilio-routes.test.ts +376 -0
  245. package/src/__tests__/unicode.test.ts +293 -0
  246. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  247. package/src/__tests__/update-bulletin.test.ts +206 -5
  248. package/src/__tests__/usage-routes.test.ts +25 -4
  249. package/src/__tests__/user-reference.test.ts +46 -61
  250. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  251. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  252. package/src/__tests__/voice-config-update.test.ts +403 -0
  253. package/src/__tests__/voice-quality.test.ts +434 -19
  254. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  255. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  256. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  257. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  258. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  259. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  260. package/src/__tests__/workspace-policy.test.ts +2 -0
  261. package/src/acp/client-handler.ts +30 -4
  262. package/src/agent/image-optimize.ts +24 -12
  263. package/src/agent/loop.ts +55 -9
  264. package/src/approvals/guardian-request-resolvers.ts +21 -15
  265. package/src/backup/__tests__/backup-key.test.ts +152 -0
  266. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  267. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  268. package/src/backup/__tests__/local-writer.test.ts +218 -0
  269. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  270. package/src/backup/__tests__/paths.test.ts +300 -0
  271. package/src/backup/__tests__/restore.test.ts +498 -0
  272. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  273. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  274. package/src/backup/backup-key.ts +137 -0
  275. package/src/backup/backup-worker.ts +459 -0
  276. package/src/backup/list-snapshots.ts +147 -0
  277. package/src/backup/local-writer.ts +133 -0
  278. package/src/backup/offsite-writer.ts +222 -0
  279. package/src/backup/paths.ts +226 -0
  280. package/src/backup/restore.ts +322 -0
  281. package/src/backup/snapshot-lock.ts +431 -0
  282. package/src/backup/stream-crypt.ts +263 -0
  283. package/src/browser-session/__tests__/manager.test.ts +297 -0
  284. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  285. package/src/browser-session/backends/extension.ts +26 -0
  286. package/src/browser-session/backends/local.ts +24 -0
  287. package/src/browser-session/events.ts +164 -0
  288. package/src/browser-session/index.ts +27 -0
  289. package/src/browser-session/manager.ts +159 -0
  290. package/src/browser-session/types.ts +28 -0
  291. package/src/bundler/package-resolver.ts +4 -0
  292. package/src/calls/audio-store.ts +11 -5
  293. package/src/calls/call-controller.ts +226 -71
  294. package/src/calls/call-domain.ts +9 -0
  295. package/src/calls/call-speech-output.ts +190 -0
  296. package/src/calls/call-transport.ts +77 -0
  297. package/src/calls/media-stream-audio-transcode.ts +173 -0
  298. package/src/calls/media-stream-output.ts +660 -0
  299. package/src/calls/media-stream-parser.ts +300 -0
  300. package/src/calls/media-stream-protocol.ts +166 -0
  301. package/src/calls/media-stream-server.ts +592 -0
  302. package/src/calls/media-stream-stt-session.ts +460 -0
  303. package/src/calls/media-turn-detector.ts +230 -0
  304. package/src/calls/relay-server.ts +90 -75
  305. package/src/calls/resolve-call-tts-provider.ts +136 -0
  306. package/src/calls/telephony-stt-routing.ts +145 -0
  307. package/src/calls/tts-call-strategy.ts +161 -0
  308. package/src/calls/tts-text-sanitizer.ts +32 -16
  309. package/src/calls/twilio-routes.ts +281 -17
  310. package/src/calls/voice-quality.ts +78 -35
  311. package/src/calls/voice-session-bridge.ts +8 -1
  312. package/src/channels/__tests__/types.test.ts +134 -0
  313. package/src/channels/types.ts +69 -3
  314. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  315. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  316. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  317. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  318. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  319. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  320. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  321. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  322. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  323. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  324. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  325. package/src/cli/commands/backup.ts +993 -0
  326. package/src/cli/commands/conversations.ts +77 -0
  327. package/src/cli/commands/credentials.ts +3 -4
  328. package/src/cli/commands/domain.ts +210 -0
  329. package/src/cli/commands/email.ts +273 -16
  330. package/src/cli/commands/mcp.ts +16 -4
  331. package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
  332. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  333. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  334. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  335. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
  336. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
  337. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
  338. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  339. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  340. package/src/cli/commands/oauth/apps.ts +7 -4
  341. package/src/cli/commands/oauth/connect.ts +6 -3
  342. package/src/cli/commands/oauth/disconnect.ts +1 -1
  343. package/src/cli/commands/oauth/mode.ts +12 -3
  344. package/src/cli/commands/oauth/providers.ts +215 -36
  345. package/src/cli/commands/oauth/shared.ts +7 -6
  346. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
  347. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  348. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  349. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  350. package/src/cli/commands/platform/index.ts +107 -10
  351. package/src/cli/commands/usage.ts +10 -9
  352. package/src/cli/lib/daemon-credential-client.ts +4 -0
  353. package/src/cli/program.ts +30 -4
  354. package/src/config/__tests__/backup-schema.test.ts +134 -0
  355. package/src/config/assistant-feature-flags.ts +61 -62
  356. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  357. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
  358. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  359. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  360. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  361. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  362. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  363. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  364. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  365. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  366. package/src/config/bundled-skills/contacts/SKILL.md +5 -2
  367. package/src/config/bundled-skills/document/SKILL.md +4 -0
  368. package/src/config/bundled-skills/gmail/SKILL.md +54 -8
  369. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  370. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  371. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  372. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  373. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  374. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  375. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  376. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  377. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  378. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  379. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  380. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  381. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  382. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  383. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  384. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  385. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  386. package/src/config/bundled-skills/outlook/SKILL.md +9 -2
  387. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  388. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  389. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  390. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  391. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  392. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  393. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  394. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  395. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  396. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  397. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  398. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  399. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  400. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  401. package/src/config/bundled-tool-registry.ts +8 -0
  402. package/src/config/env-registry.ts +38 -0
  403. package/src/config/env.ts +49 -4
  404. package/src/config/feature-flag-registry.json +85 -14
  405. package/src/config/loader.ts +82 -13
  406. package/src/config/sanitize-for-transfer.ts +47 -0
  407. package/src/config/schema.ts +81 -15
  408. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  409. package/src/config/schemas/analysis.ts +51 -0
  410. package/src/config/schemas/backup.ts +72 -0
  411. package/src/config/schemas/calls.ts +1 -26
  412. package/src/config/schemas/elevenlabs.ts +0 -59
  413. package/src/config/schemas/filing.ts +47 -7
  414. package/src/config/schemas/heartbeat.ts +27 -5
  415. package/src/config/schemas/host-browser.ts +112 -0
  416. package/src/config/schemas/inference.ts +1 -1
  417. package/src/config/schemas/memory-lifecycle.ts +14 -2
  418. package/src/config/schemas/memory-retrieval.ts +103 -0
  419. package/src/config/schemas/security.ts +0 -6
  420. package/src/config/schemas/services.ts +52 -0
  421. package/src/config/schemas/stt.ts +59 -0
  422. package/src/config/schemas/tts.ts +230 -0
  423. package/src/config/schemas/updates.ts +14 -0
  424. package/src/config/skills.ts +4 -0
  425. package/src/config/types.ts +4 -1
  426. package/src/contacts/contact-store.ts +56 -11
  427. package/src/contacts/contacts-write.ts +38 -1
  428. package/src/context/post-turn-tool-result-truncation.ts +177 -0
  429. package/src/context/tool-result-truncation.ts +2 -1
  430. package/src/context/window-manager.ts +61 -10
  431. package/src/credential-execution/approval-bridge.ts +49 -15
  432. package/src/credential-execution/executable-discovery.ts +12 -2
  433. package/src/credential-execution/process-manager.ts +33 -2
  434. package/src/credential-health/credential-health-service.ts +366 -0
  435. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  436. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  437. package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
  438. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  439. package/src/daemon/app-source-watcher.ts +35 -0
  440. package/src/daemon/config-watcher.ts +99 -5
  441. package/src/daemon/context-overflow-approval.ts +5 -0
  442. package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
  443. package/src/daemon/conversation-agent-loop.ts +153 -42
  444. package/src/daemon/conversation-attachments.ts +40 -0
  445. package/src/daemon/conversation-error.ts +11 -0
  446. package/src/daemon/conversation-history.ts +40 -6
  447. package/src/daemon/conversation-launch.ts +220 -0
  448. package/src/daemon/conversation-lifecycle.ts +59 -9
  449. package/src/daemon/conversation-messaging.ts +37 -3
  450. package/src/daemon/conversation-notifiers.ts +5 -0
  451. package/src/daemon/conversation-process.ts +622 -13
  452. package/src/daemon/conversation-queue-manager.ts +24 -0
  453. package/src/daemon/conversation-runtime-assembly.ts +128 -36
  454. package/src/daemon/conversation-slash.ts +36 -0
  455. package/src/daemon/conversation-surfaces.ts +131 -40
  456. package/src/daemon/conversation-tool-setup.ts +99 -8
  457. package/src/daemon/conversation-usage.ts +7 -4
  458. package/src/daemon/conversation-workspace.ts +12 -0
  459. package/src/daemon/conversation.ts +292 -16
  460. package/src/daemon/date-context.ts +10 -10
  461. package/src/daemon/first-greeting.ts +3 -2
  462. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  463. package/src/daemon/handlers/conversations.ts +13 -141
  464. package/src/daemon/handlers/shared.ts +80 -0
  465. package/src/daemon/handlers/skills.ts +483 -44
  466. package/src/daemon/host-bash-proxy.ts +48 -13
  467. package/src/daemon/host-browser-proxy.ts +192 -0
  468. package/src/daemon/host-cu-proxy.ts +36 -11
  469. package/src/daemon/host-file-proxy.ts +57 -9
  470. package/src/daemon/lifecycle.ts +179 -28
  471. package/src/daemon/message-protocol.ts +13 -0
  472. package/src/daemon/message-types/conversations.ts +89 -14
  473. package/src/daemon/message-types/home.ts +40 -0
  474. package/src/daemon/message-types/host-browser.ts +100 -0
  475. package/src/daemon/message-types/meet.ts +143 -0
  476. package/src/daemon/message-types/messages.ts +19 -5
  477. package/src/daemon/message-types/schedules.ts +34 -2
  478. package/src/daemon/message-types/skills.ts +26 -0
  479. package/src/daemon/message-types/subagents.ts +2 -0
  480. package/src/daemon/message-types/surfaces.ts +2 -0
  481. package/src/daemon/server.ts +439 -14
  482. package/src/daemon/shutdown-handlers.ts +32 -4
  483. package/src/daemon/shutdown-registry.ts +40 -0
  484. package/src/daemon/tool-side-effects.ts +15 -0
  485. package/src/daemon/transport-hints.ts +5 -24
  486. package/src/email/html-renderer.ts +76 -0
  487. package/src/heartbeat/heartbeat-service.ts +93 -7
  488. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  489. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  490. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  491. package/src/home/__tests__/feed-types.test.ts +275 -0
  492. package/src/home/__tests__/feed-writer.test.ts +688 -0
  493. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  494. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  495. package/src/home/__tests__/progress-formula.test.ts +213 -0
  496. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  497. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  498. package/src/home/assistant-feed-authoring.ts +124 -0
  499. package/src/home/emit-feed-event.ts +158 -0
  500. package/src/home/feed-scheduler.ts +247 -0
  501. package/src/home/feed-types.ts +181 -0
  502. package/src/home/feed-writer.ts +469 -0
  503. package/src/home/platform-gmail-digest.ts +163 -0
  504. package/src/home/progress-formula.ts +86 -0
  505. package/src/home/relationship-state-writer.ts +824 -0
  506. package/src/home/relationship-state.ts +143 -0
  507. package/src/home/rollup-producer.ts +384 -0
  508. package/src/hooks/runner.ts +7 -0
  509. package/src/inbound/platform-callback-registration.ts +30 -20
  510. package/src/inbound/public-ingress-urls.ts +12 -0
  511. package/src/instrument.ts +1 -1
  512. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  513. package/src/ipc/cli-client.ts +151 -0
  514. package/src/ipc/cli-server.ts +234 -0
  515. package/src/ipc/gateway-client.ts +180 -0
  516. package/src/ipc/routes/index.ts +5 -0
  517. package/src/ipc/routes/wake-conversation.ts +19 -0
  518. package/src/mcp/client.ts +59 -24
  519. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  520. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  521. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  522. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  523. package/src/memory/app-store.ts +31 -1
  524. package/src/memory/attachments-store.ts +70 -0
  525. package/src/memory/auto-analysis-enqueue.ts +127 -0
  526. package/src/memory/auto-analysis-guard.ts +27 -0
  527. package/src/memory/cleanup-schedule-state.ts +37 -0
  528. package/src/memory/conversation-analyze-job.ts +73 -0
  529. package/src/memory/conversation-crud.ts +122 -0
  530. package/src/memory/conversation-disk-view.ts +7 -0
  531. package/src/memory/conversation-group-migration.ts +34 -2
  532. package/src/memory/conversation-queries.ts +6 -5
  533. package/src/memory/conversation-starters-cadence.ts +76 -0
  534. package/src/memory/conversation-title-service.ts +5 -2
  535. package/src/memory/db-init.ts +18 -0
  536. package/src/memory/db-maintenance.ts +108 -0
  537. package/src/memory/db.ts +1 -0
  538. package/src/memory/embedding-backend.test.ts +75 -0
  539. package/src/memory/embedding-backend.ts +131 -5
  540. package/src/memory/embedding-gemini.test.ts +54 -0
  541. package/src/memory/embedding-gemini.ts +20 -9
  542. package/src/memory/embedding-local.ts +176 -17
  543. package/src/memory/graph/consolidation.ts +10 -23
  544. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  545. package/src/memory/graph/extraction-job.ts +15 -0
  546. package/src/memory/graph/extraction.test.ts +23 -0
  547. package/src/memory/graph/extraction.ts +8 -0
  548. package/src/memory/graph/retriever.ts +67 -40
  549. package/src/memory/graph/scoring.test.ts +186 -0
  550. package/src/memory/graph/scoring.ts +31 -1
  551. package/src/memory/graph/store.test.ts +7 -3
  552. package/src/memory/graph/store.ts +47 -12
  553. package/src/memory/graph/tools.ts +1 -1
  554. package/src/memory/group-crud.ts +6 -1
  555. package/src/memory/indexer.ts +95 -16
  556. package/src/memory/job-handlers/cleanup.ts +11 -8
  557. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  558. package/src/memory/jobs-store.ts +64 -4
  559. package/src/memory/jobs-worker.ts +22 -9
  560. package/src/memory/llm-usage-store.ts +137 -60
  561. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  562. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  563. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  564. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  565. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  566. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  567. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  568. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  569. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  570. package/src/memory/migrations/index.ts +12 -0
  571. package/src/memory/migrations/registry.ts +16 -0
  572. package/src/memory/qdrant-manager.ts +43 -16
  573. package/src/memory/schema/conversations.ts +3 -0
  574. package/src/memory/schema/oauth.ts +21 -13
  575. package/src/memory/usage-buckets.ts +396 -0
  576. package/src/messaging/providers/gmail/client.ts +57 -6
  577. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  578. package/src/messaging/providers/slack/adapter.ts +143 -38
  579. package/src/messaging/providers/slack/client.ts +16 -0
  580. package/src/messaging/providers/slack/types.ts +4 -0
  581. package/src/notifications/decision-engine.ts +3 -3
  582. package/src/notifications/signal.ts +5 -0
  583. package/src/oauth/AGENTS.md +76 -0
  584. package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
  585. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  586. package/src/oauth/byo-connection.test.ts +26 -9
  587. package/src/oauth/byo-connection.ts +10 -8
  588. package/src/oauth/connect-orchestrator.ts +25 -21
  589. package/src/oauth/connect-types.ts +3 -3
  590. package/src/oauth/connection-resolver.test.ts +17 -4
  591. package/src/oauth/connection-resolver.ts +22 -18
  592. package/src/oauth/connection.ts +3 -1
  593. package/src/oauth/manual-token-connection.ts +13 -13
  594. package/src/oauth/oauth-store.ts +223 -100
  595. package/src/oauth/platform-connection.test.ts +101 -3
  596. package/src/oauth/platform-connection.ts +56 -35
  597. package/src/oauth/provider-serializer.ts +31 -5
  598. package/src/oauth/revoke.ts +76 -0
  599. package/src/oauth/seed-providers.ts +133 -87
  600. package/src/oauth/token-persistence.ts +1 -1
  601. package/src/permissions/checker.ts +16 -6
  602. package/src/permissions/defaults.ts +49 -1
  603. package/src/permissions/permission-mode.ts +4 -11
  604. package/src/permissions/prompter.ts +13 -1
  605. package/src/permissions/trust-store.ts +3 -3
  606. package/src/permissions/v2-consent-policy.ts +87 -0
  607. package/src/permissions/workspace-policy.ts +3 -0
  608. package/src/platform/client.test.ts +10 -0
  609. package/src/platform/sync-identity.ts +129 -0
  610. package/src/prompts/persona-resolver.ts +126 -2
  611. package/src/prompts/system-prompt.ts +76 -38
  612. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  613. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  614. package/src/prompts/templates/SOUL.md +3 -1
  615. package/src/prompts/templates/UPDATES.md +12 -0
  616. package/src/prompts/templates/channels/slack.md +20 -0
  617. package/src/prompts/update-bulletin-format.ts +26 -9
  618. package/src/prompts/update-bulletin.ts +34 -23
  619. package/src/prompts/user-reference.ts +20 -17
  620. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  621. package/src/providers/anthropic/client.ts +157 -60
  622. package/src/providers/fireworks/client.ts +2 -2
  623. package/src/providers/gemini/client.ts +9 -1
  624. package/src/providers/model-catalog.ts +6 -0
  625. package/src/providers/model-intents.ts +4 -4
  626. package/src/providers/ollama/client.ts +2 -2
  627. package/src/providers/openai/chat-completions-provider.ts +474 -0
  628. package/src/providers/openai/client.ts +25 -440
  629. package/src/providers/openai/responses-provider.ts +502 -0
  630. package/src/providers/openrouter/client.ts +101 -4
  631. package/src/providers/provider-secret-catalog.ts +139 -0
  632. package/src/providers/registry.ts +2 -2
  633. package/src/providers/retry.ts +14 -3
  634. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  635. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  636. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  637. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  638. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  639. package/src/providers/speech-to-text/deepgram.ts +115 -0
  640. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  641. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  642. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  643. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  644. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  645. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  646. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  647. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  648. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  649. package/src/providers/speech-to-text/resolve.ts +386 -6
  650. package/src/providers/types.ts +10 -1
  651. package/src/runtime/AGENTS.md +65 -0
  652. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  653. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  654. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  655. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  656. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  657. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  658. package/src/runtime/agent-wake.ts +512 -0
  659. package/src/runtime/assistant-event-hub.ts +2 -2
  660. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  661. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  662. package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
  663. package/src/runtime/auth/middleware.ts +98 -0
  664. package/src/runtime/auth/route-policy.ts +33 -9
  665. package/src/runtime/auth/token-service.ts +56 -1
  666. package/src/runtime/btw-sidechain.ts +2 -0
  667. package/src/runtime/capability-tokens.ts +414 -0
  668. package/src/runtime/channel-approvals.ts +18 -5
  669. package/src/runtime/channel-invite-transport.ts +1 -1
  670. package/src/runtime/channel-invite-transports/email.ts +14 -6
  671. package/src/runtime/channel-readiness-service.ts +12 -22
  672. package/src/runtime/chrome-extension-registry.ts +368 -0
  673. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  674. package/src/runtime/guardian-decision-types.ts +7 -0
  675. package/src/runtime/http-server.ts +815 -75
  676. package/src/runtime/http-types.ts +6 -2
  677. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  678. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  679. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
  680. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  681. package/src/runtime/migrations/migration-transport.ts +7 -0
  682. package/src/runtime/migrations/migration-wizard.ts +23 -2
  683. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  684. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  685. package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
  686. package/src/runtime/migrations/vbundle-importer.ts +89 -5
  687. package/src/runtime/pending-interactions.ts +18 -13
  688. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  689. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  690. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  691. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  692. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  693. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  694. package/src/runtime/routes/app-management-routes.ts +12 -18
  695. package/src/runtime/routes/approval-routes.ts +90 -16
  696. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  697. package/src/runtime/routes/attachment-routes.ts +216 -17
  698. package/src/runtime/routes/backup-routes.ts +519 -0
  699. package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
  700. package/src/runtime/routes/btw-routes.ts +8 -6
  701. package/src/runtime/routes/contact-routes.test.ts +298 -0
  702. package/src/runtime/routes/contact-routes.ts +132 -5
  703. package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
  704. package/src/runtime/routes/conversation-management-routes.ts +223 -0
  705. package/src/runtime/routes/conversation-routes.ts +598 -103
  706. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  707. package/src/runtime/routes/filing-routes.ts +93 -0
  708. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  709. package/src/runtime/routes/home-feed-routes.ts +334 -0
  710. package/src/runtime/routes/home-state-routes.ts +138 -0
  711. package/src/runtime/routes/host-browser-routes.ts +268 -0
  712. package/src/runtime/routes/host-file-routes.ts +9 -1
  713. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  714. package/src/runtime/routes/identity-routes.ts +262 -33
  715. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  716. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  717. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  718. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  719. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  720. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  721. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  722. package/src/runtime/routes/log-export-routes.ts +42 -22
  723. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  724. package/src/runtime/routes/memory-item-routes.ts +1 -7
  725. package/src/runtime/routes/migration-routes.ts +122 -2
  726. package/src/runtime/routes/oauth-apps.ts +15 -17
  727. package/src/runtime/routes/oauth-providers.ts +4 -0
  728. package/src/runtime/routes/schedule-routes.ts +24 -11
  729. package/src/runtime/routes/settings-routes.ts +31 -102
  730. package/src/runtime/routes/skills-routes.ts +128 -9
  731. package/src/runtime/routes/stt-routes.ts +233 -0
  732. package/src/runtime/routes/subagents-routes.ts +14 -10
  733. package/src/runtime/routes/surface-action-routes.ts +41 -2
  734. package/src/runtime/routes/tts-routes.ts +108 -24
  735. package/src/runtime/routes/usage-routes.ts +38 -9
  736. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  737. package/src/runtime/routes/user-routes.ts +13 -1
  738. package/src/runtime/routes/work-items-routes.ts +8 -1
  739. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  740. package/src/runtime/routes/workspace-routes.ts +8 -1
  741. package/src/runtime/routes/workspace-utils.ts +2 -0
  742. package/src/runtime/runtime-mode.ts +33 -0
  743. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  744. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  745. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  746. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  747. package/src/runtime/services/analyze-conversation.ts +344 -0
  748. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  749. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  750. package/src/runtime/skill-route-registry.ts +49 -0
  751. package/src/runtime/slack-block-formatting.ts +437 -10
  752. package/src/schedule/scheduler.ts +57 -5
  753. package/src/security/ces-credential-client.ts +20 -0
  754. package/src/security/ces-rpc-credential-backend.ts +17 -0
  755. package/src/security/credential-backend.ts +5 -0
  756. package/src/security/oauth2.ts +68 -29
  757. package/src/security/secure-keys.ts +143 -27
  758. package/src/security/token-manager.ts +31 -10
  759. package/src/sequence/engine.ts +23 -0
  760. package/src/sequence/types.ts +1 -1
  761. package/src/skills/catalog-files.ts +554 -0
  762. package/src/skills/category-inference.ts +122 -0
  763. package/src/skills/clawhub-files.ts +213 -0
  764. package/src/skills/clawhub.ts +84 -23
  765. package/src/skills/skill-file-provider.ts +40 -0
  766. package/src/skills/skillssh-files.ts +395 -0
  767. package/src/skills/skillssh-registry.ts +4 -4
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  769. package/src/stt/__tests__/types.test.ts +89 -0
  770. package/src/stt/daemon-batch-transcriber.ts +195 -0
  771. package/src/stt/stt-stream-session.ts +499 -0
  772. package/src/stt/types.ts +330 -0
  773. package/src/stt/wav-encoder.test.ts +373 -0
  774. package/src/stt/wav-encoder.ts +175 -0
  775. package/src/subagent/manager.ts +169 -40
  776. package/src/subagent/types.ts +19 -0
  777. package/src/tools/apps/executors.ts +11 -2
  778. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  779. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  780. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  781. package/src/tools/browser/auth-detector.ts +43 -12
  782. package/src/tools/browser/browser-execution.ts +1787 -342
  783. package/src/tools/browser/browser-manager.ts +81 -12
  784. package/src/tools/browser/browser-mode-constants.ts +12 -0
  785. package/src/tools/browser/browser-mode.ts +92 -0
  786. package/src/tools/browser/browser-status-constants.ts +33 -0
  787. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  788. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  789. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
  790. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
  791. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
  792. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  793. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  794. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  795. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  796. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  797. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  798. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
  799. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  800. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
  801. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  802. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
  803. package/src/tools/browser/cdp-client/errors.ts +49 -0
  804. package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
  805. package/src/tools/browser/cdp-client/factory.ts +914 -0
  806. package/src/tools/browser/cdp-client/index.ts +28 -0
  807. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  808. package/src/tools/browser/cdp-client/types.ts +120 -0
  809. package/src/tools/credentials/vault.ts +35 -6
  810. package/src/tools/filesystem/edit.ts +1 -1
  811. package/src/tools/filesystem/list.ts +1 -1
  812. package/src/tools/filesystem/read.ts +1 -1
  813. package/src/tools/filesystem/write.ts +2 -1
  814. package/src/tools/host-filesystem/edit.ts +1 -1
  815. package/src/tools/host-filesystem/read.ts +12 -15
  816. package/src/tools/host-filesystem/write.ts +1 -1
  817. package/src/tools/host-terminal/host-shell.ts +21 -16
  818. package/src/tools/network/web-fetch.ts +5 -2
  819. package/src/tools/network/web-search.ts +5 -2
  820. package/src/tools/permission-checker.ts +77 -82
  821. package/src/tools/registry.ts +0 -2
  822. package/src/tools/secret-detection-handler.ts +34 -0
  823. package/src/tools/shared/filesystem/image-read.ts +61 -40
  824. package/src/tools/shared/shell-output.ts +3 -1
  825. package/src/tools/side-effects.ts +2 -0
  826. package/src/tools/skills/sandbox-runner.ts +3 -2
  827. package/src/tools/subagent/spawn.ts +47 -3
  828. package/src/tools/subagent/status.ts +2 -0
  829. package/src/tools/system/register.ts +2 -16
  830. package/src/tools/terminal/safe-env.ts +15 -0
  831. package/src/tools/terminal/shell.ts +36 -20
  832. package/src/tools/tool-approval-handler.ts +48 -2
  833. package/src/tools/tool-manifest.ts +21 -0
  834. package/src/tools/types.ts +19 -0
  835. package/src/tools/ui-surface/definitions.ts +6 -1
  836. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  837. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  838. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  839. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  840. package/src/tts/provider-catalog.ts +201 -0
  841. package/src/tts/provider-registry.ts +73 -0
  842. package/src/tts/providers/deepgram-provider.ts +219 -0
  843. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  844. package/src/tts/providers/fish-audio-provider.ts +183 -0
  845. package/src/tts/providers/index.ts +42 -0
  846. package/src/tts/providers/register-builtins.ts +130 -0
  847. package/src/tts/synthesize-text.ts +110 -0
  848. package/src/tts/tts-config-resolver.ts +78 -0
  849. package/src/tts/types.ts +153 -0
  850. package/src/types/onboarding-context.ts +7 -0
  851. package/src/util/abort-reasons.ts +58 -0
  852. package/src/util/device-id.ts +32 -16
  853. package/src/util/errors.ts +9 -1
  854. package/src/util/platform.ts +63 -24
  855. package/src/util/pricing.ts +66 -3
  856. package/src/util/spawn.ts +1 -1
  857. package/src/util/truncate.ts +4 -2
  858. package/src/util/unicode.ts +201 -0
  859. package/src/version.ts +19 -24
  860. package/src/watcher/engine.ts +23 -0
  861. package/src/watcher/watcher-store.ts +31 -0
  862. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  863. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  864. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  865. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  866. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  867. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  868. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  869. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  870. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  871. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  872. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  873. package/src/workspace/migrations/registry.ts +16 -0
  874. package/src/workspace/top-level-renderer.ts +31 -1
  875. package/src/workspace/turn-commit.ts +31 -0
  876. package/src/__tests__/chrome-cdp.test.ts +0 -419
  877. package/src/__tests__/email-cli.test.ts +0 -297
  878. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  879. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  880. package/src/__tests__/permission-mode-store.test.ts +0 -277
  881. package/src/browser-extension-relay/protocol.ts +0 -63
  882. package/src/browser-extension-relay/server.ts +0 -203
  883. package/src/cli/commands/browser-relay.ts +0 -536
  884. package/src/config/schemas/sandbox.ts +0 -14
  885. package/src/email/guardrails.ts +0 -221
  886. package/src/email/provider.ts +0 -117
  887. package/src/email/providers/agentmail.ts +0 -361
  888. package/src/email/providers/index.ts +0 -65
  889. package/src/email/service.ts +0 -384
  890. package/src/email/types.ts +0 -126
  891. package/src/permissions/permission-mode-store.ts +0 -180
  892. package/src/prompts/templates/USER.md +0 -13
  893. package/src/providers/speech-to-text/types.ts +0 -17
  894. package/src/tools/browser/chrome-cdp.ts +0 -239
  895. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,967 @@
1
+ /**
2
+ * Unit tests for the /v1/backups HTTP route handlers.
3
+ *
4
+ * These tests drive the handler functions directly (bypassing the router)
5
+ * so they exercise the handler logic — input validation, path containment,
6
+ * key-loading, and error mapping — without needing a live HTTP server.
7
+ *
8
+ * Module-level mocks replace the real `config/loader`, `memory/checkpoints`,
9
+ * `backup/backup-worker`, `backup/restore`, and `backup/backup-key` modules
10
+ * with test doubles. Each test shapes the doubles through the `setMockXxx`
11
+ * helpers in the setup/teardown block.
12
+ */
13
+
14
+ import {
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ rmSync,
18
+ symlinkSync,
19
+ writeFileSync,
20
+ } from "node:fs";
21
+ import { tmpdir } from "node:os";
22
+ import { join } from "node:path";
23
+ import {
24
+ afterEach,
25
+ beforeEach,
26
+ describe,
27
+ expect,
28
+ mock,
29
+ test,
30
+ } from "bun:test";
31
+
32
+ import type { BackupRunResult } from "../../../backup/backup-worker.js";
33
+ import type { SnapshotEntry } from "../../../backup/list-snapshots.js";
34
+ import type { RestoreResult, VerifyResult } from "../../../backup/restore.js";
35
+ import type { BackupConfig } from "../../../config/schema.js";
36
+ import { BackupConfigSchema } from "../../../config/schema.js";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Module mocks — must appear before any imports of the module under test
40
+ // ---------------------------------------------------------------------------
41
+
42
+ mock.module("../../../util/logger.js", () => ({
43
+ getLogger: () =>
44
+ new Proxy({} as Record<string, unknown>, {
45
+ get: () => () => {},
46
+ }),
47
+ }));
48
+
49
+ // -- listSnapshotsInDir spy ------------------------------------------------
50
+ // Wraps the real implementation so tests can assert on which directories
51
+ // were enumerated. Needed to verify handleBackupList skips offsite
52
+ // enumeration when backup.offsite.enabled is false.
53
+
54
+ const listSnapshotsCallLog: string[] = [];
55
+ const { listSnapshotsInDir: realListSnapshotsInDir } = await import(
56
+ "../../../backup/list-snapshots.js"
57
+ );
58
+ mock.module("../../../backup/list-snapshots.js", () => ({
59
+ listSnapshotsInDir: async (dir: string) => {
60
+ listSnapshotsCallLog.push(dir);
61
+ return realListSnapshotsInDir(dir);
62
+ },
63
+ }));
64
+
65
+ // -- Config mock -----------------------------------------------------------
66
+ // Built in `beforeEach` from BackupConfigSchema defaults, with overrides
67
+ // applied per test via `setMockBackupConfig`.
68
+
69
+ let mockBackupConfig: BackupConfig = BackupConfigSchema.parse({});
70
+ let mockWorkspaceDir = "/tmp/mock-workspace-unused";
71
+
72
+ let mockInvalidateConfigCacheCalls = 0;
73
+
74
+ mock.module("../../../config/loader.js", () => ({
75
+ getConfig: () => ({
76
+ backup: mockBackupConfig,
77
+ // The handlers only touch `.backup`, but getConfig() is typed as returning
78
+ // the full AssistantConfig. Cast through `unknown` so the partial shape is
79
+ // accepted without pulling in the full config schema.
80
+ }),
81
+ invalidateConfigCache: () => {
82
+ mockInvalidateConfigCacheCalls += 1;
83
+ recoveryCallOrder.push("invalidateConfigCache");
84
+ },
85
+ }));
86
+
87
+ // -- Trust-cache mock ------------------------------------------------------
88
+ // After a successful `restoreFromSnapshot`, handleBackupRestore must call
89
+ // `invalidateConfigCache()` and `clearTrustCache()` (matching the migration
90
+ // importer). The SQLite handle reset is owned by `restoreFromSnapshot` and
91
+ // covered in restore.test.ts. Tests here record the call sequence via
92
+ // `recoveryCallOrder` and assert on the relative ordering.
93
+
94
+ let mockClearTrustCacheCalls = 0;
95
+ const recoveryCallOrder: string[] = [];
96
+
97
+ mock.module("../../../permissions/trust-store.js", () => ({
98
+ clearCache: () => {
99
+ mockClearTrustCacheCalls += 1;
100
+ recoveryCallOrder.push("clearTrustCache");
101
+ },
102
+ }));
103
+
104
+ // -- Platform paths mock ---------------------------------------------------
105
+ // `getWorkspaceDir` / `getWorkspaceHooksDir` are used inside the restore
106
+ // handler to build a DefaultPathResolver. Return test-friendly paths so
107
+ // restore tests don't pollute the real workspace.
108
+
109
+ mock.module("../../../util/platform.js", () => ({
110
+ getWorkspaceDir: () => mockWorkspaceDir,
111
+ getWorkspaceHooksDir: () => join(mockWorkspaceDir, "hooks"),
112
+ // Passed through when tests need the protected dir (e.g. via paths.ts).
113
+ getProtectedDir: () => join(mockWorkspaceDir, "protected"),
114
+ getDbPath: () => join(mockWorkspaceDir, "data", "db", "assistant.db"),
115
+ }));
116
+
117
+ // -- Memory checkpoint mock ------------------------------------------------
118
+
119
+ const mockCheckpointStore: Record<string, string | null> = {};
120
+
121
+ mock.module("../../../memory/checkpoints.js", () => ({
122
+ getMemoryCheckpoint: (key: string) => mockCheckpointStore[key] ?? null,
123
+ setMemoryCheckpoint: (key: string, value: string) => {
124
+ mockCheckpointStore[key] = value;
125
+ },
126
+ }));
127
+
128
+ // -- Backup key mock -------------------------------------------------------
129
+ // Tests override this via `setMockBackupKey` / `setMockBackupKeyMissing`.
130
+ // The mock also records how many times the key was read so tests can assert
131
+ // "key file was never touched" for plaintext-only code paths.
132
+
133
+ let mockBackupKey: Buffer | null = Buffer.alloc(32, 0xaa);
134
+ let mockReadBackupKeyCalls = 0;
135
+
136
+ mock.module("../../../backup/backup-key.js", () => ({
137
+ readBackupKey: async (_path: string) => {
138
+ mockReadBackupKeyCalls += 1;
139
+ return mockBackupKey;
140
+ },
141
+ ensureBackupKey: async (_path: string) => mockBackupKey ?? Buffer.alloc(32),
142
+ }));
143
+
144
+ // -- Backup worker mock ----------------------------------------------------
145
+ // `createSnapshotNow` is replaced so tests can control success / 409
146
+ // behavior without touching the real export pipeline.
147
+
148
+ let mockCreateSnapshotResult: BackupRunResult | null = null;
149
+ let mockCreateSnapshotError: Error | null = null;
150
+ let mockCreateSnapshotCalls = 0;
151
+
152
+ mock.module("../../../backup/backup-worker.js", () => ({
153
+ createSnapshotNow: async (_config: BackupConfig, _now: Date) => {
154
+ mockCreateSnapshotCalls += 1;
155
+ if (mockCreateSnapshotError) throw mockCreateSnapshotError;
156
+ if (mockCreateSnapshotResult == null) {
157
+ throw new Error("Test forgot to set mockCreateSnapshotResult");
158
+ }
159
+ return mockCreateSnapshotResult;
160
+ },
161
+ }));
162
+
163
+ // -- Restore module mock ---------------------------------------------------
164
+ // Both `restoreFromSnapshot` and `verifySnapshot` are replaced. Tests
165
+ // inspect `lastRestoreArgs` / `lastVerifyArgs` to assert the handler
166
+ // forwarded the correct key and options.
167
+
168
+ interface RestoreCall {
169
+ path: string;
170
+ hasKey: boolean;
171
+ workspaceDir: string | undefined;
172
+ }
173
+ interface VerifyCall {
174
+ path: string;
175
+ hasKey: boolean;
176
+ }
177
+
178
+ let lastRestoreArgs: RestoreCall | null = null;
179
+ let lastVerifyArgs: VerifyCall | null = null;
180
+ let mockRestoreResult: RestoreResult = {
181
+ manifest: {
182
+ schema_version: "1.0",
183
+ created_at: "2026-04-11T10:00:00.000Z",
184
+ files: [],
185
+ manifest_sha256: "0".repeat(64),
186
+ } as unknown as RestoreResult["manifest"],
187
+ restoredFiles: 0,
188
+ };
189
+ let mockRestoreError: Error | null = null;
190
+ let mockVerifyResult: VerifyResult = { valid: true };
191
+
192
+ mock.module("../../../backup/restore.js", () => ({
193
+ restoreFromSnapshot: async (
194
+ path: string,
195
+ opts: {
196
+ key?: Buffer;
197
+ workspaceDir?: string;
198
+ },
199
+ ) => {
200
+ recoveryCallOrder.push("restoreFromSnapshot");
201
+ lastRestoreArgs = {
202
+ path,
203
+ hasKey: opts.key != null,
204
+ workspaceDir: opts.workspaceDir,
205
+ };
206
+ if (mockRestoreError) throw mockRestoreError;
207
+ return mockRestoreResult;
208
+ },
209
+ verifySnapshot: async (path: string, opts: { key?: Buffer }) => {
210
+ lastVerifyArgs = { path, hasKey: opts.key != null };
211
+ return mockVerifyResult;
212
+ },
213
+ }));
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Import under test — after mocks
217
+ // ---------------------------------------------------------------------------
218
+
219
+ import {
220
+ backupRouteDefinitions,
221
+ handleBackupCreate,
222
+ handleBackupList,
223
+ handleBackupRestore,
224
+ handleBackupVerify,
225
+ } from "../backup-routes.js";
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Helpers
229
+ // ---------------------------------------------------------------------------
230
+
231
+ let ROOT: string;
232
+ let LOCAL_DIR: string;
233
+
234
+ /** Build a valid BackupConfig with overrides applied via spread. */
235
+ function makeConfig(overrides: Partial<BackupConfig> = {}): BackupConfig {
236
+ const base = BackupConfigSchema.parse({});
237
+ return { ...base, ...overrides };
238
+ }
239
+
240
+ /** Write a backup-shaped file to disk so `listSnapshotsInDir` picks it up. */
241
+ function writeBackupFile(
242
+ dir: string,
243
+ filename: string,
244
+ payload: string = "fake-bundle",
245
+ ): string {
246
+ mkdirSync(dir, { recursive: true });
247
+ const fullPath = join(dir, filename);
248
+ writeFileSync(fullPath, payload);
249
+ return fullPath;
250
+ }
251
+
252
+ function jsonRequest(method: string, body: unknown): Request {
253
+ return new Request("http://localhost/v1/backups", {
254
+ method,
255
+ headers: { "Content-Type": "application/json" },
256
+ body: JSON.stringify(body),
257
+ });
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Setup / teardown
262
+ // ---------------------------------------------------------------------------
263
+
264
+ beforeEach(() => {
265
+ ROOT = mkdtempSync(join(tmpdir(), "vellum-backup-routes-"));
266
+ LOCAL_DIR = join(ROOT, "local");
267
+ // Reset mocks to defaults
268
+ mockBackupConfig = makeConfig({ localDirectory: LOCAL_DIR });
269
+ mockWorkspaceDir = join(ROOT, "workspace");
270
+ for (const key of Object.keys(mockCheckpointStore)) {
271
+ delete mockCheckpointStore[key];
272
+ }
273
+ mockBackupKey = Buffer.alloc(32, 0xaa);
274
+ mockReadBackupKeyCalls = 0;
275
+ mockCreateSnapshotResult = null;
276
+ mockCreateSnapshotError = null;
277
+ mockCreateSnapshotCalls = 0;
278
+ lastRestoreArgs = null;
279
+ lastVerifyArgs = null;
280
+ mockRestoreError = null;
281
+ mockRestoreResult = {
282
+ manifest: {
283
+ schema_version: "1.0",
284
+ created_at: "2026-04-11T10:00:00.000Z",
285
+ files: [],
286
+ manifest_sha256: "0".repeat(64),
287
+ } as unknown as RestoreResult["manifest"],
288
+ restoredFiles: 0,
289
+ };
290
+ mockVerifyResult = { valid: true };
291
+ mockInvalidateConfigCacheCalls = 0;
292
+ mockClearTrustCacheCalls = 0;
293
+ recoveryCallOrder.length = 0;
294
+ listSnapshotsCallLog.length = 0;
295
+ });
296
+
297
+ afterEach(() => {
298
+ try {
299
+ rmSync(ROOT, { recursive: true, force: true });
300
+ } catch {
301
+ // best-effort
302
+ }
303
+ });
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // handleBackupList
307
+ // ---------------------------------------------------------------------------
308
+
309
+ describe("handleBackupList", () => {
310
+ test("empty workspace: returns empty local array and one unreachable iCloud default", async () => {
311
+ // Use default offsite destinations (null) so the iCloud default kicks in.
312
+ // `getICloudDriveRoot` reads `process.env.HOME` at call time. Overriding
313
+ // HOME to our ROOT tempdir both (a) keeps the test hermetic — it does not
314
+ // depend on whether the dev machine has iCloud Drive enabled or contains
315
+ // any real backup bundles at the real iCloud path, and (b) ensures the
316
+ // derived default iCloud dir is unreachable (no Library/Mobile Documents
317
+ // under ROOT) so the probe returns `reachable: false` and the snapshots
318
+ // array is empty.
319
+ const origHome = process.env.HOME;
320
+ process.env.HOME = ROOT;
321
+ try {
322
+ mockBackupConfig = makeConfig({
323
+ localDirectory: LOCAL_DIR,
324
+ offsite: {
325
+ enabled: true,
326
+ destinations: null,
327
+ },
328
+ });
329
+
330
+ const res = await handleBackupList(
331
+ new Request("http://localhost/v1/backups"),
332
+ );
333
+ expect(res.status).toBe(200);
334
+ const body = (await res.json()) as {
335
+ local: SnapshotEntry[];
336
+ offsite: Array<{
337
+ destination: { path: string; encrypt: boolean };
338
+ snapshots: SnapshotEntry[];
339
+ reachable: boolean;
340
+ }>;
341
+ nextRunAt: string | null;
342
+ };
343
+ expect(body.local).toEqual([]);
344
+ // iCloud default is present as a single destination pointing at the
345
+ // derived-from-ROOT iCloud path, which does not exist on disk.
346
+ expect(body.offsite).toHaveLength(1);
347
+ expect(body.offsite[0].destination.encrypt).toBe(true);
348
+ expect(body.offsite[0].snapshots).toEqual([]);
349
+ expect(body.offsite[0].reachable).toBe(false);
350
+ expect(body.nextRunAt).toBeNull();
351
+ } finally {
352
+ if (origHome === undefined) {
353
+ delete process.env.HOME;
354
+ } else {
355
+ process.env.HOME = origHome;
356
+ }
357
+ }
358
+ });
359
+
360
+ test("two local files: returned newest-first", async () => {
361
+ writeBackupFile(LOCAL_DIR, "backup-20260411-100000.vbundle");
362
+ writeBackupFile(LOCAL_DIR, "backup-20260411-120000.vbundle");
363
+ mockBackupConfig = makeConfig({
364
+ localDirectory: LOCAL_DIR,
365
+ offsite: { enabled: true, destinations: [] },
366
+ });
367
+
368
+ const res = await handleBackupList(new Request("http://localhost/v1/backups"));
369
+ expect(res.status).toBe(200);
370
+ const body = (await res.json()) as {
371
+ local: SnapshotEntry[];
372
+ offsite: Array<unknown>;
373
+ };
374
+ expect(body.local).toHaveLength(2);
375
+ expect(body.local[0].filename).toBe("backup-20260411-120000.vbundle");
376
+ expect(body.local[1].filename).toBe("backup-20260411-100000.vbundle");
377
+ expect(body.offsite).toEqual([]);
378
+ });
379
+
380
+ test("two offsite destinations: reachable + unreachable reflected per-entry", async () => {
381
+ const reachableDir = join(ROOT, "offsite-reachable");
382
+ const unreachableDir = join(ROOT, "nope", "deeper", "backups");
383
+ mkdirSync(reachableDir, { recursive: true });
384
+ // Put a snapshot in the reachable one so the `snapshots` array is populated.
385
+ writeBackupFile(reachableDir, "backup-20260411-100000.vbundle");
386
+
387
+ mockBackupConfig = makeConfig({
388
+ localDirectory: LOCAL_DIR,
389
+ offsite: {
390
+ enabled: true,
391
+ destinations: [
392
+ { path: reachableDir, encrypt: false },
393
+ { path: unreachableDir, encrypt: true },
394
+ ],
395
+ },
396
+ });
397
+
398
+ const res = await handleBackupList(new Request("http://localhost/v1/backups"));
399
+ expect(res.status).toBe(200);
400
+ const body = (await res.json()) as {
401
+ offsite: Array<{
402
+ destination: { path: string; encrypt: boolean };
403
+ snapshots: SnapshotEntry[];
404
+ reachable: boolean;
405
+ }>;
406
+ };
407
+ expect(body.offsite).toHaveLength(2);
408
+ expect(body.offsite[0].destination.path).toBe(reachableDir);
409
+ expect(body.offsite[0].reachable).toBe(true);
410
+ expect(body.offsite[0].snapshots).toHaveLength(1);
411
+ expect(body.offsite[0].snapshots[0].filename).toBe(
412
+ "backup-20260411-100000.vbundle",
413
+ );
414
+ expect(body.offsite[1].destination.path).toBe(unreachableDir);
415
+ expect(body.offsite[1].reachable).toBe(false);
416
+ expect(body.offsite[1].snapshots).toEqual([]);
417
+ });
418
+
419
+ test("encrypted files in a reachable offsite dir return with encrypted: true", async () => {
420
+ const encryptedDir = join(ROOT, "offsite-enc");
421
+ mkdirSync(encryptedDir, { recursive: true });
422
+ writeBackupFile(encryptedDir, "backup-20260411-100000.vbundle.enc");
423
+ writeBackupFile(encryptedDir, "backup-20260411-120000.vbundle.enc");
424
+
425
+ mockBackupConfig = makeConfig({
426
+ localDirectory: LOCAL_DIR,
427
+ offsite: {
428
+ enabled: true,
429
+ destinations: [{ path: encryptedDir, encrypt: true }],
430
+ },
431
+ });
432
+
433
+ const res = await handleBackupList(new Request("http://localhost/v1/backups"));
434
+ expect(res.status).toBe(200);
435
+ const body = (await res.json()) as {
436
+ offsite: Array<{
437
+ snapshots: SnapshotEntry[];
438
+ reachable: boolean;
439
+ }>;
440
+ };
441
+ expect(body.offsite).toHaveLength(1);
442
+ expect(body.offsite[0].reachable).toBe(true);
443
+ expect(body.offsite[0].snapshots).toHaveLength(2);
444
+ // Newest-first
445
+ expect(body.offsite[0].snapshots[0].filename).toBe(
446
+ "backup-20260411-120000.vbundle.enc",
447
+ );
448
+ expect(body.offsite[0].snapshots[0].encrypted).toBe(true);
449
+ expect(body.offsite[0].snapshots[1].encrypted).toBe(true);
450
+ });
451
+
452
+ test("nextRunAt is computed from checkpoint + intervalHours when enabled", async () => {
453
+ const lastRunMs = Date.parse("2026-04-11T10:00:00Z");
454
+ mockCheckpointStore["backup:last_run_at"] = String(lastRunMs);
455
+ mockBackupConfig = makeConfig({
456
+ enabled: true,
457
+ intervalHours: 6,
458
+ localDirectory: LOCAL_DIR,
459
+ offsite: { enabled: true, destinations: [] },
460
+ });
461
+
462
+ const res = await handleBackupList(new Request("http://localhost/v1/backups"));
463
+ expect(res.status).toBe(200);
464
+ const body = (await res.json()) as { nextRunAt: string | null };
465
+ // 6 hours after 10:00 UTC is 16:00 UTC
466
+ expect(body.nextRunAt).toBe("2026-04-11T16:00:00.000Z");
467
+ });
468
+
469
+ test("nextRunAt is null when backup is disabled", async () => {
470
+ mockCheckpointStore["backup:last_run_at"] = String(
471
+ Date.parse("2026-04-11T10:00:00Z"),
472
+ );
473
+ mockBackupConfig = makeConfig({
474
+ enabled: false,
475
+ localDirectory: LOCAL_DIR,
476
+ offsite: { enabled: true, destinations: [] },
477
+ });
478
+
479
+ const res = await handleBackupList(new Request("http://localhost/v1/backups"));
480
+ const body = (await res.json()) as { nextRunAt: string | null };
481
+ expect(body.nextRunAt).toBeNull();
482
+ });
483
+
484
+ test("offsite.enabled=false returns offsite:[] and offsiteEnabled:false without probing destinations", async () => {
485
+ // Regression test: when the user disables offsite backups, the HTTP
486
+ // handler must mirror the worker's behavior and return an empty offsite
487
+ // list without enumerating any destinations. Previously the handler
488
+ // would still probe each configured destination, causing the macOS UI
489
+ // to render offsite cards even after offsite was turned off.
490
+ //
491
+ // Even with destinations present in config, `offsite.enabled=false`
492
+ // should short-circuit the enumeration loop.
493
+ const configuredDestDir = join(ROOT, "offsite-still-configured");
494
+ mkdirSync(configuredDestDir, { recursive: true });
495
+ mockBackupConfig = makeConfig({
496
+ localDirectory: LOCAL_DIR,
497
+ offsite: {
498
+ enabled: false,
499
+ destinations: [
500
+ { path: configuredDestDir, encrypt: true },
501
+ ],
502
+ },
503
+ });
504
+
505
+ const res = await handleBackupList(
506
+ new Request("http://localhost/v1/backups"),
507
+ );
508
+ expect(res.status).toBe(200);
509
+ const body = (await res.json()) as {
510
+ local: SnapshotEntry[];
511
+ offsite: unknown[];
512
+ offsiteEnabled: boolean;
513
+ };
514
+ expect(body.offsite).toEqual([]);
515
+ expect(body.offsiteEnabled).toBe(false);
516
+ // listSnapshotsInDir should only have been called for the local dir —
517
+ // never for any offsite destination.
518
+ expect(listSnapshotsCallLog).toEqual([LOCAL_DIR]);
519
+ });
520
+
521
+ test("offsite.enabled=true returns offsiteEnabled:true", async () => {
522
+ mockBackupConfig = makeConfig({
523
+ localDirectory: LOCAL_DIR,
524
+ offsite: { enabled: true, destinations: [] },
525
+ });
526
+
527
+ const res = await handleBackupList(
528
+ new Request("http://localhost/v1/backups"),
529
+ );
530
+ expect(res.status).toBe(200);
531
+ const body = (await res.json()) as { offsiteEnabled: boolean };
532
+ expect(body.offsiteEnabled).toBe(true);
533
+ });
534
+ });
535
+
536
+ // ---------------------------------------------------------------------------
537
+ // handleBackupCreate
538
+ // ---------------------------------------------------------------------------
539
+
540
+ describe("handleBackupCreate", () => {
541
+ const fakeRunResult: BackupRunResult = {
542
+ local: {
543
+ path: "/tmp/fake/backup-20260411-100000.vbundle",
544
+ filename: "backup-20260411-100000.vbundle",
545
+ createdAt: new Date("2026-04-11T10:00:00Z"),
546
+ sizeBytes: 100,
547
+ encrypted: false,
548
+ },
549
+ offsite: [],
550
+ durationMs: 42,
551
+ };
552
+
553
+ test("manual create bypasses enabled flag and succeeds with disabled config", async () => {
554
+ mockBackupConfig = makeConfig({
555
+ enabled: false,
556
+ localDirectory: LOCAL_DIR,
557
+ offsite: { enabled: false, destinations: null },
558
+ });
559
+ mockCreateSnapshotResult = fakeRunResult;
560
+
561
+ const res = await handleBackupCreate(
562
+ new Request("http://localhost/v1/backups/create", { method: "POST" }),
563
+ );
564
+ expect(res.status).toBe(200);
565
+ const body = (await res.json()) as BackupRunResult;
566
+ expect(body.durationMs).toBe(42);
567
+ expect(body.offsite).toEqual([]);
568
+ expect(mockCreateSnapshotCalls).toBe(1);
569
+ });
570
+
571
+ test("plaintext-only destinations do not create backup.key file", async () => {
572
+ const plaintextDir = join(ROOT, "offsite-plain");
573
+ mkdirSync(plaintextDir, { recursive: true });
574
+ mockBackupConfig = makeConfig({
575
+ enabled: true,
576
+ localDirectory: LOCAL_DIR,
577
+ offsite: {
578
+ enabled: true,
579
+ destinations: [{ path: plaintextDir, encrypt: false }],
580
+ },
581
+ });
582
+ mockCreateSnapshotResult = fakeRunResult;
583
+
584
+ // The mocked createSnapshotNow never touches the key file. We assert:
585
+ // (a) the HTTP layer itself did not try to load readBackupKey, and
586
+ // (b) no backup.key file exists under the protected dir (which is under
587
+ // our ROOT per the platform mock).
588
+ mockReadBackupKeyCalls = 0;
589
+ const res = await handleBackupCreate(
590
+ new Request("http://localhost/v1/backups/create", { method: "POST" }),
591
+ );
592
+ expect(res.status).toBe(200);
593
+ expect(mockReadBackupKeyCalls).toBe(0);
594
+ // ROOT is a fresh temp dir — no protected/backup.key was ever written.
595
+ const keyFileExists = await import("node:fs").then((m) =>
596
+ m.existsSync(join(ROOT, "workspace", "protected", "backup.key")),
597
+ );
598
+ expect(keyFileExists).toBe(false);
599
+ });
600
+
601
+ test("concurrent call returns 409 when mock raises 'snapshot in progress'", async () => {
602
+ mockBackupConfig = makeConfig({ localDirectory: LOCAL_DIR });
603
+ mockCreateSnapshotError = new Error("snapshot in progress");
604
+
605
+ const res = await handleBackupCreate(
606
+ new Request("http://localhost/v1/backups/create", { method: "POST" }),
607
+ );
608
+ expect(res.status).toBe(409);
609
+ const body = (await res.json()) as { error: { code: string } };
610
+ expect(body.error.code).toBe("CONFLICT");
611
+ });
612
+
613
+ test("cross-process conflict ('locked by pid N') is still mapped to 409", async () => {
614
+ // Regression test for the startsWith matcher in handleBackupCreate: the
615
+ // cross-process file lock in snapshot-lock.ts throws
616
+ // "snapshot in progress (locked by pid N)" rather than the bare
617
+ // "snapshot in progress" message the in-process flag emits. Both must
618
+ // map to 409 / CONFLICT — pin the matcher against future drift.
619
+ mockBackupConfig = makeConfig({ localDirectory: LOCAL_DIR });
620
+ mockCreateSnapshotError = new Error(
621
+ "snapshot in progress (locked by pid 12345)",
622
+ );
623
+
624
+ const res = await handleBackupCreate(
625
+ new Request("http://localhost/v1/backups/create", { method: "POST" }),
626
+ );
627
+ expect(res.status).toBe(409);
628
+ const body = (await res.json()) as { error: { code: string } };
629
+ expect(body.error.code).toBe("CONFLICT");
630
+ });
631
+
632
+ test("other errors are surfaced as 500", async () => {
633
+ mockCreateSnapshotError = new Error("disk full");
634
+ const res = await handleBackupCreate(
635
+ new Request("http://localhost/v1/backups/create", { method: "POST" }),
636
+ );
637
+ expect(res.status).toBe(500);
638
+ const body = (await res.json()) as { error: { code: string; message: string } };
639
+ expect(body.error.code).toBe("INTERNAL_ERROR");
640
+ expect(body.error.message).toBe("disk full");
641
+ });
642
+ });
643
+
644
+ // ---------------------------------------------------------------------------
645
+ // handleBackupRestore
646
+ // ---------------------------------------------------------------------------
647
+
648
+ describe("handleBackupRestore", () => {
649
+ test("rejects path outside the allowed directories with 400", async () => {
650
+ const outsidePath = join(ROOT, "elsewhere", "backup-20260411-100000.vbundle");
651
+ mkdirSync(join(ROOT, "elsewhere"), { recursive: true });
652
+ writeFileSync(outsidePath, "payload");
653
+ mockBackupConfig = makeConfig({
654
+ localDirectory: LOCAL_DIR,
655
+ offsite: { enabled: true, destinations: [] },
656
+ });
657
+
658
+ const res = await handleBackupRestore(
659
+ jsonRequest("POST", { path: outsidePath }),
660
+ );
661
+ expect(res.status).toBe(400);
662
+ const body = (await res.json()) as { error: { code: string; message: string } };
663
+ expect(body.error.code).toBe("BAD_REQUEST");
664
+ expect(body.error.message).toMatch(/outside/i);
665
+ expect(lastRestoreArgs).toBeNull();
666
+ });
667
+
668
+ test("rejects symlink that escapes the allowed directories", async () => {
669
+ // Create a valid-looking symlink inside LOCAL_DIR that points to a file
670
+ // outside. realpath() follows the symlink, so containment check fails.
671
+ const outsideTarget = join(ROOT, "evil-target.vbundle");
672
+ writeFileSync(outsideTarget, "payload");
673
+ mkdirSync(LOCAL_DIR, { recursive: true });
674
+ const symlinkPath = join(LOCAL_DIR, "backup-20260411-100000.vbundle");
675
+ symlinkSync(outsideTarget, symlinkPath);
676
+ mockBackupConfig = makeConfig({
677
+ localDirectory: LOCAL_DIR,
678
+ offsite: { enabled: true, destinations: [] },
679
+ });
680
+
681
+ const res = await handleBackupRestore(
682
+ jsonRequest("POST", { path: symlinkPath }),
683
+ );
684
+ expect(res.status).toBe(400);
685
+ expect(lastRestoreArgs).toBeNull();
686
+ });
687
+
688
+ test("plaintext .vbundle inside local dir is restored without loading key", async () => {
689
+ const snapshotPath = writeBackupFile(
690
+ LOCAL_DIR,
691
+ "backup-20260411-100000.vbundle",
692
+ );
693
+ mockBackupConfig = makeConfig({
694
+ localDirectory: LOCAL_DIR,
695
+ offsite: { enabled: true, destinations: [] },
696
+ });
697
+ mockReadBackupKeyCalls = 0;
698
+
699
+ const res = await handleBackupRestore(
700
+ jsonRequest("POST", { path: snapshotPath }),
701
+ );
702
+ expect(res.status).toBe(200);
703
+ expect(mockReadBackupKeyCalls).toBe(0);
704
+ expect(lastRestoreArgs).not.toBeNull();
705
+ expect(lastRestoreArgs!.hasKey).toBe(false);
706
+ // restoreFromSnapshot should be called with the realpath'd snapshot path.
707
+ // On macOS, `/var/...` resolves to `/private/var/...`, so compare against
708
+ // the realpath of the input rather than the raw string.
709
+ const expectedRealpath = await (
710
+ await import("node:fs/promises")
711
+ ).realpath(snapshotPath);
712
+ expect(lastRestoreArgs!.path).toBe(expectedRealpath);
713
+ });
714
+
715
+ test("encrypted .vbundle.enc inside local dir loads key and restores", async () => {
716
+ const snapshotPath = writeBackupFile(
717
+ LOCAL_DIR,
718
+ "backup-20260411-100000.vbundle.enc",
719
+ );
720
+ mockBackupConfig = makeConfig({
721
+ localDirectory: LOCAL_DIR,
722
+ offsite: { enabled: true, destinations: [] },
723
+ });
724
+ mockBackupKey = Buffer.alloc(32, 0xbb);
725
+ mockReadBackupKeyCalls = 0;
726
+
727
+ const res = await handleBackupRestore(
728
+ jsonRequest("POST", { path: snapshotPath }),
729
+ );
730
+ expect(res.status).toBe(200);
731
+ expect(mockReadBackupKeyCalls).toBe(1);
732
+ expect(lastRestoreArgs).not.toBeNull();
733
+ expect(lastRestoreArgs!.hasKey).toBe(true);
734
+ });
735
+
736
+ test("encrypted bundle with missing backup.key returns a clear 400", async () => {
737
+ const snapshotPath = writeBackupFile(
738
+ LOCAL_DIR,
739
+ "backup-20260411-100000.vbundle.enc",
740
+ );
741
+ mockBackupConfig = makeConfig({
742
+ localDirectory: LOCAL_DIR,
743
+ offsite: { enabled: true, destinations: [] },
744
+ });
745
+ mockBackupKey = null; // readBackupKey returns null when the file is missing
746
+
747
+ const res = await handleBackupRestore(
748
+ jsonRequest("POST", { path: snapshotPath }),
749
+ );
750
+ expect(res.status).toBe(400);
751
+ const body = (await res.json()) as { error: { code: string; message: string } };
752
+ expect(body.error.code).toBe("BAD_REQUEST");
753
+ expect(body.error.message).toMatch(/backup.key is missing/);
754
+ // restoreFromSnapshot must NOT have been called — we bail before handing
755
+ // the path to the restore helper.
756
+ expect(lastRestoreArgs).toBeNull();
757
+ });
758
+
759
+ test("successful restore runs the full recovery sequence in order", async () => {
760
+ // Regression test for the restore-corrupts-daemon-state gap:
761
+ // handleBackupRestore must invoke `restoreFromSnapshot` FIRST (which
762
+ // internally calls `resetDb()` before overwriting `assistant.db`), then
763
+ // invalidateConfigCache() + clearTrustCache() AFTER (so the daemon
764
+ // re-reads the restored config/trust rules). The migration importer
765
+ // already does this — the backup handler must match.
766
+ // Note: `resetDb` ordering vs the commit step is verified in
767
+ // src/backup/__tests__/restore.test.ts; here we only verify the
768
+ // handler-level ordering of restoreFromSnapshot → cache invalidations.
769
+ const snapshotPath = writeBackupFile(
770
+ LOCAL_DIR,
771
+ "backup-20260411-100000.vbundle",
772
+ );
773
+ mockBackupConfig = makeConfig({
774
+ localDirectory: LOCAL_DIR,
775
+ offsite: { enabled: true, destinations: [] },
776
+ });
777
+
778
+ const res = await handleBackupRestore(
779
+ jsonRequest("POST", { path: snapshotPath }),
780
+ );
781
+ expect(res.status).toBe(200);
782
+ expect(mockInvalidateConfigCacheCalls).toBe(1);
783
+ expect(mockClearTrustCacheCalls).toBe(1);
784
+ expect(recoveryCallOrder).toEqual([
785
+ "restoreFromSnapshot",
786
+ "invalidateConfigCache",
787
+ "clearTrustCache",
788
+ ]);
789
+ });
790
+
791
+ test("restore failure leaves caches untouched", async () => {
792
+ // When `restoreFromSnapshot` throws, the handler must NOT invalidate
793
+ // in-process caches — nothing new was written to disk, so the cached
794
+ // config/trust state still reflects reality. The SQLite handle reset is
795
+ // owned by `restoreFromSnapshot` and covered in
796
+ // src/backup/__tests__/restore.test.ts.
797
+ const snapshotPath = writeBackupFile(
798
+ LOCAL_DIR,
799
+ "backup-20260411-100000.vbundle",
800
+ );
801
+ mockBackupConfig = makeConfig({
802
+ localDirectory: LOCAL_DIR,
803
+ offsite: { enabled: true, destinations: [] },
804
+ });
805
+ mockRestoreError = new Error("simulated restore failure");
806
+
807
+ const res = await handleBackupRestore(
808
+ jsonRequest("POST", { path: snapshotPath }),
809
+ );
810
+ expect(res.status).toBe(500);
811
+ // Caches should NOT be invalidated on failure — the in-process caches
812
+ // still reflect the pre-restore state on disk (the bundle write failed
813
+ // so there's nothing new to re-read).
814
+ expect(mockInvalidateConfigCacheCalls).toBe(0);
815
+ expect(mockClearTrustCacheCalls).toBe(0);
816
+ });
817
+
818
+ test("response no longer exposes credentialsIncluded", async () => {
819
+ // The dead credentials plumbing has been removed from the backup surface.
820
+ // Credentials intentionally live in the OS keychain / CES and are not
821
+ // part of the backup round trip.
822
+ const snapshotPath = writeBackupFile(
823
+ LOCAL_DIR,
824
+ "backup-20260411-100000.vbundle",
825
+ );
826
+ mockBackupConfig = makeConfig({
827
+ localDirectory: LOCAL_DIR,
828
+ offsite: { enabled: true, destinations: [] },
829
+ });
830
+
831
+ const res = await handleBackupRestore(
832
+ jsonRequest("POST", { path: snapshotPath }),
833
+ );
834
+ expect(res.status).toBe(200);
835
+ const body = (await res.json()) as Record<string, unknown>;
836
+ expect("credentialsIncluded" in body).toBe(false);
837
+ expect(body.manifest).toBeDefined();
838
+ expect(body.restoredFiles).toBeDefined();
839
+ });
840
+
841
+ test("missing path field returns 400", async () => {
842
+ const res = await handleBackupRestore(jsonRequest("POST", {}));
843
+ expect(res.status).toBe(400);
844
+ const body = (await res.json()) as { error: { code: string; message: string } };
845
+ expect(body.error.code).toBe("BAD_REQUEST");
846
+ });
847
+
848
+ test("malformed JSON body returns 400", async () => {
849
+ const req = new Request("http://localhost/v1/backups/restore", {
850
+ method: "POST",
851
+ headers: { "Content-Type": "application/json" },
852
+ body: "{not-json",
853
+ });
854
+ const res = await handleBackupRestore(req);
855
+ expect(res.status).toBe(400);
856
+ });
857
+ });
858
+
859
+ // ---------------------------------------------------------------------------
860
+ // handleBackupVerify
861
+ // ---------------------------------------------------------------------------
862
+
863
+ describe("handleBackupVerify", () => {
864
+ test("corrupted bundle returns { valid: false }", async () => {
865
+ const snapshotPath = writeBackupFile(
866
+ LOCAL_DIR,
867
+ "backup-20260411-100000.vbundle",
868
+ "not-a-real-bundle",
869
+ );
870
+ mockBackupConfig = makeConfig({
871
+ localDirectory: LOCAL_DIR,
872
+ offsite: { enabled: true, destinations: [] },
873
+ });
874
+ mockVerifyResult = { valid: false, error: "bad checksum" };
875
+
876
+ const res = await handleBackupVerify(
877
+ jsonRequest("POST", { path: snapshotPath }),
878
+ );
879
+ expect(res.status).toBe(200);
880
+ const body = (await res.json()) as VerifyResult;
881
+ expect(body.valid).toBe(false);
882
+ expect(body.error).toBe("bad checksum");
883
+ expect(lastVerifyArgs!.hasKey).toBe(false);
884
+ });
885
+
886
+ test("valid plaintext bundle returns { valid: true } without loading key", async () => {
887
+ const snapshotPath = writeBackupFile(
888
+ LOCAL_DIR,
889
+ "backup-20260411-100000.vbundle",
890
+ );
891
+ mockBackupConfig = makeConfig({
892
+ localDirectory: LOCAL_DIR,
893
+ offsite: { enabled: true, destinations: [] },
894
+ });
895
+ mockReadBackupKeyCalls = 0;
896
+ mockVerifyResult = {
897
+ valid: true,
898
+ manifest: {
899
+ schema_version: "1.0",
900
+ created_at: "2026-04-11T10:00:00.000Z",
901
+ files: [],
902
+ manifest_sha256: "0".repeat(64),
903
+ } as unknown as VerifyResult["manifest"],
904
+ };
905
+
906
+ const res = await handleBackupVerify(
907
+ jsonRequest("POST", { path: snapshotPath }),
908
+ );
909
+ expect(res.status).toBe(200);
910
+ expect(mockReadBackupKeyCalls).toBe(0);
911
+ const body = (await res.json()) as VerifyResult;
912
+ expect(body.valid).toBe(true);
913
+ });
914
+
915
+ test("encrypted bundle with key loads key and forwards to verifySnapshot", async () => {
916
+ const snapshotPath = writeBackupFile(
917
+ LOCAL_DIR,
918
+ "backup-20260411-100000.vbundle.enc",
919
+ );
920
+ mockBackupConfig = makeConfig({
921
+ localDirectory: LOCAL_DIR,
922
+ offsite: { enabled: true, destinations: [] },
923
+ });
924
+ mockBackupKey = Buffer.alloc(32, 0xcc);
925
+ mockReadBackupKeyCalls = 0;
926
+
927
+ const res = await handleBackupVerify(
928
+ jsonRequest("POST", { path: snapshotPath }),
929
+ );
930
+ expect(res.status).toBe(200);
931
+ expect(mockReadBackupKeyCalls).toBe(1);
932
+ expect(lastVerifyArgs!.hasKey).toBe(true);
933
+ });
934
+
935
+ test("path outside allowed directories returns 400", async () => {
936
+ const outsidePath = join(ROOT, "elsewhere", "backup-20260411-100000.vbundle");
937
+ mkdirSync(join(ROOT, "elsewhere"), { recursive: true });
938
+ writeFileSync(outsidePath, "payload");
939
+ mockBackupConfig = makeConfig({
940
+ localDirectory: LOCAL_DIR,
941
+ offsite: { enabled: true, destinations: [] },
942
+ });
943
+
944
+ const res = await handleBackupVerify(
945
+ jsonRequest("POST", { path: outsidePath }),
946
+ );
947
+ expect(res.status).toBe(400);
948
+ expect(lastVerifyArgs).toBeNull();
949
+ });
950
+ });
951
+
952
+ // ---------------------------------------------------------------------------
953
+ // backupRouteDefinitions
954
+ // ---------------------------------------------------------------------------
955
+
956
+ describe("backupRouteDefinitions", () => {
957
+ test("registers four routes with the expected endpoint+method pairs", () => {
958
+ const defs = backupRouteDefinitions();
959
+ const pairs = defs.map((d) => `${d.method} ${d.endpoint}`).sort();
960
+ expect(pairs).toEqual([
961
+ "GET backups",
962
+ "POST backups/create",
963
+ "POST backups/restore",
964
+ "POST backups/verify",
965
+ ]);
966
+ });
967
+ });