@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -45,7 +45,10 @@ mock.module("../security/secure-keys.js", () => ({
45
45
 
46
46
  // Mock global fetch
47
47
  const _originalFetch = globalThis.fetch;
48
- const mockFetch = async (input: string | URL | Request, init?: RequestInit) => {
48
+ const mockFetch = async (
49
+ input: string | URL | Request,
50
+ _init?: RequestInit,
51
+ ) => {
49
52
  const url =
50
53
  typeof input === "string"
51
54
  ? input
@@ -57,14 +60,6 @@ const mockFetch = async (input: string | URL | Request, init?: RequestInit) => {
57
60
  return new Response("Not Found", { status: 404 });
58
61
  }
59
62
 
60
- // Verify the Authorization header never leaks secrets into the request path
61
- const authHeader = init?.headers
62
- ? (init.headers as Record<string, string>)["Authorization"]
63
- : undefined;
64
- if (authHeader && !authHeader.startsWith("Api-Key ")) {
65
- throw new Error("Unexpected auth header format");
66
- }
67
-
68
63
  return new Response(JSON.stringify(entry.body), {
69
64
  status: entry.status,
70
65
  headers: { "Content-Type": "application/json" },
@@ -266,17 +261,17 @@ describe("fetchManagedCatalog", () => {
266
261
  expect(descriptor.handle).toBe("platform_oauth:conn_minimal");
267
262
  });
268
263
 
269
- test("error messages never contain API key values", async () => {
264
+ test("error messages never contain sensitive details", async () => {
270
265
  mockPlatformBaseUrl = "https://platform.example.com";
271
266
  mockAssistantApiKey = "sk-super-secret-key-12345";
272
267
  mockPlatformAssistantId = "ast-uuid-1234";
273
268
 
274
- // Simulate a network error
269
+ // Simulate a network error whose message contains sensitive data
275
270
  const savedFetch = globalThis.fetch;
276
271
  const errorFetch: typeof fetch = Object.assign(
277
272
  async () => {
278
273
  throw new Error(
279
- "Connect failed to https://platform.example.com/v1/assistants/ast-uuid-1234/oauth/managed/catalog/ with Api-Key sk-super-secret-key-12345",
274
+ "Connect failed to https://platform.example.com/v1/assistants/ast-uuid-1234/oauth/managed/catalog/ with Bearer sk-super-secret-key-12345",
280
275
  );
281
276
  },
282
277
  { preconnect: savedFetch.preconnect },
@@ -287,9 +282,12 @@ describe("fetchManagedCatalog", () => {
287
282
  const result = await fetchManagedCatalog();
288
283
  expect(result.ok).toBe(false);
289
284
  expect(result.error).toBeDefined();
290
- // Ensure the raw API key is not in the error message
285
+ // Raw error message (with URL, API key, etc.) must not leak
291
286
  expect(result.error).not.toContain("sk-super-secret-key-12345");
292
- expect(result.error).toContain("[REDACTED]");
287
+ expect(result.error).not.toContain("platform.example.com");
288
+ expect(result.error).not.toContain("Connect failed");
289
+ // Should only contain the error class name
290
+ expect(result.error).toContain("Error");
293
291
  } finally {
294
292
  globalThis.fetch = savedFetch;
295
293
  }
@@ -7,6 +7,11 @@ mock.module("../security/secure-keys.js", () => ({
7
7
  deleteSecureKeyAsync: jest.fn().mockResolvedValue("deleted"),
8
8
  }));
9
9
 
10
+ // Mock platform-callback-registration (imported by client.ts)
11
+ mock.module("../inbound/platform-callback-registration.js", () => ({
12
+ shouldUsePlatformCallbacks: jest.fn().mockReturnValue(false),
13
+ }));
14
+
10
15
  const { McpClient } = await import("../mcp/client.js");
11
16
  const { McpServerManager } = await import("../mcp/manager.js");
12
17
  const { createMcpTool } = await import("../tools/mcp/mcp-tool-factory.js");
@@ -7,6 +7,11 @@ mock.module("../security/secure-keys.js", () => ({
7
7
  deleteSecureKeyAsync: jest.fn().mockResolvedValue("deleted"),
8
8
  }));
9
9
 
10
+ // Mock platform-callback-registration (imported by client.ts)
11
+ mock.module("../inbound/platform-callback-registration.js", () => ({
12
+ shouldUsePlatformCallbacks: jest.fn().mockReturnValue(false),
13
+ }));
14
+
10
15
  const { McpClient } = await import("../mcp/client.js");
11
16
 
12
17
  /**
@@ -22,6 +22,7 @@ import { getDb, initializeDb } from "../memory/db.js";
22
22
  import {
23
23
  backfillMemoryRecallLogMessageId,
24
24
  getMemoryRecallLogByMessageIds,
25
+ normalizeTopCandidates,
25
26
  recordMemoryRecallLog,
26
27
  } from "../memory/memory-recall-log-store.js";
27
28
  import { memoryRecallLogs } from "../memory/schema.js";
@@ -61,6 +62,7 @@ describe("memory-recall-log-store", () => {
61
62
  topCandidatesJson: [{ id: "c1", score: 0.9 }],
62
63
  injectedText: "some memory context",
63
64
  reason: "user query matched memories",
65
+ queryContext: "what is the weather like",
64
66
  });
65
67
 
66
68
  backfillMemoryRecallLogMessageId(conversationId, messageId);
@@ -84,6 +86,34 @@ describe("memory-recall-log-store", () => {
84
86
  expect(result!.topCandidates).toEqual([{ id: "c1", score: 0.9 }]);
85
87
  expect(result!.injectedText).toBe("some memory context");
86
88
  expect(result!.reason).toBe("user query matched memories");
89
+ expect(result!.queryContext).toBe("what is the weather like");
90
+ });
91
+
92
+ test("queryContext defaults to null when omitted", () => {
93
+ const conversationId = "conv-no-query-ctx";
94
+ const messageId = "msg-no-query-ctx";
95
+
96
+ recordMemoryRecallLog({
97
+ conversationId,
98
+ enabled: true,
99
+ degraded: false,
100
+ semanticHits: 1,
101
+ mergedCount: 1,
102
+ selectedCount: 1,
103
+ tier1Count: 1,
104
+ tier2Count: 0,
105
+ hybridSearchLatencyMs: 50,
106
+ sparseVectorUsed: false,
107
+ injectedTokens: 100,
108
+ latencyMs: 80,
109
+ topCandidatesJson: [],
110
+ });
111
+
112
+ backfillMemoryRecallLogMessageId(conversationId, messageId);
113
+
114
+ const result = getMemoryRecallLogByMessageIds([messageId]);
115
+ expect(result).not.toBeNull();
116
+ expect(result!.queryContext).toBeNull();
87
117
  });
88
118
 
89
119
  test("returns null when no log exists for a messageId", () => {
@@ -148,4 +178,106 @@ describe("memory-recall-log-store", () => {
148
178
  expect(secondLog).not.toBeNull();
149
179
  expect(secondLog!.degraded).toBe(true);
150
180
  });
181
+
182
+ test("normalizes SSE-event format candidates to inspector format on read", () => {
183
+ const conversationId = "conv-normalize-sse";
184
+ const messageId = "msg-normalize-sse";
185
+
186
+ // Store candidates in SSE-event format (key/finalScore/semantic/recency/kind)
187
+ recordMemoryRecallLog({
188
+ conversationId,
189
+ enabled: true,
190
+ degraded: false,
191
+ semanticHits: 2,
192
+ mergedCount: 1,
193
+ selectedCount: 1,
194
+ tier1Count: 1,
195
+ tier2Count: 0,
196
+ hybridSearchLatencyMs: 100,
197
+ sparseVectorUsed: false,
198
+ injectedTokens: 200,
199
+ latencyMs: 120,
200
+ topCandidatesJson: [
201
+ {
202
+ key: "node-abc",
203
+ finalScore: 0.85,
204
+ semantic: 0.9,
205
+ recency: 0.1,
206
+ kind: "episode",
207
+ type: "episodic",
208
+ },
209
+ ],
210
+ });
211
+
212
+ backfillMemoryRecallLogMessageId(conversationId, messageId);
213
+
214
+ const result = getMemoryRecallLogByMessageIds([messageId]);
215
+ expect(result).not.toBeNull();
216
+ const candidates = result!.topCandidates as Array<Record<string, unknown>>;
217
+ expect(candidates).toHaveLength(1);
218
+ expect(candidates[0]).toEqual({
219
+ nodeId: "node-abc",
220
+ score: 0.85,
221
+ semanticSimilarity: 0.9,
222
+ recencyBoost: 0.1,
223
+ type: "episodic",
224
+ });
225
+ // kind should be stripped
226
+ expect(candidates[0]).not.toHaveProperty("kind");
227
+ // Old field names should not be present
228
+ expect(candidates[0]).not.toHaveProperty("key");
229
+ expect(candidates[0]).not.toHaveProperty("finalScore");
230
+ expect(candidates[0]).not.toHaveProperty("semantic");
231
+ expect(candidates[0]).not.toHaveProperty("recency");
232
+ });
233
+
234
+ test("passes through candidates already in inspector format unchanged", () => {
235
+ const conversationId = "conv-normalize-inspector";
236
+ const messageId = "msg-normalize-inspector";
237
+
238
+ // Store candidates already in inspector format (nodeId/score/semanticSimilarity/recencyBoost)
239
+ recordMemoryRecallLog({
240
+ conversationId,
241
+ enabled: true,
242
+ degraded: false,
243
+ semanticHits: 1,
244
+ mergedCount: 1,
245
+ selectedCount: 1,
246
+ tier1Count: 1,
247
+ tier2Count: 0,
248
+ hybridSearchLatencyMs: 80,
249
+ sparseVectorUsed: false,
250
+ injectedTokens: 100,
251
+ latencyMs: 90,
252
+ topCandidatesJson: [
253
+ {
254
+ nodeId: "node-xyz",
255
+ score: 0.92,
256
+ semanticSimilarity: 0.88,
257
+ recencyBoost: 0.05,
258
+ type: "semantic",
259
+ },
260
+ ],
261
+ });
262
+
263
+ backfillMemoryRecallLogMessageId(conversationId, messageId);
264
+
265
+ const result = getMemoryRecallLogByMessageIds([messageId]);
266
+ expect(result).not.toBeNull();
267
+ const candidates = result!.topCandidates as Array<Record<string, unknown>>;
268
+ expect(candidates).toHaveLength(1);
269
+ expect(candidates[0]).toEqual({
270
+ nodeId: "node-xyz",
271
+ score: 0.92,
272
+ semanticSimilarity: 0.88,
273
+ recencyBoost: 0.05,
274
+ type: "semantic",
275
+ });
276
+ });
277
+
278
+ test("normalizeTopCandidates handles non-array input", () => {
279
+ expect(normalizeTopCandidates(null)).toBeNull();
280
+ expect(normalizeTopCandidates("not-an-array")).toBe("not-an-array");
281
+ expect(normalizeTopCandidates(42)).toBe(42);
282
+ });
151
283
  });
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Integration tests for the streaming vbundle export builder.
3
+ *
4
+ * Tests cover:
5
+ * - Format parity: streaming builder produces archives identical to sync builder
6
+ * - SHA-256 integrity: manifest checksums match actual file content
7
+ * - Temp file lifecycle: cleanup removes the temp file
8
+ * - Cleanup idempotency: calling cleanup twice doesn't throw
9
+ * - Round-trip: streamExportVBundle -> validateVBundle succeeds
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { gunzipSync } from "node:zlib";
15
+ import { beforeAll, describe, expect, mock, test } from "bun:test";
16
+
17
+ const testDir = process.env.VELLUM_WORKSPACE_DIR!;
18
+ const testDbDir = join(testDir, "data", "db");
19
+ const testDbPath = join(testDbDir, "assistant.db");
20
+ const testConfigPath = join(testDir, "config.json");
21
+
22
+ mock.module("../util/logger.js", () => ({
23
+ getLogger: () =>
24
+ new Proxy({} as Record<string, unknown>, {
25
+ get: () => () => {},
26
+ }),
27
+ }));
28
+
29
+ mock.module("../permissions/trust-store.js", () => ({
30
+ getAllRules: () => [],
31
+ isStarterBundleAccepted: () => false,
32
+ clearCache: () => {},
33
+ }));
34
+
35
+ mock.module("../config/loader.js", () => ({
36
+ getConfig: () => ({
37
+ ui: {},
38
+ model: "test",
39
+ provider: "test",
40
+ memory: { enabled: false },
41
+ rateLimit: { maxRequestsPerMinute: 0 },
42
+ secretDetection: { enabled: false },
43
+ }),
44
+ }));
45
+
46
+ mock.module("../config/env.js", () => ({
47
+ isHttpAuthDisabled: () => true,
48
+ hasUngatedHttpAuthDisabled: () => false,
49
+ getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
50
+ getGatewayPort: () => 7830,
51
+ getRuntimeHttpPort: () => 7821,
52
+ getRuntimeHttpHost: () => "127.0.0.1",
53
+ getRuntimeGatewayOriginSecret: () => undefined,
54
+ getIngressPublicBaseUrl: () => undefined,
55
+ setIngressPublicBaseUrl: () => {},
56
+ }));
57
+
58
+ import {
59
+ buildExportVBundle,
60
+ streamExportVBundle,
61
+ } from "../runtime/migrations/vbundle-builder.js";
62
+ import { validateVBundle } from "../runtime/migrations/vbundle-validator.js";
63
+
64
+ // Test fixture data: a minimal SQLite header to simulate a real database file
65
+ const SQLITE_HEADER = new Uint8Array([
66
+ 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74,
67
+ 0x20, 0x33, 0x00,
68
+ ]);
69
+ const TEST_CONFIG = { provider: "anthropic", model: "test-model" };
70
+
71
+ // Generate a synthetic large file (~10 MB of repeated data)
72
+ const LARGE_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
73
+ const LARGE_FILE_NAME = "large-test-data.txt";
74
+
75
+ function generateLargeFileContent(): Buffer {
76
+ const line =
77
+ "This is a line of repeated test data for streaming export verification.\n";
78
+ const lineBuffer = Buffer.from(line);
79
+ const buf = Buffer.alloc(LARGE_FILE_SIZE);
80
+ for (let offset = 0; offset < LARGE_FILE_SIZE; offset += lineBuffer.length) {
81
+ const remaining = LARGE_FILE_SIZE - offset;
82
+ lineBuffer.copy(buf, offset, 0, Math.min(lineBuffer.length, remaining));
83
+ }
84
+ return buf;
85
+ }
86
+
87
+ beforeAll(() => {
88
+ // Write test fixture files so the export reads real data
89
+ mkdirSync(testDbDir, { recursive: true });
90
+ writeFileSync(testDbPath, SQLITE_HEADER);
91
+ writeFileSync(testConfigPath, JSON.stringify(TEST_CONFIG, null, 2));
92
+ writeFileSync(join(testDir, LARGE_FILE_NAME), generateLargeFileContent());
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Tar parsing helper (mirrors vbundle-validator's internal parser)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ interface TarEntry {
100
+ name: string;
101
+ data: Uint8Array;
102
+ }
103
+
104
+ function parseTarEntries(gzippedData: Uint8Array): TarEntry[] {
105
+ const tarData = gunzipSync(gzippedData);
106
+ const entries: TarEntry[] = [];
107
+ let offset = 0;
108
+ const BLOCK_SIZE = 512;
109
+
110
+ while (offset + BLOCK_SIZE <= tarData.length) {
111
+ const header = tarData.subarray(offset, offset + BLOCK_SIZE);
112
+ if (header.every((b) => b === 0)) break;
113
+
114
+ let end = 0;
115
+ while (end < 100 && header[end] !== 0) end++;
116
+ const name = new TextDecoder().decode(header.subarray(0, end));
117
+
118
+ let sizeEnd = 124;
119
+ while (sizeEnd < 136 && header[sizeEnd] !== 0) sizeEnd++;
120
+ const sizeStr = new TextDecoder().decode(header.subarray(124, sizeEnd));
121
+ const size = parseInt(sizeStr, 8) || 0;
122
+
123
+ const dataStart = offset + BLOCK_SIZE;
124
+ const data = tarData.subarray(dataStart, dataStart + size);
125
+ const dataBlocks = Math.ceil(size / BLOCK_SIZE);
126
+
127
+ if (header[156] === "0".charCodeAt(0) || header[156] === 0) {
128
+ entries.push({ name, data: new Uint8Array(data) });
129
+ }
130
+
131
+ offset = dataStart + dataBlocks * BLOCK_SIZE;
132
+ }
133
+
134
+ return entries;
135
+ }
136
+
137
+ function sha256Hex(data: Uint8Array): string {
138
+ return createHash("sha256").update(data).digest("hex");
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Streaming export tests
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe("streamExportVBundle", () => {
146
+ test("returns a temp file that exists on disk", async () => {
147
+ const result = await streamExportVBundle({
148
+ workspaceDir: testDir,
149
+ });
150
+
151
+ try {
152
+ expect(existsSync(result.tempPath)).toBe(true);
153
+ expect(result.size).toBeGreaterThan(0);
154
+ } finally {
155
+ await result.cleanup();
156
+ }
157
+ });
158
+
159
+ test("manifest contains correct SHA-256 checksums for all files", async () => {
160
+ const result = await streamExportVBundle({
161
+ workspaceDir: testDir,
162
+ });
163
+
164
+ try {
165
+ const archiveData = new Uint8Array(readFileSync(result.tempPath));
166
+ const entries = parseTarEntries(archiveData);
167
+
168
+ for (const fileEntry of result.manifest.files) {
169
+ const tarEntry = entries.find((e) => e.name === fileEntry.path);
170
+ expect(tarEntry).toBeDefined();
171
+ expect(sha256Hex(tarEntry!.data)).toBe(fileEntry.sha256);
172
+ }
173
+ } finally {
174
+ await result.cleanup();
175
+ }
176
+ });
177
+
178
+ test("decompressed archive produces same files as buildExportVBundle", async () => {
179
+ const streamResult = await streamExportVBundle({
180
+ workspaceDir: testDir,
181
+ });
182
+
183
+ try {
184
+ const syncResult = buildExportVBundle({
185
+ workspaceDir: testDir,
186
+ });
187
+
188
+ const streamArchiveData = new Uint8Array(
189
+ readFileSync(streamResult.tempPath),
190
+ );
191
+ const streamEntries = parseTarEntries(streamArchiveData);
192
+ const syncEntries = parseTarEntries(syncResult.archive);
193
+
194
+ // Filter out manifest.json — compare only data files
195
+ const streamFileEntries = streamEntries
196
+ .filter((e) => e.name !== "manifest.json")
197
+ .sort((a, b) => a.name.localeCompare(b.name));
198
+ const syncFileEntries = syncEntries
199
+ .filter((e) => e.name !== "manifest.json")
200
+ .sort((a, b) => a.name.localeCompare(b.name));
201
+
202
+ // Same set of files
203
+ expect(streamFileEntries.map((e) => e.name)).toEqual(
204
+ syncFileEntries.map((e) => e.name),
205
+ );
206
+
207
+ // Same content (verified via checksums)
208
+ for (let i = 0; i < streamFileEntries.length; i++) {
209
+ expect(sha256Hex(streamFileEntries[i].data)).toBe(
210
+ sha256Hex(syncFileEntries[i].data),
211
+ );
212
+ }
213
+
214
+ // Same manifest file entries (excluding timing fields)
215
+ const streamManifestFiles = streamResult.manifest.files
216
+ .slice()
217
+ .sort((a, b) => a.path.localeCompare(b.path));
218
+ const syncManifestFiles = syncResult.manifest.files
219
+ .slice()
220
+ .sort((a, b) => a.path.localeCompare(b.path));
221
+
222
+ expect(streamManifestFiles.map((f) => f.path)).toEqual(
223
+ syncManifestFiles.map((f) => f.path),
224
+ );
225
+ expect(streamManifestFiles.map((f) => f.sha256)).toEqual(
226
+ syncManifestFiles.map((f) => f.sha256),
227
+ );
228
+ expect(streamManifestFiles.map((f) => f.size)).toEqual(
229
+ syncManifestFiles.map((f) => f.size),
230
+ );
231
+ } finally {
232
+ await streamResult.cleanup();
233
+ }
234
+ });
235
+
236
+ test("cleanup removes the temp file", async () => {
237
+ const result = await streamExportVBundle({
238
+ workspaceDir: testDir,
239
+ });
240
+
241
+ expect(existsSync(result.tempPath)).toBe(true);
242
+ await result.cleanup();
243
+ expect(existsSync(result.tempPath)).toBe(false);
244
+ });
245
+ });
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Round-trip test
249
+ // ---------------------------------------------------------------------------
250
+
251
+ describe("streamExportVBundle round-trip", () => {
252
+ test("streaming archive passes validateVBundle with matching manifest", async () => {
253
+ const result = await streamExportVBundle({
254
+ workspaceDir: testDir,
255
+ });
256
+
257
+ try {
258
+ const archiveData = new Uint8Array(readFileSync(result.tempPath));
259
+ const validationResult = validateVBundle(archiveData);
260
+
261
+ expect(validationResult.is_valid).toBe(true);
262
+ expect(validationResult.errors).toHaveLength(0);
263
+ expect(validationResult.manifest).toBeDefined();
264
+
265
+ // Verify manifest fields match
266
+ expect(validationResult.manifest!.schema_version).toBe(
267
+ result.manifest.schema_version,
268
+ );
269
+ expect(validationResult.manifest!.manifest_sha256).toBe(
270
+ result.manifest.manifest_sha256,
271
+ );
272
+
273
+ // Verify file entries match
274
+ const validatedFiles = validationResult
275
+ .manifest!.files.slice()
276
+ .sort((a, b) => a.path.localeCompare(b.path));
277
+ const resultFiles = result.manifest.files
278
+ .slice()
279
+ .sort((a, b) => a.path.localeCompare(b.path));
280
+
281
+ expect(validatedFiles).toEqual(resultFiles);
282
+ } finally {
283
+ await result.cleanup();
284
+ }
285
+ });
286
+ });
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // Cleanup idempotency test
290
+ // ---------------------------------------------------------------------------
291
+
292
+ describe("streamExportVBundle cleanup", () => {
293
+ test("calling cleanup twice does not throw", async () => {
294
+ const result = await streamExportVBundle({
295
+ workspaceDir: testDir,
296
+ });
297
+
298
+ await result.cleanup();
299
+ // Second call should not throw
300
+ await result.cleanup();
301
+
302
+ expect(existsSync(result.tempPath)).toBe(false);
303
+ });
304
+ });
@@ -355,10 +355,10 @@ describe("handleMigrationImport", () => {
355
355
  expect(writtenData).toEqual(newDbData);
356
356
  });
357
357
 
358
- test("workspace is cleared before restore — files are created fresh", async () => {
359
- // Only new-format bundles (workspace/ prefix) trigger workspace clearing.
360
- // With clearing, existing files are removed before writing, so all files
361
- // in the bundle are "created" (not "overwritten") and no backups are made.
358
+ test("workspace is cleared before restore — preserved dirs are overwritten", async () => {
359
+ // New-format bundles (workspace/ prefix) trigger workspace clearing, but
360
+ // data/db/ is preserved during clearing to avoid destroying the database
361
+ // if the import fails partway. The DB file is overwritten (with backup).
362
362
  const newDbData = new Uint8Array([0x01, 0x02, 0x03]);
363
363
  const vbundle = createValidVBundle([
364
364
  { path: "workspace/data/db/assistant.db", data: newDbData },
@@ -373,17 +373,18 @@ describe("handleMigrationImport", () => {
373
373
  const body = (await res.json()) as ImportCommitResponse;
374
374
 
375
375
  expect(body.success).toBe(true);
376
- expect(body.summary.backups_created).toBe(0);
376
+ expect(body.summary.backups_created).toBe(1);
377
377
 
378
378
  const dbFile = body.files.find(
379
379
  (f) => f.path === "workspace/data/db/assistant.db",
380
380
  );
381
381
  expect(dbFile).toBeDefined();
382
- expect(dbFile!.action).toBe("created");
382
+ expect(dbFile!.action).toBe("overwritten");
383
383
  });
384
384
 
385
- test("reports all files as created after workspace clear", async () => {
386
- // New-format workspace/ paths trigger clearing both files are fresh.
385
+ test("workspace clear: preserved dirs overwritten, cleared dirs created", async () => {
386
+ // data/db/ is preserved during workspace clearing DB is "overwritten".
387
+ // config.json lives at the workspace root which IS cleared → "created".
387
388
  const newDbData = new Uint8Array([0xaa, 0xbb]);
388
389
  const newConfigData = new TextEncoder().encode('{"provider":"openai"}');
389
390
  const vbundle = createValidVBundle([
@@ -408,8 +409,8 @@ describe("handleMigrationImport", () => {
408
409
  (f) => f.path === "workspace/config.json",
409
410
  );
410
411
 
411
- // Both are "created" because workspace was cleared before writing
412
- expect(dbFile!.action).toBe("created");
412
+ // data/db/ preserved during clearing overwritten; config.json cleared created
413
+ expect(dbFile!.action).toBe("overwritten");
413
414
  expect(configFile!.action).toBe("created");
414
415
  });
415
416
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Global test utility for mocking `globalThis.fetch`.
3
+ *
4
+ * Usage:
5
+ * mockFetch("/some/path", { method: "POST" }, { body: { ok: true }, status: 200 });
6
+ *
7
+ * Entries are consumed in order — the first match wins and is removed, so
8
+ * register multiple responses for the same path to simulate sequences.
9
+ */
10
+
11
+ interface MockedResponse {
12
+ body?: unknown;
13
+ status: number;
14
+ }
15
+
16
+ interface MockFetchEntry {
17
+ path: string;
18
+ init: Partial<RequestInit>;
19
+ response: MockedResponse | Response;
20
+ }
21
+
22
+ const entries: MockFetchEntry[] = [];
23
+ const calls: { path: string; init: RequestInit }[] = [];
24
+ let originalFetch: typeof globalThis.fetch | undefined;
25
+
26
+ export function mockFetch(
27
+ path: string,
28
+ init: Partial<RequestInit>,
29
+ response: MockedResponse | Response,
30
+ ): void {
31
+ if (!originalFetch) {
32
+ originalFetch = globalThis.fetch;
33
+ }
34
+
35
+ entries.push({ path, init, response });
36
+
37
+ globalThis.fetch = (async (
38
+ input: RequestInfo | URL,
39
+ actualInit?: RequestInit,
40
+ ) => {
41
+ const url = String(input);
42
+ calls.push({ path: url, init: actualInit ?? {} });
43
+
44
+ const idx = entries.findIndex((e) => {
45
+ if (!url.includes(e.path)) return false;
46
+ for (const [key, val] of Object.entries(e.init)) {
47
+ if (
48
+ (actualInit as Record<string, unknown> | undefined)?.[key] !== val
49
+ ) {
50
+ return false;
51
+ }
52
+ }
53
+ return true;
54
+ });
55
+
56
+ if (idx === -1) {
57
+ return new Response(JSON.stringify({ detail: "No mock matched" }), {
58
+ status: 500,
59
+ });
60
+ }
61
+
62
+ const entry = entries[idx];
63
+ entries.splice(idx, 1);
64
+
65
+ if (entry.response instanceof Response) {
66
+ return entry.response;
67
+ }
68
+
69
+ return new Response(JSON.stringify(entry.response.body ?? null), {
70
+ status: entry.response.status,
71
+ headers: { "Content-Type": "application/json" },
72
+ });
73
+ }) as unknown as typeof globalThis.fetch;
74
+ }
75
+
76
+ export function getMockFetchCalls(): { path: string; init: RequestInit }[] {
77
+ return calls;
78
+ }
79
+
80
+ export function resetMockFetch(): void {
81
+ if (originalFetch) {
82
+ globalThis.fetch = originalFetch;
83
+ originalFetch = undefined;
84
+ }
85
+ entries.length = 0;
86
+ calls.length = 0;
87
+ }