@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
@@ -9,8 +9,20 @@ import { afterEach, describe, expect, mock, test } from "bun:test";
9
9
 
10
10
  const sentMessages: unknown[] = [];
11
11
  let mockHasClient = true; // Default to true for CU unified flow tests
12
- let mockCuClients: Array<{ clientId: string; capabilities: string[] }> = [
13
- { clientId: "mock-client-1", capabilities: ["host_cu"] },
12
+ // Default principal id used for both ctx.trustContext and clients in the
13
+ // existing single-user tests. Tests that exercise cross-user behaviour
14
+ // override this on individual clients and on the SurfaceConversationContext.
15
+ const DEFAULT_PRINCIPAL = "user-1";
16
+ let mockCuClients: Array<{
17
+ clientId: string;
18
+ capabilities: string[];
19
+ actorPrincipalId?: string;
20
+ }> = [
21
+ {
22
+ clientId: "mock-client-1",
23
+ capabilities: ["host_cu"],
24
+ actorPrincipalId: DEFAULT_PRINCIPAL,
25
+ },
14
26
  ];
15
27
 
16
28
  mock.module("../runtime/assistant-event-hub.js", () => ({
@@ -22,6 +34,8 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
22
34
  cap === "host_cu" ? mockCuClients : [],
23
35
  getClientById: (id: string) =>
24
36
  mockCuClients.find((c) => c.clientId === id) ?? null,
37
+ getActorPrincipalIdForClient: (id: string) =>
38
+ mockCuClients.find((c) => c.clientId === id)?.actorPrincipalId,
25
39
  },
26
40
  }));
27
41
 
@@ -38,12 +52,25 @@ type SurfaceConversationContext =
38
52
  /**
39
53
  * Build a minimal SurfaceConversationContext with optional hostCuProxy.
40
54
  * Only the fields required by the CU routing path are populated.
55
+ *
56
+ * `trustContext` defaults to a guardian context owned by `DEFAULT_PRINCIPAL`.
57
+ * Pass `null` to omit the field entirely (used to verify same-user
58
+ * enforcement when the conversation has no source actor principal).
41
59
  */
