@vellumai/assistant 0.7.2 → 0.7.3

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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -0,0 +1,224 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
4
+ import {
5
+ applyRuntimeInjections,
6
+ stripInjectionsForCompaction,
7
+ } from "../daemon/conversation-runtime-assembly.js";
8
+ import {
9
+ DEFAULT_INJECTOR_ORDER,
10
+ defaultInjectorsPlugin,
11
+ DISK_PRESSURE_WARNING_PROMPT,
12
+ } from "../plugins/defaults/injectors.js";
13
+ import {
14
+ registerPlugin,
15
+ resetPluginRegistryForTests,
16
+ } from "../plugins/registry.js";
17
+ import type { Injector, TurnContext } from "../plugins/types.js";
18
+ import type { Message } from "../providers/types.js";
19
+
20
+ function findInjector(name: string): Injector {
21
+ const injector = defaultInjectorsPlugin.injectors?.find(
22
+ (candidate) => candidate.name === name,
23
+ );
24
+ if (!injector) {
25
+ throw new Error(`injector '${name}' not registered`);
26
+ }
27
+ return injector;
28
+ }
29
+
30
+ function makeContext(overrides: Partial<TurnContext> = {}): TurnContext {
31
+ return {
32
+ requestId: "req-test",
33
+ conversationId: "conv-test",
34
+ turnIndex: 0,
35
+ trust: { sourceChannel: "vellum", trustClass: "guardian" },
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ function tailTexts(messages: Message[]): string[] {
41
+ const tail = messages[messages.length - 1];
42
+ if (!tail || tail.role !== "user") return [];
43
+ return tail.content
44
+ .filter((block): block is { type: "text"; text: string } => {
45
+ return block.type === "text";
46
+ })
47
+ .map((block) => block.text);
48
+ }
49
+
50
+ const diskPressureInjector = findInjector("disk-pressure-warning");
51
+ const cleanupContext = { cleanupModeActive: true };
52
+
53
+ describe("disk-pressure-warning injector", () => {
54
+ beforeEach(() => {
55
+ resetPluginRegistryForTests();
56
+ registerPlugin(defaultInjectorsPlugin);
57
+ _setOverridesForTesting({ "safe-storage-limits": true });
58
+ });
59
+
60
+ test("emits the exact cleanup prompt while safe storage limits are enabled", async () => {
61
+ const block = await diskPressureInjector.produce(
62
+ makeContext({
63
+ injectionInputs: { diskPressureContext: cleanupContext },
64
+ }),
65
+ );
66
+
67
+ expect(block).toEqual({
68
+ id: "disk-pressure-warning",
69
+ text: DISK_PRESSURE_WARNING_PROMPT,
70
+ placement: "prepend-user-tail",
71
+ });
72
+ expect(diskPressureInjector.order).toBe(
73
+ DEFAULT_INJECTOR_ORDER.diskPressureWarning,
74
+ );
75
+ expect(DISK_PRESSURE_WARNING_PROMPT).toBe(`<disk_pressure_warning>
76
+ Disk usage is critically low: this assistant is in storage cleanup mode because the workspace volume is at least 95% full.
77
+
78
+ In your first paragraph, warn the user that storage is critically low and that normal work is suspended until space is freed.
79
+
80
+ Then help the user clean up storage. Prefer safe inspection steps first, such as checking available space and finding large directories. Ask before deleting files or caches unless the user has already clearly approved the specific cleanup action.
81
+
82
+ Do not work on unrelated tasks until disk usage drops below the critical threshold or the user explicitly overrides the lock. Background processes and messages from trusted contacts are blocked while this cleanup mode is active.
83
+ </disk_pressure_warning>`);
84
+ });
85
+
86
+ test("omits the prompt when cleanup context is null or inactive", async () => {
87
+ await expect(
88
+ diskPressureInjector.produce(
89
+ makeContext({ injectionInputs: { diskPressureContext: null } }),
90
+ ),
91
+ ).resolves.toBeNull();
92
+
93
+ await expect(
94
+ diskPressureInjector.produce(
95
+ makeContext({
96
+ injectionInputs: {
97
+ diskPressureContext: { cleanupModeActive: false },
98
+ },
99
+ }),
100
+ ),
101
+ ).resolves.toBeNull();
102
+ });
103
+
104
+ test("omits the prompt when safe storage limits are disabled", async () => {
105
+ _setOverridesForTesting({ "safe-storage-limits": false });
106
+
107
+ await expect(
108
+ diskPressureInjector.produce(
109
+ makeContext({
110
+ injectionInputs: { diskPressureContext: cleanupContext },
111
+ }),
112
+ ),
113
+ ).resolves.toBeNull();
114
+ });
115
+
116
+ test("prepends ahead of workspace and unified turn context in full mode", async () => {
117
+ const runMessages: Message[] = [
118
+ { role: "user", content: [{ type: "text", text: "clean up space" }] },
119
+ ];
120
+ const workspace = "<workspace>\nRoot: /workspace\n</workspace>";
121
+ const turnContext = "<turn_context>\ninterface: macos\n</turn_context>";
122
+
123
+ const result = await applyRuntimeInjections(runMessages, {
124
+ turnContext: makeContext(),
125
+ diskPressureContext: cleanupContext,
126
+ workspaceTopLevelContext: workspace,
127
+ unifiedTurnContext: turnContext,
128
+ });
129
+
130
+ expect(tailTexts(result.messages).slice(0, 4)).toEqual([
131
+ DISK_PRESSURE_WARNING_PROMPT,
132
+ workspace,
133
+ turnContext,
134
+ "clean up space",
135
+ ]);
136
+ expect(
137
+ result.blocks.injectorChainBlock?.startsWith(
138
+ DISK_PRESSURE_WARNING_PROMPT,
139
+ ),
140
+ ).toBe(true);
141
+ });
142
+
143
+ test("survives minimal mode as safety-critical context", async () => {
144
+ const result = await applyRuntimeInjections(
145
+ [{ role: "user", content: [{ type: "text", text: "status" }] }],
146
+ {
147
+ turnContext: makeContext(),
148
+ mode: "minimal",
149
+ diskPressureContext: cleanupContext,
150
+ workspaceTopLevelContext: "<workspace>...</workspace>",
151
+ unifiedTurnContext: "<turn_context>...</turn_context>",
152
+ },
153
+ );
154
+
155
+ expect(tailTexts(result.messages)).toEqual([
156
+ DISK_PRESSURE_WARNING_PROMPT,
157
+ "<turn_context>...</turn_context>",
158
+ "status",
159
+ ]);
160
+ });
161
+
162
+ test("applies after Slack chronological transcript replacement", async () => {
163
+ const originalRun: Message[] = [
164
+ {
165
+ role: "user",
166
+ content: [{ type: "text", text: "latest raw user text" }],
167
+ },
168
+ ];
169
+ const slackTranscript: Message[] = [
170
+ {
171
+ role: "user",
172
+ content: [{ type: "text", text: "[12:00 user]: earlier" }],
173
+ },
174
+ {
175
+ role: "user",
176
+ content: [{ type: "text", text: "[12:01 @assistant]: cleanup?" }],
177
+ },
178
+ ];
179
+
180
+ const result = await applyRuntimeInjections(originalRun, {
181
+ turnContext: makeContext(),
182
+ diskPressureContext: cleanupContext,
183
+ channelCapabilities: {
184
+ channel: "slack",
185
+ dashboardCapable: false,
186
+ supportsDynamicUi: false,
187
+ supportsVoiceInput: false,
188
+ chatType: "channel",
189
+ },
190
+ slackChronologicalMessages: slackTranscript,
191
+ });
192
+
193
+ expect(result.messages).toHaveLength(2);
194
+ const texts = tailTexts(result.messages);
195
+ expect(texts[0]).toBe(DISK_PRESSURE_WARNING_PROMPT);
196
+ expect(
197
+ texts.some((text) => text.startsWith("<channel_capabilities>")),
198
+ ).toBe(true);
199
+ expect(texts[texts.length - 1]).toBe("[12:01 @assistant]: cleanup?");
200
+ });
201
+
202
+ test("compaction strip plus re-apply does not duplicate the warning", async () => {
203
+ const runMessages: Message[] = [
204
+ { role: "user", content: [{ type: "text", text: "find large files" }] },
205
+ ];
206
+
207
+ const first = await applyRuntimeInjections(runMessages, {
208
+ turnContext: makeContext(),
209
+ diskPressureContext: cleanupContext,
210
+ });
211
+ const stripped = stripInjectionsForCompaction(first.messages);
212
+ expect(tailTexts(stripped)).toEqual(["find large files"]);
213
+
214
+ const second = await applyRuntimeInjections(stripped, {
215
+ turnContext: makeContext(),
216
+ diskPressureContext: cleanupContext,
217
+ });
218
+ expect(
219
+ tailTexts(second.messages).filter(
220
+ (text) => text === DISK_PRESSURE_WARNING_PROMPT,
221
+ ),
222
+ ).toHaveLength(1);
223
+ });
224
+ });
@@ -125,6 +125,16 @@ describe("PUT /v1/config/llm/profiles/:name — managed profile guard", () => {
125
125
  ).toThrow(BadRequestError);
126
126
  });
127
127
 
128
+ test("allows edits to custom-balanced (user-owned)", () => {
129
+ savedRaw = null;
130
+ const result = replaceRoute.handler({
131
+ pathParams: { name: "custom-balanced" },
132
+ body: { provider: "openai", model: "gpt-4o" },
133
+ });
134
+ expect(result).toEqual({ ok: true });
135
+ expect(savedRaw).not.toBeNull();
136
+ });
137
+
128
138
  test("allows edits to a user-defined profile", () => {
129
139
  savedRaw = null;
130
140
  const result = replaceRoute.handler({
@@ -165,6 +175,14 @@ describe("PATCH /v1/config — managed profile deletion guard", () => {
165
175
  ).rejects.toThrow(BadRequestError);
166
176
  });
167
177
 
178
+ test("allows deletion of custom-balanced via null (user-owned)", async () => {
179
+ savedRaw = null;
180
+ const result = await patchRoute.handler({
181
+ body: { llm: { profiles: { "custom-balanced": null } } },
182
+ });
183
+ expect(result).toEqual({ ok: true });
184
+ });
185
+
168
186
  test("allows deletion of a user-defined profile via null", async () => {
169
187
  savedRaw = null;
170
188
  const result = await patchRoute.handler({
@@ -122,6 +122,136 @@ describe("MCP AbortSignal threading", () => {
122
122
  });
123
123
 
124
124
  describe("createMcpTool execute", () => {
125
+ test("keeps safe MCP tool names unchanged", () => {
126
+ const fakeManager = { callTool: jest.fn() } as any;
127
+
128
+ const tool = createMcpTool(
129
+ {
130
+ name: "my-tool",
131
+ description: "A test tool",
132
+ inputSchema: { type: "object", properties: {} },
133
+ },
134
+ "test-server",
135
+ {
136
+ transport: { type: "stdio", command: "echo", args: [] },
137
+ enabled: true,
138
+ defaultRiskLevel: "high",
139
+ maxTools: 100,
140
+ },
141
+ fakeManager,
142
+ );
143
+
144
+ expect(tool.name).toBe("mcp__test-server__my-tool");
145
+ expect(tool.getDefinition().name).toBe("mcp__test-server__my-tool");
146
+ });
147
+
148
+ test("keeps MCP tool names with trailing whitespace distinct", () => {
149
+ const fakeManager = { callTool: jest.fn() } as any;
150
+
151
+ const plain = createMcpTool(
152
+ {
153
+ name: "deploy",
154
+ description: "Deploy",
155
+ inputSchema: { type: "object", properties: {} },
156
+ },
157
+ "test-server",
158
+ {
159
+ transport: { type: "stdio", command: "echo", args: [] },
160
+ enabled: true,
161
+ defaultRiskLevel: "high",
162
+ maxTools: 100,
163
+ },
164
+ fakeManager,
165
+ );
166
+ const padded = createMcpTool(
167
+ {
168
+ name: "deploy ",
169
+ description: "Deploy padded",
170
+ inputSchema: { type: "object", properties: {} },
171
+ },
172
+ "test-server",
173
+ {
174
+ transport: { type: "stdio", command: "echo", args: [] },
175
+ enabled: true,
176
+ defaultRiskLevel: "high",
177
+ maxTools: 100,
178
+ },
179
+ fakeManager,
180
+ );
181
+
182
+ expect(plain.name).toBe("mcp__test-server__deploy");
183
+ expect(padded.name).toMatch(/^mcp__test-server__deploy__[a-f0-9]{12}$/);
184
+ expect(padded.name).not.toBe(plain.name);
185
+ });
186
+
187
+ test("exposes provider-safe MCP names while preserving raw execution names", async () => {
188
+ const callToolSpy = jest.fn().mockResolvedValue({
189
+ content: "tool result",
190
+ isError: false,
191
+ });
192
+ const fakeManager = { callTool: callToolSpy } as any;
193
+
194
+ const tool = createMcpTool(
195
+ {
196
+ name: "create link",
197
+ description: "Create a Stripe Link CLI resource",
198
+ inputSchema: { type: "object", properties: {} },
199
+ },
200
+ "stripe.link-cli",
201
+ {
202
+ transport: { type: "stdio", command: "echo", args: [] },
203
+ enabled: true,
204
+ defaultRiskLevel: "high",
205
+ maxTools: 100,
206
+ },
207
+ fakeManager,
208
+ );
209
+
210
+ expect(tool.name).toMatch(/^[a-zA-Z0-9_-]{1,64}$/);
211
+ expect(tool.name.startsWith("mcp__stripe_link-cli__create_link__")).toBe(
212
+ true,
213
+ );
214
+ expect(tool.getDefinition().name).toBe(tool.name);
215
+
216
+ await tool.execute(
217
+ { someArg: "value" },
218
+ {
219
+ workingDir: "/tmp",
220
+ conversationId: "conv-1",
221
+ trustClass: "guardian",
222
+ },
223
+ );
224
+
225
+ expect(callToolSpy).toHaveBeenCalledWith(
226
+ "stripe.link-cli",
227
+ "create link",
228
+ { someArg: "value" },
229
+ undefined,
230
+ );
231
+ });
232
+
233
+ test("caps long MCP names at the provider limit", () => {
234
+ const fakeManager = { callTool: jest.fn() } as any;
235
+ const tool = createMcpTool(
236
+ {
237
+ name: "x".repeat(180),
238
+ description: "A test tool",
239
+ inputSchema: { type: "object", properties: {} },
240
+ },
241
+ "server",
242
+ {
243
+ transport: { type: "stdio", command: "echo", args: [] },
244
+ enabled: true,
245
+ defaultRiskLevel: "high",
246
+ maxTools: 100,
247
+ },
248
+ fakeManager,
249
+ );
250
+
251
+ expect(tool.name).toHaveLength(64);
252
+ expect(tool.name).toMatch(/^[a-zA-Z0-9_-]{1,64}$/);
253
+ });
254
+
125
255
  test("threads context.signal through manager.callTool", async () => {
126
256
  const callToolSpy = jest.fn().mockResolvedValue({
127
257
  content: "tool result",
@@ -15,7 +15,6 @@ interface CapturedSearch {
15
15
 
16
16
  const capturedSearches: CapturedSearch[] = [];
17
17
  const getConfiguredProviderCalls: string[] = [];
18
- const scopeByConversation = new Map<string, string | undefined>();
19
18
  const testConfig = {} as AssistantConfig;
20
19
 
21
20
  mock.module("../config/loader.js", () => ({
@@ -56,13 +55,11 @@ mock.module("../memory/embedding-backend.js", () => ({
56
55
 
57
56
  mock.module("../memory/conversation-crud.js", () => ({
58
57
  addMessage: () => ({ id: "msg-1" }),
59
- createConversation: () => ({ id: "conv-1", memoryScopeId: "default" }),
58
+ createConversation: () => ({ id: "conv-1" }),
60
59
  deleteConversation: () => true,
61
60
  getAssistantMessageIdsInTurn: () => [],
62
61
  getConversation: () => null,
63
62
  getConversationHostAccess: () => false,
64
- getConversationMemoryScopeId: (conversationId: string) =>
65
- scopeByConversation.get(conversationId),
66
63
  getConversationOverrideProfile: () => undefined,
67
64
  getConversationSource: () => null,
68
65
  getMessageById: () => null,
@@ -165,12 +162,9 @@ describe("memory admin recall", () => {
165
162
  beforeEach(() => {
166
163
  capturedSearches.length = 0;
167
164
  getConfiguredProviderCalls.length = 0;
168
- scopeByConversation.clear();
169
165
  });
170
166
 
171
- test("uses the conversation memory scope and safe admin sources", async () => {
172
- scopeByConversation.set("conv-admin", "scope-admin");
173
-
167
+ test("uses safe admin sources", async () => {
174
168
  const result = await queryMemory("launch notes", "conv-admin");
175
169
 
176
170
  expect(capturedSearches).toHaveLength(1);
@@ -180,7 +174,6 @@ describe("memory admin recall", () => {
180
174
  });
181
175
  expect(capturedSearches[0].context).toMatchObject({
182
176
  workingDir: getWorkspaceDir(),
183
- memoryScopeId: "scope-admin",
184
177
  conversationId: "conv-admin",
185
178
  config: testConfig,
186
179
  });
@@ -202,13 +195,12 @@ describe("memory admin recall", () => {
202
195
  });
203
196
  });
204
197
 
205
- test("falls back to default scope without invoking a provider", async () => {
198
+ test("does not invoke a provider for deterministic recall", async () => {
206
199
  await queryMemory("offline recall", "missing-conversation");
207
200
 
208
201
  expect(capturedSearches).toHaveLength(1);
209
202
  expect(capturedSearches[0].context).toMatchObject({
210
203
  workingDir: getWorkspaceDir(),
211
- memoryScopeId: "default",
212
204
  conversationId: "missing-conversation",
213
205
  });
214
206
  expect(capturedSearches[0].input.sources).toEqual([
@@ -158,6 +158,27 @@ describe("runDefaultMemoryRetrieval", () => {
158
158
  expect(result.nowContent).toBe("now-default");
159
159
  });
160
160
 
161
+ test("propagates errors from prepareMemory rather than swallowing them", async () => {
162
+ // Memory is critical — failures must surface to the caller (the agent
163
+ // loop) rather than silently degrading to an empty memory block.
164
+ const failingPrepare = mock(
165
+ (
166
+ _msgs: Message[],
167
+ _cfg: AssistantConfig,
168
+ _signal: AbortSignal,
169
+ _onEvent: (msg: ServerMessage) => void,
170
+ ) => Promise.reject(new Error("retrieval failed")),
171
+ );
172
+ const graphMemory = {
173
+ prepareMemory: failingPrepare,
174
+ } as unknown as ConversationGraphMemory;
175
+ const deps = makeDeps({ graphMemory, isTrustedActor: true });
176
+
177
+ await expect(
178
+ runDefaultMemoryRetrieval(makeMemoryArgs(), deps),
179
+ ).rejects.toThrow("retrieval failed");
180
+ });
181
+
161
182
  test("passes through null PKB and NOW when the files are absent", async () => {
162
183
  readPkbContextMock.mockImplementation(() => null);
163
184
  readNowContextMock.mockImplementation(() => null);
@@ -314,7 +335,7 @@ describe("memoryRetrieval pipeline — default vs custom plugin", () => {
314
335
  (innerArgs: MemoryArgs) => runDefaultMemoryRetrieval(innerArgs, deps),
315
336
  args,
316
337
  makeTurnCtx(),
317
- 30, // tiny budget real production path uses 5_000ms
338
+ 30, // tiny pipeline budget to keep the test fast
318
339
  );
319
340
  } catch (err) {
320
341
  caught = err;
@@ -0,0 +1,180 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ normalizeOnboardingContext,
5
+ normalizeTasks,
6
+ normalizeTools,
7
+ TASK_DISPLAY_LABELS,
8
+ TOOL_DISPLAY_NAMES,
9
+ } from "../prompts/normalize-onboarding.js";
10
+ import type { OnboardingContext } from "../types/onboarding-context.js";
11
+
12
+ describe("normalizeTools", () => {
13
+ test("known tool IDs produce display labels", () => {
14
+ expect(normalizeTools(["github"])).toEqual(["GitHub"]);
15
+ expect(normalizeTools(["google-calendar"])).toEqual(["Google Calendar"]);
16
+ expect(normalizeTools(["slack"])).toEqual(["Slack"]);
17
+ expect(normalizeTools(["notion"])).toEqual(["Notion"]);
18
+ expect(normalizeTools(["linear"])).toEqual(["Linear"]);
19
+ expect(normalizeTools(["gmail"])).toEqual(["Gmail"]);
20
+ expect(normalizeTools(["google-drive"])).toEqual(["Google Drive"]);
21
+ expect(normalizeTools(["figma"])).toEqual(["Figma"]);
22
+ expect(normalizeTools(["jira"])).toEqual(["Jira"]);
23
+ expect(normalizeTools(["outlook"])).toEqual(["Outlook"]);
24
+ expect(normalizeTools(["excel"])).toEqual(["Excel"]);
25
+ expect(normalizeTools(["apple-notes"])).toEqual(["Apple Notes"]);
26
+ });
27
+
28
+ test("all known tool IDs from the client onboarding UI are mapped", () => {
29
+ const clientToolIds = [
30
+ "gmail",
31
+ "outlook",
32
+ "google-calendar",
33
+ "slack",
34
+ "notion",
35
+ "linear",
36
+ "jira",
37
+ "github",
38
+ "figma",
39
+ "google-drive",
40
+ "excel",
41
+ "apple-notes",
42
+ ];
43
+ expect(Object.keys(TOOL_DISPLAY_NAMES)).toEqual(
44
+ expect.arrayContaining(clientToolIds),
45
+ );
46
+ expect(Object.keys(TOOL_DISPLAY_NAMES)).toHaveLength(clientToolIds.length);
47
+ });
48
+
49
+ test("unknown/custom tool IDs pass through with first-letter capitalization", () => {
50
+ expect(normalizeTools(["trello"])).toEqual(["Trello"]);
51
+ expect(normalizeTools(["asana"])).toEqual(["Asana"]);
52
+ });
53
+
54
+ test("mixed known and unknown IDs normalize correctly", () => {
55
+ expect(normalizeTools(["github", "trello", "slack"])).toEqual([
56
+ "GitHub",
57
+ "Trello",
58
+ "Slack",
59
+ ]);
60
+ });
61
+
62
+ test("empty array produces empty array", () => {
63
+ expect(normalizeTools([])).toEqual([]);
64
+ });
65
+ });
66
+
67
+ describe("normalizeTasks", () => {
68
+ test("known task IDs produce plain-language labels", () => {
69
+ expect(normalizeTasks(["code-building"])).toEqual([
70
+ "builds code, apps, or tools",
71
+ ]);
72
+ expect(normalizeTasks(["writing"])).toEqual([
73
+ "writes docs, emails, or content",
74
+ ]);
75
+ expect(normalizeTasks(["research"])).toEqual([
76
+ "does research and analysis",
77
+ ]);
78
+ expect(normalizeTasks(["project-management"])).toEqual([
79
+ "plans and coordinates work",
80
+ ]);
81
+ expect(normalizeTasks(["scheduling"])).toEqual([
82
+ "handles meetings, calendar, and logistics",
83
+ ]);
84
+ expect(normalizeTasks(["personal"])).toEqual(["handles life admin"]);
85
+ });
86
+
87
+ test("all six known task IDs are mapped", () => {
88
+ const knownIds = [
89
+ "code-building",
90
+ "writing",
91
+ "research",
92
+ "project-management",
93
+ "scheduling",
94
+ "personal",
95
+ ];
96
+ expect(Object.keys(TASK_DISPLAY_LABELS)).toEqual(
97
+ expect.arrayContaining(knownIds),
98
+ );
99
+ expect(Object.keys(TASK_DISPLAY_LABELS)).toHaveLength(knownIds.length);
100
+ });
101
+
102
+ test("unknown/custom task IDs pass through unchanged", () => {
103
+ expect(normalizeTasks(["data-entry"])).toEqual(["data-entry"]);
104
+ expect(normalizeTasks(["custom-workflow"])).toEqual(["custom-workflow"]);
105
+ });
106
+
107
+ test("mixed known and unknown IDs normalize correctly", () => {
108
+ expect(normalizeTasks(["writing", "data-entry", "research"])).toEqual([
109
+ "writes docs, emails, or content",
110
+ "data-entry",
111
+ "does research and analysis",
112
+ ]);
113
+ });
114
+
115
+ test("empty array produces empty array", () => {
116
+ expect(normalizeTasks([])).toEqual([]);
117
+ });
118
+ });
119
+
120
+ describe("normalizeOnboardingContext", () => {
121
+ test("maps userName to preferredName", () => {
122
+ const ctx: OnboardingContext = {
123
+ tools: [],
124
+ tasks: [],
125
+ tone: "friendly",
126
+ userName: "Alice",
127
+ };
128
+ const result = normalizeOnboardingContext(ctx);
129
+ expect(result.preferredName).toBe("Alice");
130
+ });
131
+
132
+ test("absent userName yields undefined preferredName", () => {
133
+ const ctx: OnboardingContext = {
134
+ tools: [],
135
+ tasks: [],
136
+ tone: "professional",
137
+ };
138
+ const result = normalizeOnboardingContext(ctx);
139
+ expect(result.preferredName).toBeUndefined();
140
+ });
141
+
142
+ test("tone passes through", () => {
143
+ const ctx: OnboardingContext = {
144
+ tools: [],
145
+ tasks: [],
146
+ tone: "casual",
147
+ };
148
+ const result = normalizeOnboardingContext(ctx);
149
+ expect(result.tone).toBe("casual");
150
+ });
151
+
152
+ test("assistantName passes through", () => {
153
+ const ctx: OnboardingContext = {
154
+ tools: [],
155
+ tasks: [],
156
+ tone: "friendly",
157
+ assistantName: "Jarvis",
158
+ };
159
+ const result = normalizeOnboardingContext(ctx);
160
+ expect(result.assistantName).toBe("Jarvis");
161
+ });
162
+
163
+ test("normalizes tools and tasks together", () => {
164
+ const ctx: OnboardingContext = {
165
+ tools: ["github", "trello"],
166
+ tasks: ["code-building", "data-entry"],
167
+ tone: "professional",
168
+ userName: "Bob",
169
+ assistantName: "Friday",
170
+ };
171
+ const result = normalizeOnboardingContext(ctx);
172
+ expect(result).toEqual({
173
+ preferredName: "Bob",
174
+ commonWork: ["builds code, apps, or tools", "data-entry"],
175
+ dailyTools: ["GitHub", "Trello"],
176
+ tone: "professional",
177
+ assistantName: "Friday",
178
+ });
179
+ });
180
+ });