42
60
  function buildMockContext(
43
61
  hostCuProxy?: InstanceType<typeof HostCuProxy>,
62
+ trustGuardianPrincipalId: string | null = DEFAULT_PRINCIPAL,
44
63
  ): SurfaceConversationContext {
45
64
  return {
46
65
  conversationId: "test-session",
66
+ trustContext:
67
+ trustGuardianPrincipalId != null
68
+ ? {
69
+ sourceChannel: "vellum",
70
+ trustClass: "guardian",
71
+ guardianPrincipalId: trustGuardianPrincipalId,
72
+ }
73
+ : undefined,
47
74
  traceEmitter: { emit: () => {} },
48
75
  sendToClient: () => {},
49
76
  pendingSurfaceActions: new Map(),
@@ -72,7 +99,13 @@ describe("surfaceProxyResolver — CU tool routing", () => {
72
99
  function setupProxy(maxSteps?: number): SurfaceConversationContext {
73
100
  sentMessages.length = 0;
74
101
  mockHasClient = true;
75
- mockCuClients = [{ clientId: "mock-client-1", capabilities: ["host_cu"] }];
102
+ mockCuClients = [
103
+ {
104
+ clientId: "mock-client-1",
105
+ capabilities: ["host_cu"],
106
+ actorPrincipalId: DEFAULT_PRINCIPAL,
107
+ },
108
+ ];
76
109
  proxy = new HostCuProxy(maxSteps);
77
110
  return buildMockContext(proxy);
78
111
  }
@@ -375,11 +408,19 @@ describe("surfaceProxyResolver — CU tool routing", () => {
375
408
  // -------------------------------------------------------------------------
376
409
 
377
410
  describe("multi-client ambiguity guard", () => {
378
- test("returns error when multiple CU clients connected and no target_client_id given", async () => {
411
+ test("returns error when multiple same-user CU clients connected and no target_client_id given", async () => {
379
412
  const ctx = setupProxy();
380
413
  mockCuClients = [
381
- { clientId: "client-a", capabilities: ["host_cu"] },
382
- { clientId: "client-b", capabilities: ["host_cu"] },
414
+ {
415
+ clientId: "client-a",
416
+ capabilities: ["host_cu"],
417
+ actorPrincipalId: DEFAULT_PRINCIPAL,
418
+ },
419
+ {
420
+ clientId: "client-b",
421
+ capabilities: ["host_cu"],
422
+ actorPrincipalId: DEFAULT_PRINCIPAL,
423
+ },
383
424
  ];
384
425
 
385
426
  const result = await surfaceProxyResolver(ctx, "computer_use_click", {
@@ -397,8 +438,16 @@ describe("surfaceProxyResolver — CU tool routing", () => {
397
438
  test("proceeds when multiple clients connected and target_client_id is given", async () => {
398
439
  const ctx = setupProxy();
399
440
  mockCuClients = [
400
- { clientId: "client-a", capabilities: ["host_cu"] },
401
- { clientId: "client-b", capabilities: ["host_cu"] },
441
+ {
442
+ clientId: "client-a",
443
+ capabilities: ["host_cu"],
444
+ actorPrincipalId: DEFAULT_PRINCIPAL,
445
+ },
446
+ {
447
+ clientId: "client-b",
448
+ capabilities: ["host_cu"],
449
+ actorPrincipalId: DEFAULT_PRINCIPAL,
450
+ },
402
451
  ];
403
452
 
404
453
  const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", {
@@ -508,8 +557,18 @@ describe("surfaceProxyResolver — CU tool routing", () => {
508
557
  test("dispatches and records action when targetClientId is valid", async () => {
509
558
  const ctx = setupProxy();
510
559
  mockCuClients = [
511
- { clientId: "cu-client", capabilities: ["host_cu"] },
512
- { clientId: "client-b", capabilities: ["host_cu"] }, // would otherwise trip ambiguity guard
560
+ {
561
+ clientId: "cu-client",
562
+ capabilities: ["host_cu"],
563
+ actorPrincipalId: DEFAULT_PRINCIPAL,
564
+ },
565
+ // Second client present to ensure target_client_id resolves
566
+ // unambiguously and would otherwise trip the ambiguity guard.
567
+ {
568
+ clientId: "client-b",
569
+ capabilities: ["host_cu"],
570
+ actorPrincipalId: DEFAULT_PRINCIPAL,
571
+ },
513
572
  ];
514
573
 
515
574
  const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", {
@@ -532,6 +591,113 @@ describe("surfaceProxyResolver — CU tool routing", () => {
532
591
  });
533
592
  });
534
593
 
594
+ // -------------------------------------------------------------------------
595
+ // Same-user enforcement (dispatch layer)
596
+ //
597
+ // The proxy enforces this internally as well — these tests verify the
598
+ // dispatch performs the same-user rejection before the proxy is invoked
599
+ // (so no step is burned and no action history mutated), and uses the
600
+ // canonical rejection message.
601
+ // -------------------------------------------------------------------------
602
+
603
+ describe("same-user enforcement", () => {
604
+ test("rejects targeted CU dispatch from a different actor principal", async () => {
605
+ sentMessages.length = 0;
606
+ mockHasClient = true;
607
+ mockCuClients = [
608
+ {
609
+ clientId: "cu-client",
610
+ capabilities: ["host_cu"],
611
+ actorPrincipalId: "user-other",
612
+ },
613
+ ];
614
+ proxy = new HostCuProxy();
615
+ const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL);
616
+
617
+ const result = await surfaceProxyResolver(ctx, "computer_use_click", {
618
+ element_id: 1,
619
+ reasoning: "click",
620
+ target_client_id: "cu-client",
621
+ });
622
+
623
+ expect(result.isError).toBe(true);
624
+ expect(result.content).toContain(
625
+ "Submitting actor does not match the target client's actor",
626
+ );
627
+ // No state mutation, no dispatch.
628
+ expect(proxy.stepCount).toBe(0);
629
+ expect(proxy.actionHistory).toHaveLength(0);
630
+ expect(sentMessages).toHaveLength(0);
631
+ });
632
+
633
+ test("rejects when the conversation has no source actor principal", async () => {
634
+ sentMessages.length = 0;
635
+ mockHasClient = true;
636
+ mockCuClients = [
637
+ {
638
+ clientId: "cu-client",
639
+ capabilities: ["host_cu"],
640
+ actorPrincipalId: DEFAULT_PRINCIPAL,
641
+ },
642
+ ];
643
+ proxy = new HostCuProxy();
644
+ const ctx = buildMockContext(proxy, null);
645
+
646
+ const result = await surfaceProxyResolver(ctx, "computer_use_click", {
647
+ element_id: 1,
648
+ reasoning: "click",
649
+ target_client_id: "cu-client",
650
+ });
651
+
652
+ expect(result.isError).toBe(true);
653
+ expect(result.content).toContain(
654
+ "Submitting actor does not match the target client's actor",
655
+ );
656
+ expect(sentMessages).toHaveLength(0);
657
+ });
658
+
659
+ test("auto-resolves to the unique same-user CU client when cross-user clients are present", async () => {
660
+ // Regression: previously the dispatch counted only same-user clients
661
+ // for the multi-client guard, so 1 same-user + 1 cross-user passed the
662
+ // guard with no targetClientId — and the proxy then broadcast to ALL
663
+ // host_cu subscribers, including the cross-user one.
664
+ sentMessages.length = 0;
665
+ mockHasClient = true;
666
+ mockCuClients = [
667
+ {
668
+ clientId: "cu-mine",
669
+ capabilities: ["host_cu"],
670
+ actorPrincipalId: DEFAULT_PRINCIPAL,
671
+ },
672
+ {
673
+ clientId: "cu-other",
674
+ capabilities: ["host_cu"],
675
+ actorPrincipalId: "user-other",
676
+ },
677
+ ];
678
+ proxy = new HostCuProxy();
679
+ const ctx = buildMockContext(proxy, DEFAULT_PRINCIPAL);
680
+
681
+ const resultPromise = surfaceProxyResolver(ctx, "computer_use_click", {
682
+ element_id: 1,
683
+ reasoning: "click",
684
+ // Intentionally no target_client_id — exercises auto-resolve.
685
+ });
686
+
687
+ // Broadcast happens, but with the same-user clientId set so only
688
+ // that client receives it.
689
+ expect(sentMessages).toHaveLength(1);
690
+ const sent = sentMessages[0] as Record<string, unknown>;
691
+ expect(sent.targetClientId).toBe("cu-mine");
692
+
693
+ // Manually resolve to clean up the pending promise.
694
+ proxy.processObservation(sent.requestId as string, {
695
+ executionResult: "ok",
696
+ });
697
+ await resultPromise;
698
+ });
699
+ });
700
+
535
701
  describe("step limit enforcement through resolver", () => {
536
702
  test("rejects action tools when step limit exceeded", async () => {
537
703
  const ctx = setupProxy(2); // maxSteps = 2
@@ -1,8 +1,11 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import { UiConfigSchema } from "../config/schemas/platform.js";
3
4
  import {
5
+ canonicalizeTimeZone,
4
6
  extractUserTimeZoneFromRecall,
5
7
  formatTurnTimestamp,
8
+ resolveTurnTimezoneContext,
6
9
  } from "../daemon/date-context.js";
7
10
 
8
11
  // ---------------------------------------------------------------------------
@@ -85,6 +88,156 @@ describe("extractUserTimeZoneFromRecall", () => {
85
88
  });
86
89
  });
87
90
 
91
+ // ---------------------------------------------------------------------------
92
+ // UiConfigSchema timezone fields
93
+ // ---------------------------------------------------------------------------
94
+
95
+ describe("UiConfigSchema timezone fields", () => {
96
+ test("accepts canonicalizable IANA timezone identifiers", () => {
97
+ const result = UiConfigSchema.parse({
98
+ userTimezone: "america/new_york",
99
+ detectedTimezone: "america/los_angeles",
100
+ });
101
+
102
+ expect(result.userTimezone).toBe("America/New_York");
103
+ expect(result.detectedTimezone).toBe("America/Los_Angeles");
104
+ expect(UiConfigSchema.parse({ userTimezone: "UTC" }).userTimezone).toBe(
105
+ "UTC",
106
+ );
107
+ });
108
+
109
+ test("accepts empty-string clearing sentinels", () => {
110
+ const result = UiConfigSchema.parse({
111
+ userTimezone: "",
112
+ detectedTimezone: "",
113
+ });
114
+
115
+ expect(result.userTimezone).toBe("");
116
+ expect(result.detectedTimezone).toBe("");
117
+ });
118
+
119
+ test("rejects invalid non-empty userTimezone and detectedTimezone values", () => {
120
+ expect(() =>
121
+ UiConfigSchema.parse({ userTimezone: "not-a-timezone" }),
122
+ ).toThrow("ui.userTimezone must be a valid IANA timezone identifier");
123
+ expect(() =>
124
+ UiConfigSchema.parse({ detectedTimezone: "Mars/Olympus_Mons" }),
125
+ ).toThrow("ui.detectedTimezone must be a valid IANA timezone identifier");
126
+ expect(() => UiConfigSchema.parse({ userTimezone: "+05:30" })).toThrow(
127
+ "ui.userTimezone must be a valid IANA timezone identifier",
128
+ );
129
+ });
130
+
131
+ test("rejects ambiguous abbreviations and offset strings", () => {
132
+ for (const value of ["EST", "PST", "UTC+5:30", "GMT-0800", "+05:30"]) {
133
+ expect(() => UiConfigSchema.parse({ userTimezone: value })).toThrow(
134
+ "ui.userTimezone must be a valid IANA timezone identifier",
135
+ );
136
+ expect(() => UiConfigSchema.parse({ detectedTimezone: value })).toThrow(
137
+ "ui.detectedTimezone must be a valid IANA timezone identifier",
138
+ );
139
+ }
140
+ });
141
+ });
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // canonicalizeTimeZone
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe("canonicalizeTimeZone", () => {
148
+ test("returns canonical timezone identifiers and ignores empty values", () => {
149
+ expect(canonicalizeTimeZone("america/new_york")).toBe("America/New_York");
150
+ expect(canonicalizeTimeZone("")).toBeNull();
151
+ expect(canonicalizeTimeZone(null)).toBeNull();
152
+ expect(canonicalizeTimeZone("not-a-timezone")).toBeNull();
153
+ });
154
+ });
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // resolveTurnTimezoneContext
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe("resolveTurnTimezoneContext", () => {
161
+ test("prefers configured user timezone over automatic sources", () => {
162
+ const context = resolveTurnTimezoneContext({
163
+ configuredUserTimeZone: "America/New_York",
164
+ clientTimezone: "America/Chicago",
165
+ detectedTimezone: "Asia/Tokyo",
166
+ hostTimeZone: "Europe/London",
167
+ });
168
+
169
+ expect(context.effectiveTimezone).toBe("America/New_York");
170
+ expect(context.source).toBe("configuredUserTimezone");
171
+ });
172
+
173
+ test("prefers client timezone over detected and host timezones", () => {
174
+ const context = resolveTurnTimezoneContext({
175
+ clientTimezone: "America/Chicago",
176
+ detectedTimezone: "Asia/Tokyo",
177
+ hostTimeZone: "Europe/London",
178
+ });
179
+
180
+ expect(context.effectiveTimezone).toBe("America/Chicago");
181
+ expect(context.source).toBe("clientTimezone");
182
+ });
183
+
184
+ test("prefers detected timezone over host timezone", () => {
185
+ const context = resolveTurnTimezoneContext({
186
+ detectedTimezone: "Asia/Tokyo",
187
+ hostTimeZone: "Europe/London",
188
+ });
189
+
190
+ expect(context.effectiveTimezone).toBe("Asia/Tokyo");
191
+ expect(context.source).toBe("detectedTimezone");
192
+ });
193
+
194
+ test("uses host timezone before UTC fallback", () => {
195
+ const context = resolveTurnTimezoneContext({
196
+ hostTimeZone: "Europe/London",
197
+ });
198
+
199
+ expect(context.effectiveTimezone).toBe("Europe/London");
200
+ expect(context.source).toBe("hostTimezone");
201
+ });
202
+
203
+ test("falls back to UTC when no timezone resolves", () => {
204
+ const context = resolveTurnTimezoneContext({
205
+ configuredUserTimeZone: "not-a-timezone",
206
+ clientTimezone: "also-invalid",
207
+ detectedTimezone: "",
208
+ hostTimeZone: "still-invalid",
209
+ });
210
+
211
+ expect(context.effectiveTimezone).toBe("UTC");
212
+ expect(context.source).toBe("utcFallback");
213
+ });
214
+
215
+ test("ignores empty strings during runtime resolution", () => {
216
+ const context = resolveTurnTimezoneContext({
217
+ configuredUserTimeZone: "",
218
+ clientTimezone: "",
219
+ detectedTimezone: "",
220
+ hostTimeZone: "UTC",
221
+ });
222
+
223
+ expect(context.configuredUserTimezone).toBeNull();
224
+ expect(context.clientTimezone).toBeNull();
225
+ expect(context.detectedTimezone).toBeNull();
226
+ expect(context.effectiveTimezone).toBe("UTC");
227
+ expect(context.source).toBe("hostTimezone");
228
+ });
229
+
230
+ test("does not use recalled profile timezone in normal turn precedence", () => {
231
+ const context = resolveTurnTimezoneContext({
232
+ userTimeZone: "Asia/Tokyo",
233
+ hostTimeZone: "UTC",
234
+ });
235
+
236
+ expect(context.effectiveTimezone).toBe("UTC");
237
+ expect(context.source).toBe("hostTimezone");
238
+ });
239
+ });
240
+
88
241
  // ---------------------------------------------------------------------------
89
242
  // formatTurnTimestamp
90
243
  // ---------------------------------------------------------------------------
@@ -127,11 +280,20 @@ describe("formatTurnTimestamp", () => {
127
280
  expect(result).toBe("2026-04-02 (Thursday) 06:52:33 +00:00 (UTC)");
128
281
  });
129
282
 
130
- test("handles user timezone override", () => {
283
+ test("handles configured user timezone override", () => {
131
284
  const result = formatTurnTimestamp({
132
285
  nowMs: THU_APR_02_0652,
133
286
  hostTimeZone: "UTC",
134
- userTimeZone: "Asia/Tokyo",
287
+ configuredUserTimeZone: "Asia/Tokyo",
288
+ });
289
+ expect(result).toBe("2026-04-02 (Thursday) 15:52:33 +09:00 (Asia/Tokyo)");
290
+ });
291
+
292
+ test("uses client timezone when no configured override exists", () => {
293
+ const result = formatTurnTimestamp({
294
+ nowMs: THU_APR_02_0652,
295
+ hostTimeZone: "UTC",
296
+ clientTimezone: "Asia/Tokyo",
135
297
  });
136
298
  expect(result).toBe("2026-04-02 (Thursday) 15:52:33 +09:00 (Asia/Tokyo)");
137
299
  });
@@ -0,0 +1,262 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { DiskPressureTransitionResult } from "../daemon/disk-pressure-guard.js";
4
+ import type { DiskUsageInfo } from "../util/disk-usage.js";
5
+
6
+ let diskSample: DiskUsageInfo | null = null;
7
+ let diskSampleError: unknown = null;
8
+ let diskSampleCalls = 0;
9
+
10
+ mock.module("../config/loader.js", () => ({
11
+ getConfig: () => ({}),
12
+ }));
13
+
14
+ mock.module("../util/disk-usage.js", () => ({
15
+ getDiskUsageInfo: () => {
16
+ diskSampleCalls += 1;
17
+ if (diskSampleError) throw diskSampleError;
18
+ return diskSample;
19
+ },
20
+ }));
21
+
22
+ mock.module("../runtime/assistant-event.js", () => ({
23
+ buildAssistantEvent: (message: unknown, conversationId?: string) => ({
24
+ id: "event-test",
25
+ type: "message",
26
+ timestamp: new Date().toISOString(),
27
+ conversationId,
28
+ message,
29
+ }),
30
+ }));
31
+
32
+ mock.module("../runtime/assistant-event-hub.js", () => ({
33
+ AssistantEventHub: class {},
34
+ broadcastMessage: () => {},
35
+ capabilityForMessageType: () => undefined,
36
+ assistantEventHub: {
37
+ publish: async () => {},
38
+ },
39
+ }));
40
+
41
+ const { _setOverridesForTesting } =
42
+ await import("../config/assistant-feature-flags.js");
43
+ const {
44
+ DISK_PRESSURE_OVERRIDE_CONFIRMATION,
45
+ DISK_PRESSURE_THRESHOLD_PERCENT,
46
+ __getDiskPressureGuardTimerForTests,
47
+ __resetDiskPressureGuardForTests,
48
+ acknowledgeDiskPressureLock,
49
+ evaluateDiskPressureNow,
50
+ getDiskPressureStatus,
51
+ overrideDiskPressureLock,
52
+ startDiskPressureGuard,
53
+ stopDiskPressureGuard,
54
+ } = await import("../daemon/disk-pressure-guard.js");
55
+
56
+ function setFeatureFlag(enabled: boolean): void {
57
+ _setOverridesForTesting({ "safe-storage-limits": enabled });
58
+ }
59
+
60
+ function setDiskUsage(usedMb: number, totalMb = 100): void {
61
+ diskSample = {
62
+ path: "/workspace",
63
+ totalMb,
64
+ usedMb,
65
+ freeMb: Math.max(0, totalMb - usedMb),
66
+ };
67
+ diskSampleError = null;
68
+ }
69
+
70
+ function expectRejected(
71
+ result: DiskPressureTransitionResult,
72
+ reason: Exclude<DiskPressureTransitionResult, { ok: true }>["reason"],
73
+ ): asserts result is Exclude<DiskPressureTransitionResult, { ok: true }> {
74
+ expect(result.ok).toBe(false);
75
+ if (result.ok) {
76
+ throw new Error("Expected disk pressure transition to be rejected");
77
+ }
78
+ expect(result.reason).toBe(reason);
79
+ }
80
+
81
+ beforeEach(() => {
82
+ __resetDiskPressureGuardForTests();
83
+ setFeatureFlag(true);
84
+ setDiskUsage(10);
85
+ diskSampleCalls = 0;
86
+ });
87
+
88
+ afterEach(() => {
89
+ __resetDiskPressureGuardForTests();
90
+ _setOverridesForTesting({});
91
+ diskSample = null;
92
+ diskSampleError = null;
93
+ diskSampleCalls = 0;
94
+ });
95
+
96
+ describe("disk pressure guard", () => {
97
+ test("returns a stable disabled status without sampling when the flag is disabled", () => {
98
+ setDiskUsage(99);
99
+ setFeatureFlag(false);
100
+
101
+ const status = evaluateDiskPressureNow();
102
+
103
+ expect(status.enabled).toBe(false);
104
+ expect(status.state).toBe("disabled");
105
+ expect(status.locked).toBe(false);
106
+ expect(status.effectivelyLocked).toBe(false);
107
+ expect(status.usagePercent).toBeNull();
108
+ expect(diskSampleCalls).toBe(0);
109
+ expect(getDiskPressureStatus()).toEqual(status);
110
+ });
111
+
112
+ test("locks when sampled usage reaches the threshold", () => {
113
+ setDiskUsage(DISK_PRESSURE_THRESHOLD_PERCENT);
114
+
115
+ const status = evaluateDiskPressureNow();
116
+
117
+ expect(status.enabled).toBe(true);
118
+ expect(status.state).toBe("critical");
119
+ expect(status.locked).toBe(true);
120
+ expect(status.acknowledged).toBe(false);
121
+ expect(status.overrideActive).toBe(false);
122
+ expect(status.effectivelyLocked).toBe(true);
123
+ expect(status.lockId).toBeTruthy();
124
+ expect(status.usagePercent).toBe(DISK_PRESSURE_THRESHOLD_PERCENT);
125
+ expect(status.thresholdPercent).toBe(DISK_PRESSURE_THRESHOLD_PERCENT);
126
+ expect(status.path).toBe("/workspace");
127
+ expect(status.lastCheckedAt).toBeTruthy();
128
+ expect(status.blockedCapabilities.length).toBeGreaterThan(0);
129
+ });
130
+
131
+ test("acknowledges an active lock without overriding it", () => {
132
+ setDiskUsage(99);
133
+ evaluateDiskPressureNow();
134
+
135
+ const result = acknowledgeDiskPressureLock();
136
+
137
+ expect(result.ok).toBe(true);
138
+ expect(result.status.acknowledged).toBe(true);
139
+ expect(result.status.overrideActive).toBe(false);
140
+ expect(result.status.effectivelyLocked).toBe(true);
141
+ });
142
+
143
+ test("unlocks and clears acknowledgement and override when usage falls below threshold", () => {
144
+ setDiskUsage(99);
145
+ evaluateDiskPressureNow();
146
+ acknowledgeDiskPressureLock();
147
+ overrideDiskPressureLock(DISK_PRESSURE_OVERRIDE_CONFIRMATION);
148
+
149
+ setDiskUsage(20);
150
+ const status = evaluateDiskPressureNow();
151
+
152
+ expect(status.state).toBe("ok");
153
+ expect(status.locked).toBe(false);
154
+ expect(status.acknowledged).toBe(false);
155
+ expect(status.overrideActive).toBe(false);
156
+ expect(status.effectivelyLocked).toBe(false);
157
+ expect(status.lockId).toBeNull();
158
+ expect(status.blockedCapabilities).toEqual([]);
159
+ });
160
+
161
+ test("overrides an active lock only with the exact confirmation after trimming whitespace", () => {
162
+ setDiskUsage(99);
163
+ evaluateDiskPressureNow();
164
+
165
+ const invalid = overrideDiskPressureLock("I accept the risks");
166
+ expectRejected(invalid, "invalid_confirmation");
167
+ expect(invalid.status.effectivelyLocked).toBe(true);
168
+
169
+ const valid = overrideDiskPressureLock(
170
+ ` ${DISK_PRESSURE_OVERRIDE_CONFIRMATION} `,
171
+ );
172
+
173
+ expect(valid.ok).toBe(true);
174
+ expect(valid.status.locked).toBe(true);
175
+ expect(valid.status.overrideActive).toBe(true);
176
+ expect(valid.status.effectivelyLocked).toBe(false);
177
+ });
178
+
179
+ test("rejects acknowledgement when no lock is active", () => {
180
+ setDiskUsage(10);
181
+ evaluateDiskPressureNow();
182
+
183
+ const result = acknowledgeDiskPressureLock();
184
+
185
+ expectRejected(result, "not_locked");
186
+ expect(result.status.locked).toBe(false);
187
+ });
188
+
189
+ test("rejects override when no lock is active", () => {
190
+ setDiskUsage(10);
191
+ evaluateDiskPressureNow();
192
+
193
+ const result = overrideDiskPressureLock(
194
+ DISK_PRESSURE_OVERRIDE_CONFIRMATION,
195
+ );
196
+
197
+ expectRejected(result, "not_locked");
198
+ expect(result.status.locked).toBe(false);
199
+ });
200
+
201
+ test("rejects repeated override while preserving the existing override", () => {
202
+ setDiskUsage(99);
203
+ evaluateDiskPressureNow();
204
+ const first = overrideDiskPressureLock(DISK_PRESSURE_OVERRIDE_CONFIRMATION);
205
+ expect(first.ok).toBe(true);
206
+
207
+ const second = overrideDiskPressureLock(
208
+ DISK_PRESSURE_OVERRIDE_CONFIRMATION,
209
+ );
210
+
211
+ expectRejected(second, "already_overridden");
212
+ expect(second.status.overrideActive).toBe(true);
213
+ expect(second.status.effectivelyLocked).toBe(false);
214
+ });
215
+
216
+ test("sample failures degrade open and do not preserve a prior lock", () => {
217
+ setDiskUsage(99);
218
+ evaluateDiskPressureNow();
219
+ expect(getDiskPressureStatus().locked).toBe(true);
220
+
221
+ diskSampleError = new Error("sample failed");
222
+ const status = evaluateDiskPressureNow();
223
+
224
+ expect(status.enabled).toBe(true);
225
+ expect(status.state).toBe("unknown");
226
+ expect(status.locked).toBe(false);
227
+ expect(status.effectivelyLocked).toBe(false);
228
+ expect(status.error).toBe("sample failed");
229
+ expect(status.lastCheckedAt).toBeTruthy();
230
+ });
231
+
232
+ test("timer start and stop are idempotent", () => {
233
+ expect(__getDiskPressureGuardTimerForTests()).toBeNull();
234
+
235
+ startDiskPressureGuard();
236
+ const firstTimer = __getDiskPressureGuardTimerForTests();
237
+ expect(firstTimer).toBeTruthy();
238
+
239
+ startDiskPressureGuard();
240
+ expect(__getDiskPressureGuardTimerForTests()).toBe(firstTimer);
241
+
242
+ stopDiskPressureGuard();
243
+ expect(__getDiskPressureGuardTimerForTests()).toBeNull();
244
+
245
+ stopDiskPressureGuard();
246
+ expect(__getDiskPressureGuardTimerForTests()).toBeNull();
247
+ });
248
+
249
+ test("disabling the flag clears an active timer and lock", () => {
250
+ setDiskUsage(99);
251
+ evaluateDiskPressureNow();
252
+ startDiskPressureGuard();
253
+ expect(__getDiskPressureGuardTimerForTests()).toBeTruthy();
254
+
255
+ setFeatureFlag(false);
256
+ const status = evaluateDiskPressureNow();
257
+
258
+ expect(status.enabled).toBe(false);
259
+ expect(status.locked).toBe(false);
260
+ expect(__getDiskPressureGuardTimerForTests()).toBeNull();
261
+ });
262
+ });