@vellumai/assistant 0.4.17 → 0.4.18

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 (515) hide show
  1. package/eslint.config.mjs +2 -2
  2. package/package.json +1 -1
  3. package/src/__tests__/access-request-decision.test.ts +128 -120
  4. package/src/__tests__/account-registry.test.ts +121 -110
  5. package/src/__tests__/active-skill-tools.test.ts +200 -172
  6. package/src/__tests__/actor-token-service.test.ts +341 -274
  7. package/src/__tests__/agent-loop-thinking.test.ts +28 -19
  8. package/src/__tests__/agent-loop.test.ts +798 -378
  9. package/src/__tests__/anthropic-provider.test.ts +405 -247
  10. package/src/__tests__/app-builder-tool-scripts.test.ts +97 -97
  11. package/src/__tests__/app-bundler.test.ts +112 -79
  12. package/src/__tests__/app-executors.test.ts +205 -178
  13. package/src/__tests__/app-git-history.test.ts +90 -73
  14. package/src/__tests__/app-git-service.test.ts +67 -53
  15. package/src/__tests__/app-open-proxy.test.ts +29 -25
  16. package/src/__tests__/approval-conversation-turn.test.ts +100 -81
  17. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +45 -17
  18. package/src/__tests__/approval-message-composer.test.ts +119 -119
  19. package/src/__tests__/approval-primitive.test.ts +264 -233
  20. package/src/__tests__/approval-routes-http.test.ts +4 -3
  21. package/src/__tests__/asset-materialize-tool.test.ts +250 -178
  22. package/src/__tests__/asset-search-tool.test.ts +251 -191
  23. package/src/__tests__/assistant-attachment-directive.test.ts +187 -142
  24. package/src/__tests__/assistant-attachments.test.ts +254 -186
  25. package/src/__tests__/assistant-event-hub.test.ts +105 -63
  26. package/src/__tests__/assistant-event.test.ts +66 -58
  27. package/src/__tests__/assistant-events-sse-hardening.test.ts +113 -73
  28. package/src/__tests__/assistant-feature-flag-guard.test.ts +78 -52
  29. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +48 -45
  30. package/src/__tests__/assistant-feature-flags-integration.test.ts +118 -77
  31. package/src/__tests__/assistant-id-boundary-guard.test.ts +158 -104
  32. package/src/__tests__/attachments-store.test.ts +240 -183
  33. package/src/__tests__/attachments.test.ts +70 -62
  34. package/src/__tests__/audit-log-rotation.test.ts +50 -35
  35. package/src/__tests__/browser-fill-credential.test.ts +169 -101
  36. package/src/__tests__/browser-manager.test.ts +97 -75
  37. package/src/__tests__/browser-runtime-check.test.ts +16 -15
  38. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +12 -10
  39. package/src/__tests__/browser-skill-endstate.test.ts +97 -72
  40. package/src/__tests__/bundle-scanner.test.ts +47 -22
  41. package/src/__tests__/bundled-asset.test.ts +74 -47
  42. package/src/__tests__/call-constants.test.ts +19 -19
  43. package/src/__tests__/call-controller.test.ts +0 -1
  44. package/src/__tests__/call-conversation-messages.test.ts +90 -65
  45. package/src/__tests__/call-domain.test.ts +149 -121
  46. package/src/__tests__/call-pointer-message-composer.test.ts +113 -83
  47. package/src/__tests__/call-pointer-messages.test.ts +213 -154
  48. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +9 -10
  49. package/src/__tests__/call-recovery.test.ts +232 -212
  50. package/src/__tests__/call-routes-http.test.ts +0 -1
  51. package/src/__tests__/call-start-guardian-guard.test.ts +32 -30
  52. package/src/__tests__/call-state-machine.test.ts +62 -51
  53. package/src/__tests__/call-state.test.ts +89 -75
  54. package/src/__tests__/call-store.test.ts +387 -316
  55. package/src/__tests__/callback-handoff-copy.test.ts +84 -82
  56. package/src/__tests__/canonical-guardian-store.test.ts +331 -280
  57. package/src/__tests__/channel-approval-routes.test.ts +1643 -1115
  58. package/src/__tests__/channel-approval.test.ts +139 -137
  59. package/src/__tests__/channel-approvals.test.ts +0 -1
  60. package/src/__tests__/channel-delivery-store.test.ts +232 -194
  61. package/src/__tests__/channel-guardian.test.ts +5 -3
  62. package/src/__tests__/channel-invite-transport.test.ts +107 -92
  63. package/src/__tests__/channel-policy.test.ts +42 -38
  64. package/src/__tests__/channel-readiness-service.test.ts +119 -102
  65. package/src/__tests__/channel-reply-delivery.test.ts +147 -118
  66. package/src/__tests__/channel-retry-sweep.test.ts +153 -110
  67. package/src/__tests__/checker.test.ts +3309 -1850
  68. package/src/__tests__/clarification-resolver.test.ts +91 -79
  69. package/src/__tests__/classifier.test.ts +64 -54
  70. package/src/__tests__/claude-code-skill-regression.test.ts +42 -37
  71. package/src/__tests__/claude-code-tool-profiles.test.ts +31 -29
  72. package/src/__tests__/clawhub.test.ts +92 -82
  73. package/src/__tests__/cli.test.ts +30 -30
  74. package/src/__tests__/clipboard.test.ts +53 -46
  75. package/src/__tests__/commit-guarantee.test.ts +59 -52
  76. package/src/__tests__/commit-message-enrichment-service.test.ts +203 -75
  77. package/src/__tests__/compaction.benchmark.test.ts +33 -31
  78. package/src/__tests__/computer-use-session-compaction.test.ts +60 -50
  79. package/src/__tests__/computer-use-session-lifecycle.test.ts +145 -117
  80. package/src/__tests__/computer-use-session-working-dir.test.ts +62 -48
  81. package/src/__tests__/computer-use-skill-baseline.test.ts +22 -19
  82. package/src/__tests__/computer-use-skill-endstate.test.ts +45 -31
  83. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +121 -88
  84. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +65 -42
  85. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +33 -18
  86. package/src/__tests__/computer-use-tools.test.ts +121 -98
  87. package/src/__tests__/config-schema.test.ts +443 -347
  88. package/src/__tests__/config-watcher.test.ts +96 -81
  89. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +148 -133
  90. package/src/__tests__/conflict-intent-tokenization.test.ts +96 -78
  91. package/src/__tests__/conflict-policy.test.ts +151 -80
  92. package/src/__tests__/conflict-store.test.ts +203 -157
  93. package/src/__tests__/connection-policy.test.ts +89 -59
  94. package/src/__tests__/contacts-tools.test.ts +247 -178
  95. package/src/__tests__/context-memory-e2e.test.ts +306 -214
  96. package/src/__tests__/context-token-estimator.test.ts +114 -74
  97. package/src/__tests__/context-window-manager.test.ts +269 -167
  98. package/src/__tests__/contradiction-checker.test.ts +161 -135
  99. package/src/__tests__/conversation-attention-store.test.ts +350 -290
  100. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  101. package/src/__tests__/conversation-pairing.test.ts +220 -113
  102. package/src/__tests__/conversation-store.test.ts +390 -235
  103. package/src/__tests__/credential-broker-browser-fill.test.ts +325 -250
  104. package/src/__tests__/credential-broker-server-use.test.ts +283 -243
  105. package/src/__tests__/credential-broker.test.ts +128 -74
  106. package/src/__tests__/credential-host-pattern-match.test.ts +64 -44
  107. package/src/__tests__/credential-metadata-store.test.ts +360 -311
  108. package/src/__tests__/credential-policy-validate.test.ts +81 -65
  109. package/src/__tests__/credential-resolve.test.ts +212 -145
  110. package/src/__tests__/credential-security-e2e.test.ts +144 -103
  111. package/src/__tests__/credential-security-invariants.test.ts +253 -208
  112. package/src/__tests__/credential-selection.test.ts +254 -146
  113. package/src/__tests__/credential-vault-unit.test.ts +531 -341
  114. package/src/__tests__/credential-vault.test.ts +761 -484
  115. package/src/__tests__/daemon-assistant-events.test.ts +91 -66
  116. package/src/__tests__/daemon-lifecycle.test.ts +258 -190
  117. package/src/__tests__/daemon-server-session-init.test.ts +0 -1
  118. package/src/__tests__/date-context.test.ts +314 -249
  119. package/src/__tests__/db-migration-rollback.test.ts +259 -130
  120. package/src/__tests__/db-schedule-syntax-migration.test.ts +78 -41
  121. package/src/__tests__/delete-managed-skill-tool.test.ts +77 -53
  122. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  123. package/src/__tests__/dictation-mode-detection.test.ts +77 -55
  124. package/src/__tests__/dictation-profile-store.test.ts +70 -56
  125. package/src/__tests__/dictation-text-processing.test.ts +53 -35
  126. package/src/__tests__/diff.test.ts +102 -98
  127. package/src/__tests__/domain-normalize.test.ts +54 -54
  128. package/src/__tests__/domain-policy.test.ts +71 -55
  129. package/src/__tests__/dynamic-page-surface.test.ts +31 -33
  130. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +69 -69
  131. package/src/__tests__/edit-engine.test.ts +56 -56
  132. package/src/__tests__/elevenlabs-client.test.ts +117 -91
  133. package/src/__tests__/elevenlabs-config.test.ts +32 -31
  134. package/src/__tests__/email-classifier.test.ts +15 -12
  135. package/src/__tests__/email-cli.test.ts +121 -108
  136. package/src/__tests__/emit-signal-routing-intent.test.ts +76 -69
  137. package/src/__tests__/encrypted-store.test.ts +180 -154
  138. package/src/__tests__/entity-extractor.test.ts +108 -87
  139. package/src/__tests__/entity-search.test.ts +664 -258
  140. package/src/__tests__/ephemeral-permissions.test.ts +224 -188
  141. package/src/__tests__/event-bus.test.ts +81 -77
  142. package/src/__tests__/extract-email.test.ts +29 -20
  143. package/src/__tests__/file-edit-tool.test.ts +62 -44
  144. package/src/__tests__/file-ops-service.test.ts +131 -114
  145. package/src/__tests__/file-read-tool.test.ts +48 -31
  146. package/src/__tests__/file-write-tool.test.ts +43 -37
  147. package/src/__tests__/filesystem-tools.test.ts +238 -209
  148. package/src/__tests__/followup-tools.test.ts +237 -162
  149. package/src/__tests__/forbidden-legacy-symbols.test.ts +19 -20
  150. package/src/__tests__/frontmatter.test.ts +96 -81
  151. package/src/__tests__/fuzzy-match-property.test.ts +75 -81
  152. package/src/__tests__/fuzzy-match.test.ts +71 -65
  153. package/src/__tests__/gateway-client-managed-outbound.test.ts +76 -57
  154. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  155. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  156. package/src/__tests__/gemini-image-service.test.ts +113 -100
  157. package/src/__tests__/gemini-provider.test.ts +297 -220
  158. package/src/__tests__/get-weather.test.ts +188 -114
  159. package/src/__tests__/gmail-integration.test.ts +0 -1
  160. package/src/__tests__/guardian-action-conversation-turn.test.ts +226 -171
  161. package/src/__tests__/guardian-action-copy-generator.test.ts +111 -93
  162. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  163. package/src/__tests__/guardian-action-followup-store.test.ts +199 -167
  164. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +297 -250
  165. package/src/__tests__/guardian-action-late-reply.test.ts +462 -316
  166. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +23 -18
  167. package/src/__tests__/guardian-action-store.test.ts +158 -109
  168. package/src/__tests__/guardian-action-sweep.test.ts +114 -100
  169. package/src/__tests__/guardian-actions-endpoint.test.ts +440 -256
  170. package/src/__tests__/guardian-control-plane-policy.test.ts +497 -331
  171. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +217 -215
  172. package/src/__tests__/guardian-dispatch.test.ts +316 -256
  173. package/src/__tests__/guardian-grant-minting.test.ts +247 -178
  174. package/src/__tests__/guardian-outbound-http.test.ts +5 -3
  175. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +99 -96
  176. package/src/__tests__/guardian-question-copy.test.ts +17 -17
  177. package/src/__tests__/guardian-question-mode.test.ts +134 -100
  178. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  179. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  180. package/src/__tests__/guardian-verification-intent-routing.test.ts +94 -88
  181. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  182. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +0 -1
  183. package/src/__tests__/handle-user-message-secret-resume.test.ts +0 -1
  184. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +92 -76
  185. package/src/__tests__/handlers-cu-observation-blob.test.ts +103 -70
  186. package/src/__tests__/handlers-ipc-blob-probe.test.ts +77 -51
  187. package/src/__tests__/handlers-slack-config.test.ts +63 -54
  188. package/src/__tests__/handlers-task-submit-slash.test.ts +18 -18
  189. package/src/__tests__/handlers-telegram-config.test.ts +662 -329
  190. package/src/__tests__/handlers-twitter-config.test.ts +525 -298
  191. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -2
  192. package/src/__tests__/headless-browser-interactions.test.ts +444 -280
  193. package/src/__tests__/headless-browser-navigate.test.ts +116 -79
  194. package/src/__tests__/headless-browser-read-tools.test.ts +123 -86
  195. package/src/__tests__/headless-browser-snapshot.test.ts +71 -56
  196. package/src/__tests__/heartbeat-service.test.ts +76 -58
  197. package/src/__tests__/history-repair-observability.test.ts +14 -14
  198. package/src/__tests__/history-repair.test.ts +171 -167
  199. package/src/__tests__/home-base-bootstrap.test.ts +30 -27
  200. package/src/__tests__/hooks-blocking.test.ts +86 -37
  201. package/src/__tests__/hooks-cli.test.ts +104 -68
  202. package/src/__tests__/hooks-config.test.ts +81 -43
  203. package/src/__tests__/hooks-discovery.test.ts +106 -96
  204. package/src/__tests__/hooks-integration.test.ts +78 -72
  205. package/src/__tests__/hooks-manager.test.ts +99 -61
  206. package/src/__tests__/hooks-runner.test.ts +94 -71
  207. package/src/__tests__/hooks-settings.test.ts +69 -64
  208. package/src/__tests__/hooks-templates.test.ts +85 -54
  209. package/src/__tests__/hooks-ts-runner.test.ts +82 -45
  210. package/src/__tests__/hooks-watch.test.ts +32 -22
  211. package/src/__tests__/host-file-edit-tool.test.ts +190 -148
  212. package/src/__tests__/host-file-read-tool.test.ts +86 -63
  213. package/src/__tests__/host-file-write-tool.test.ts +98 -64
  214. package/src/__tests__/host-shell-tool.test.ts +342 -233
  215. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  216. package/src/__tests__/ingress-member-store.test.ts +163 -159
  217. package/src/__tests__/ingress-reconcile.test.ts +0 -1
  218. package/src/__tests__/ingress-routes-http.test.ts +441 -356
  219. package/src/__tests__/ingress-url-consistency.test.ts +125 -64
  220. package/src/__tests__/integration-status.test.ts +93 -73
  221. package/src/__tests__/intent-routing.test.ts +148 -118
  222. package/src/__tests__/invite-redemption-service.test.ts +163 -121
  223. package/src/__tests__/ipc-blob-store.test.ts +104 -91
  224. package/src/__tests__/ipc-contract-inventory.test.ts +27 -15
  225. package/src/__tests__/ipc-contract.test.ts +24 -23
  226. package/src/__tests__/ipc-protocol.test.ts +52 -46
  227. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +61 -50
  228. package/src/__tests__/ipc-snapshot.test.ts +1135 -1056
  229. package/src/__tests__/ipc-validate.test.ts +240 -179
  230. package/src/__tests__/key-migration.test.ts +123 -90
  231. package/src/__tests__/keychain.test.ts +150 -123
  232. package/src/__tests__/lifecycle-docs-guard.test.ts +65 -64
  233. package/src/__tests__/llm-usage-store.test.ts +112 -87
  234. package/src/__tests__/managed-skill-lifecycle.test.ts +147 -108
  235. package/src/__tests__/managed-store.test.ts +411 -360
  236. package/src/__tests__/mcp-cli.test.ts +189 -123
  237. package/src/__tests__/mcp-health-check.test.ts +26 -21
  238. package/src/__tests__/media-generate-image.test.ts +122 -99
  239. package/src/__tests__/media-reuse-story.e2e.test.ts +282 -214
  240. package/src/__tests__/media-visibility-policy.test.ts +86 -38
  241. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +146 -100
  242. package/src/__tests__/memory-lifecycle-e2e.test.ts +385 -297
  243. package/src/__tests__/memory-query-builder.test.ts +32 -33
  244. package/src/__tests__/memory-recall-quality.test.ts +761 -407
  245. package/src/__tests__/memory-regressions.experimental.test.ts +443 -380
  246. package/src/__tests__/memory-regressions.test.ts +3725 -2642
  247. package/src/__tests__/memory-retrieval-budget.test.ts +7 -8
  248. package/src/__tests__/memory-retrieval.benchmark.test.ts +144 -109
  249. package/src/__tests__/memory-upsert-concurrency.test.ts +292 -201
  250. package/src/__tests__/messaging-send-tool.test.ts +36 -29
  251. package/src/__tests__/migration-cli-flows.test.ts +69 -53
  252. package/src/__tests__/migration-ordering.test.ts +103 -86
  253. package/src/__tests__/mime-builder.test.ts +55 -32
  254. package/src/__tests__/mock-signup-server.test.ts +384 -246
  255. package/src/__tests__/model-intents.test.ts +61 -37
  256. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +9 -12
  257. package/src/__tests__/no-is-trusted-guard.test.ts +24 -21
  258. package/src/__tests__/non-member-access-request.test.ts +3 -2
  259. package/src/__tests__/notification-broadcaster.test.ts +99 -81
  260. package/src/__tests__/notification-decision-fallback.test.ts +223 -178
  261. package/src/__tests__/notification-decision-strategy.test.ts +375 -337
  262. package/src/__tests__/notification-deep-link.test.ts +67 -61
  263. package/src/__tests__/notification-guardian-path.test.ts +248 -206
  264. package/src/__tests__/notification-routing-intent.test.ts +166 -93
  265. package/src/__tests__/notification-thread-candidate-validation.test.ts +78 -75
  266. package/src/__tests__/notification-thread-candidates.test.ts +64 -61
  267. package/src/__tests__/oauth-callback-registry.test.ts +40 -30
  268. package/src/__tests__/oauth-connect-handler.test.ts +109 -89
  269. package/src/__tests__/oauth-scope-policy.test.ts +63 -55
  270. package/src/__tests__/oauth2-gateway-transport.test.ts +252 -174
  271. package/src/__tests__/onboarding-starter-tasks.test.ts +93 -89
  272. package/src/__tests__/onboarding-template-contract.test.ts +93 -94
  273. package/src/__tests__/openai-provider.test.ts +366 -274
  274. package/src/__tests__/pairing-concurrent.test.ts +18 -12
  275. package/src/__tests__/pairing-routes.test.ts +45 -41
  276. package/src/__tests__/parallel-tool.benchmark.test.ts +108 -58
  277. package/src/__tests__/parser.test.ts +316 -226
  278. package/src/__tests__/path-classifier.test.ts +24 -25
  279. package/src/__tests__/path-policy.test.ts +187 -147
  280. package/src/__tests__/phone.test.ts +36 -36
  281. package/src/__tests__/platform-move-helper.test.ts +48 -40
  282. package/src/__tests__/platform-socket-path.test.ts +23 -24
  283. package/src/__tests__/platform-workspace-migration.test.ts +464 -414
  284. package/src/__tests__/platform.test.ts +61 -53
  285. package/src/__tests__/playbook-execution.test.ts +397 -265
  286. package/src/__tests__/playbook-tools.test.ts +267 -196
  287. package/src/__tests__/prebuilt-home-base-seed.test.ts +30 -27
  288. package/src/__tests__/pricing.test.ts +316 -136
  289. package/src/__tests__/profile-compiler.test.ts +206 -188
  290. package/src/__tests__/provider-commit-message-generator.test.ts +114 -106
  291. package/src/__tests__/provider-error-scenarios.test.ts +212 -158
  292. package/src/__tests__/provider-fail-open-selection.test.ts +51 -44
  293. package/src/__tests__/provider-registry-ollama.test.ts +13 -9
  294. package/src/__tests__/provider-streaming.benchmark.test.ts +232 -183
  295. package/src/__tests__/proxy-approval-callback.test.ts +180 -119
  296. package/src/__tests__/public-ingress-urls.test.ts +112 -94
  297. package/src/__tests__/qdrant-manager.test.ts +147 -98
  298. package/src/__tests__/ratelimit.test.ts +152 -82
  299. package/src/__tests__/recording-handler.test.ts +273 -151
  300. package/src/__tests__/recording-intent-fallback.test.ts +94 -75
  301. package/src/__tests__/recording-intent-handler.test.ts +0 -1
  302. package/src/__tests__/recording-intent.test.ts +578 -379
  303. package/src/__tests__/recording-state-machine.test.ts +530 -316
  304. package/src/__tests__/recurrence-engine-rruleset.test.ts +150 -92
  305. package/src/__tests__/recurrence-engine.test.ts +81 -41
  306. package/src/__tests__/recurrence-types.test.ts +63 -44
  307. package/src/__tests__/relay-server.test.ts +2131 -1602
  308. package/src/__tests__/reminder-store.test.ts +158 -80
  309. package/src/__tests__/reminder.test.ts +113 -109
  310. package/src/__tests__/remote-skill-policy.test.ts +96 -72
  311. package/src/__tests__/request-file-tool.test.ts +74 -67
  312. package/src/__tests__/response-tier.test.ts +131 -74
  313. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  314. package/src/__tests__/runtime-events-sse-parity.test.ts +167 -145
  315. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  316. package/src/__tests__/sandbox-diagnostics.test.ts +66 -56
  317. package/src/__tests__/sandbox-host-parity.test.ts +377 -301
  318. package/src/__tests__/scaffold-managed-skill-tool.test.ts +213 -161
  319. package/src/__tests__/schedule-store.test.ts +268 -205
  320. package/src/__tests__/schedule-tools.test.ts +702 -524
  321. package/src/__tests__/scheduler-recurrence.test.ts +240 -130
  322. package/src/__tests__/scoped-approval-grants.test.ts +258 -168
  323. package/src/__tests__/scoped-grant-security-matrix.test.ts +160 -146
  324. package/src/__tests__/script-proxy-certs.test.ts +38 -35
  325. package/src/__tests__/script-proxy-connect-tunnel.test.ts +71 -46
  326. package/src/__tests__/script-proxy-decision-trace.test.ts +161 -84
  327. package/src/__tests__/script-proxy-http-forwarder.test.ts +146 -129
  328. package/src/__tests__/script-proxy-injection-runtime.test.ts +139 -113
  329. package/src/__tests__/script-proxy-mitm-handler.test.ts +226 -142
  330. package/src/__tests__/script-proxy-policy-runtime.test.ts +126 -86
  331. package/src/__tests__/script-proxy-policy.test.ts +308 -153
  332. package/src/__tests__/script-proxy-rewrite-specificity.test.ts +74 -62
  333. package/src/__tests__/script-proxy-router.test.ts +111 -77
  334. package/src/__tests__/script-proxy-session-manager.test.ts +156 -113
  335. package/src/__tests__/script-proxy-session-runtime.test.ts +28 -24
  336. package/src/__tests__/secret-allowlist.test.ts +105 -90
  337. package/src/__tests__/secret-ingress-handler.test.ts +41 -30
  338. package/src/__tests__/secret-onetime-send.test.ts +67 -50
  339. package/src/__tests__/secret-prompt-log-hygiene.test.ts +35 -31
  340. package/src/__tests__/secret-response-routing.test.ts +50 -41
  341. package/src/__tests__/secret-scanner-executor.test.ts +152 -111
  342. package/src/__tests__/secret-scanner.test.ts +495 -413
  343. package/src/__tests__/secure-keys.test.ts +132 -121
  344. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  345. package/src/__tests__/send-notification-tool.test.ts +43 -42
  346. package/src/__tests__/sensitive-output-placeholders.test.ts +72 -64
  347. package/src/__tests__/sequence-store.test.ts +335 -167
  348. package/src/__tests__/server-history-render.test.ts +341 -202
  349. package/src/__tests__/session-abort-tool-results.test.ts +133 -70
  350. package/src/__tests__/session-confirmation-signals.test.ts +252 -160
  351. package/src/__tests__/session-conflict-gate.test.ts +775 -585
  352. package/src/__tests__/session-error.test.ts +222 -191
  353. package/src/__tests__/session-evictor.test.ts +79 -62
  354. package/src/__tests__/session-init.benchmark.test.ts +170 -108
  355. package/src/__tests__/session-load-history-repair.test.ts +273 -139
  356. package/src/__tests__/session-messaging-secret-redirect.test.ts +130 -90
  357. package/src/__tests__/session-pre-run-repair.test.ts +106 -59
  358. package/src/__tests__/session-profile-injection.test.ts +198 -130
  359. package/src/__tests__/session-provider-retry-repair.test.ts +223 -141
  360. package/src/__tests__/session-queue.test.ts +624 -321
  361. package/src/__tests__/session-runtime-assembly.test.ts +425 -329
  362. package/src/__tests__/session-runtime-workspace.test.ts +69 -61
  363. package/src/__tests__/session-skill-tools.test.ts +973 -678
  364. package/src/__tests__/session-slash-known.test.ts +185 -133
  365. package/src/__tests__/session-slash-queue.test.ts +147 -81
  366. package/src/__tests__/session-slash-unknown.test.ts +135 -90
  367. package/src/__tests__/session-surfaces-task-progress.test.ts +122 -87
  368. package/src/__tests__/session-tool-setup-app-refresh.test.ts +338 -177
  369. package/src/__tests__/session-tool-setup-memory-scope.test.ts +63 -40
  370. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +60 -37
  371. package/src/__tests__/session-tool-setup-tools-disabled.test.ts +28 -26
  372. package/src/__tests__/session-undo.test.ts +43 -30
  373. package/src/__tests__/session-workspace-cache-state.test.ts +108 -67
  374. package/src/__tests__/session-workspace-injection.test.ts +245 -117
  375. package/src/__tests__/session-workspace-tool-tracking.test.ts +260 -93
  376. package/src/__tests__/shared-filesystem-errors.test.ts +47 -47
  377. package/src/__tests__/shell-credential-ref.test.ts +126 -90
  378. package/src/__tests__/shell-identity.test.ts +134 -111
  379. package/src/__tests__/shell-parser-fuzz.test.ts +263 -179
  380. package/src/__tests__/shell-parser-property.test.ts +435 -288
  381. package/src/__tests__/shell-tool-proxy-mode.test.ts +142 -70
  382. package/src/__tests__/size-guard.test.ts +42 -44
  383. package/src/__tests__/skill-feature-flags-integration.test.ts +79 -52
  384. package/src/__tests__/skill-feature-flags.test.ts +75 -47
  385. package/src/__tests__/skill-include-graph.test.ts +143 -148
  386. package/src/__tests__/skill-load-feature-flag.test.ts +94 -59
  387. package/src/__tests__/skill-load-tool.test.ts +371 -199
  388. package/src/__tests__/skill-projection-feature-flag.test.ts +131 -88
  389. package/src/__tests__/skill-projection.benchmark.test.ts +93 -65
  390. package/src/__tests__/skill-script-runner-host.test.ts +460 -250
  391. package/src/__tests__/skill-script-runner-sandbox.test.ts +168 -108
  392. package/src/__tests__/skill-script-runner.test.ts +115 -74
  393. package/src/__tests__/skill-tool-factory.test.ts +140 -96
  394. package/src/__tests__/skill-tool-manifest.test.ts +306 -210
  395. package/src/__tests__/skill-version-hash.test.ts +70 -56
  396. package/src/__tests__/skills.test.ts +0 -1
  397. package/src/__tests__/slack-channel-config.test.ts +127 -84
  398. package/src/__tests__/slack-skill.test.ts +60 -47
  399. package/src/__tests__/slash-commands-catalog.test.ts +37 -31
  400. package/src/__tests__/slash-commands-parser.test.ts +71 -64
  401. package/src/__tests__/slash-commands-resolver.test.ts +143 -107
  402. package/src/__tests__/slash-commands-rewrite.test.ts +22 -22
  403. package/src/__tests__/speaker-identification.test.ts +28 -25
  404. package/src/__tests__/starter-bundle.test.ts +27 -23
  405. package/src/__tests__/starter-task-flow.test.ts +67 -52
  406. package/src/__tests__/subagent-manager-notify.test.ts +154 -108
  407. package/src/__tests__/subagent-tools.test.ts +311 -270
  408. package/src/__tests__/subagent-types.test.ts +40 -40
  409. package/src/__tests__/surface-mutex-cleanup.test.ts +42 -30
  410. package/src/__tests__/swarm-dag-pathological.test.ts +122 -111
  411. package/src/__tests__/swarm-orchestrator.test.ts +135 -101
  412. package/src/__tests__/swarm-plan-validator.test.ts +125 -73
  413. package/src/__tests__/swarm-recursion.test.ts +58 -46
  414. package/src/__tests__/swarm-router-planner.test.ts +99 -74
  415. package/src/__tests__/swarm-session-integration.test.ts +148 -91
  416. package/src/__tests__/swarm-tool.test.ts +65 -45
  417. package/src/__tests__/swarm-worker-backend.test.ts +59 -45
  418. package/src/__tests__/swarm-worker-runner.test.ts +133 -118
  419. package/src/__tests__/system-prompt.test.ts +290 -256
  420. package/src/__tests__/task-compiler.test.ts +176 -120
  421. package/src/__tests__/task-management-tools.test.ts +561 -456
  422. package/src/__tests__/task-memory-cleanup.test.ts +627 -362
  423. package/src/__tests__/task-runner.test.ts +117 -94
  424. package/src/__tests__/task-scheduler.test.ts +113 -84
  425. package/src/__tests__/task-tools.test.ts +349 -264
  426. package/src/__tests__/terminal-sandbox.test.ts +138 -108
  427. package/src/__tests__/terminal-tools.test.ts +350 -305
  428. package/src/__tests__/thread-seed-composer.test.ts +307 -180
  429. package/src/__tests__/tool-approval-handler.test.ts +238 -137
  430. package/src/__tests__/tool-audit-listener.test.ts +69 -69
  431. package/src/__tests__/tool-domain-event-publisher.test.ts +142 -132
  432. package/src/__tests__/tool-execution-abort-cleanup.test.ts +153 -146
  433. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +136 -105
  434. package/src/__tests__/tool-executor-lifecycle-events.test.ts +355 -239
  435. package/src/__tests__/tool-executor-redaction.test.ts +112 -109
  436. package/src/__tests__/tool-executor-shell-integration.test.ts +130 -79
  437. package/src/__tests__/tool-executor.test.ts +1274 -674
  438. package/src/__tests__/tool-grant-request-escalation.test.ts +401 -283
  439. package/src/__tests__/tool-metrics-listener.test.ts +97 -85
  440. package/src/__tests__/tool-notification-listener.test.ts +42 -25
  441. package/src/__tests__/tool-permission-simulate-handler.test.ts +137 -113
  442. package/src/__tests__/tool-policy.test.ts +44 -25
  443. package/src/__tests__/tool-profiling-listener.test.ts +99 -93
  444. package/src/__tests__/tool-result-truncation.test.ts +5 -4
  445. package/src/__tests__/tool-trace-listener.test.ts +131 -111
  446. package/src/__tests__/top-level-renderer.test.ts +62 -58
  447. package/src/__tests__/top-level-scanner.test.ts +68 -64
  448. package/src/__tests__/trace-emitter.test.ts +56 -56
  449. package/src/__tests__/trust-context-guards.test.ts +65 -65
  450. package/src/__tests__/trust-store.test.ts +1239 -806
  451. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  452. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  453. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +3 -2
  454. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -2
  455. package/src/__tests__/trusted-contact-verification.test.ts +251 -231
  456. package/src/__tests__/turn-commit.test.ts +259 -200
  457. package/src/__tests__/twilio-provider.test.ts +140 -126
  458. package/src/__tests__/twilio-rest.test.ts +22 -18
  459. package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -1
  460. package/src/__tests__/twilio-routes-twiml.test.ts +55 -55
  461. package/src/__tests__/twilio-routes.test.ts +0 -1
  462. package/src/__tests__/twitter-auth-handler.test.ts +184 -139
  463. package/src/__tests__/twitter-cli-error-shaping.test.ts +88 -73
  464. package/src/__tests__/twitter-cli-routing.test.ts +146 -99
  465. package/src/__tests__/twitter-oauth-client.test.ts +82 -65
  466. package/src/__tests__/update-bulletin-format.test.ts +69 -66
  467. package/src/__tests__/update-bulletin-state.test.ts +66 -60
  468. package/src/__tests__/update-bulletin.test.ts +150 -114
  469. package/src/__tests__/update-template-contract.test.ts +15 -10
  470. package/src/__tests__/url-safety.test.ts +288 -265
  471. package/src/__tests__/user-reference.test.ts +32 -32
  472. package/src/__tests__/view-image-tool.test.ts +118 -96
  473. package/src/__tests__/voice-invite-redemption.test.ts +111 -106
  474. package/src/__tests__/voice-quality.test.ts +117 -102
  475. package/src/__tests__/voice-scoped-grant-consumer.test.ts +204 -146
  476. package/src/__tests__/voice-session-bridge.test.ts +351 -216
  477. package/src/__tests__/weather-skill-regression.test.ts +170 -120
  478. package/src/__tests__/web-fetch.test.ts +664 -526
  479. package/src/__tests__/web-search.test.ts +379 -213
  480. package/src/__tests__/work-item-output.test.ts +90 -53
  481. package/src/__tests__/workspace-git-service.test.ts +437 -356
  482. package/src/__tests__/workspace-heartbeat-service.test.ts +125 -91
  483. package/src/__tests__/workspace-lifecycle.test.ts +98 -64
  484. package/src/__tests__/workspace-policy.test.ts +139 -71
  485. package/src/commands/__tests__/cc-command-registry.test.ts +142 -134
  486. package/src/config/__tests__/feature-flag-registry-guard.test.ts +48 -39
  487. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +25 -10
  488. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -1
  489. package/src/config/bundled-skills/messaging/SKILL.md +4 -3
  490. package/src/config/bundled-skills/messaging/tools/gmail-outreach-scan.ts +15 -5
  491. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +16 -5
  492. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +34 -32
  493. package/src/config/bundled-tool-registry.ts +2 -0
  494. package/src/config/env.ts +3 -4
  495. package/src/memory/db-connection.ts +16 -10
  496. package/src/messaging/providers/gmail/adapter.ts +10 -3
  497. package/src/messaging/providers/gmail/client.ts +280 -72
  498. package/src/runtime/auth/__tests__/context.test.ts +75 -65
  499. package/src/runtime/auth/__tests__/credential-service.test.ts +137 -114
  500. package/src/runtime/auth/__tests__/guard-tests.test.ts +84 -90
  501. package/src/runtime/auth/__tests__/ipc-auth-context.test.ts +40 -40
  502. package/src/runtime/auth/__tests__/middleware.test.ts +80 -74
  503. package/src/runtime/auth/__tests__/policy.test.ts +9 -9
  504. package/src/runtime/auth/__tests__/route-policy.test.ts +76 -65
  505. package/src/runtime/auth/__tests__/scopes.test.ts +68 -60
  506. package/src/runtime/auth/__tests__/subject.test.ts +54 -54
  507. package/src/runtime/auth/__tests__/token-service.test.ts +115 -108
  508. package/src/runtime/auth/scopes.ts +3 -0
  509. package/src/runtime/auth/token-service.ts +4 -1
  510. package/src/runtime/auth/types.ts +2 -1
  511. package/src/runtime/http-server.ts +2 -1
  512. package/src/security/secure-keys.ts +103 -53
  513. package/src/tools/browser/__tests__/auth-cache.test.ts +69 -63
  514. package/src/tools/browser/__tests__/auth-detector.test.ts +218 -157
  515. package/src/tools/browser/__tests__/jit-auth.test.ts +83 -99
@@ -1,4 +1,4 @@
1
- import { describe, expect,test } from 'bun:test';
1
+ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
4
  type AssistantAttachmentDraft,
@@ -12,47 +12,47 @@ import {
12
12
  MAX_ASSISTANT_ATTACHMENT_BYTES,
13
13
  MAX_ASSISTANT_ATTACHMENTS,
14
14
  validateDrafts,
15
- } from '../daemon/assistant-attachments.js';
15
+ } from "../daemon/assistant-attachments.js";
16
16
 
17
17
  // ---------------------------------------------------------------------------
18
18
  // estimateBase64Bytes
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
- describe('estimateBase64Bytes', () => {
22
- test('returns 0 for empty string', () => {
23
- expect(estimateBase64Bytes('')).toBe(0);
21
+ describe("estimateBase64Bytes", () => {
22
+ test("returns 0 for empty string", () => {
23
+ expect(estimateBase64Bytes("")).toBe(0);
24
24
  });
25
25
 
26
- test('handles no-padding base64', () => {
26
+ test("handles no-padding base64", () => {
27
27
  // "abc" → base64 "YWJj" (4 chars, 0 padding → 3 bytes)
28
- expect(estimateBase64Bytes('YWJj')).toBe(3);
28
+ expect(estimateBase64Bytes("YWJj")).toBe(3);
29
29
  });
30
30
 
31
- test('handles single-pad base64', () => {
31
+ test("handles single-pad base64", () => {
32
32
  // "ab" → base64 "YWI=" (4 chars, 1 padding → 2 bytes)
33
- expect(estimateBase64Bytes('YWI=')).toBe(2);
33
+ expect(estimateBase64Bytes("YWI=")).toBe(2);
34
34
  });
35
35
 
36
- test('handles double-pad base64', () => {
36
+ test("handles double-pad base64", () => {
37
37
  // "a" → base64 "YQ==" (4 chars, 2 padding → 1 byte)
38
- expect(estimateBase64Bytes('YQ==')).toBe(1);
38
+ expect(estimateBase64Bytes("YQ==")).toBe(1);
39
39
  });
40
40
 
41
- test('estimates correctly for longer strings', () => {
41
+ test("estimates correctly for longer strings", () => {
42
42
  // 12 base64 chars, no padding → 9 bytes
43
- expect(estimateBase64Bytes('SGVsbG8gV29y')).toBe(9);
43
+ expect(estimateBase64Bytes("SGVsbG8gV29y")).toBe(9);
44
44
  });
45
45
 
46
- test('trims trailing whitespace and newlines before estimating', () => {
46
+ test("trims trailing whitespace and newlines before estimating", () => {
47
47
  // "a" → base64 "YQ==" → 1 byte; trailing newline should not affect result
48
- expect(estimateBase64Bytes('YQ==\n')).toBe(1);
49
- expect(estimateBase64Bytes('YQ==\r\n')).toBe(1);
50
- expect(estimateBase64Bytes(' YQ== ')).toBe(1);
48
+ expect(estimateBase64Bytes("YQ==\n")).toBe(1);
49
+ expect(estimateBase64Bytes("YQ==\r\n")).toBe(1);
50
+ expect(estimateBase64Bytes(" YQ== ")).toBe(1);
51
51
  });
52
52
 
53
- test('handles embedded line breaks in base64 (e.g. PEM-style)', () => {
53
+ test("handles embedded line breaks in base64 (e.g. PEM-style)", () => {
54
54
  // "YWJj" split across lines should still yield 3 bytes
55
- expect(estimateBase64Bytes('YW\nJj')).toBe(3);
55
+ expect(estimateBase64Bytes("YW\nJj")).toBe(3);
56
56
  });
57
57
  });
58
58
 
@@ -60,39 +60,39 @@ describe('estimateBase64Bytes', () => {
60
60
  // inferMimeType
61
61
  // ---------------------------------------------------------------------------
62
62
 
63
- describe('inferMimeType', () => {
64
- test('infers image types', () => {
65
- expect(inferMimeType('photo.png')).toBe('image/png');
66
- expect(inferMimeType('photo.jpg')).toBe('image/jpeg');
67
- expect(inferMimeType('photo.jpeg')).toBe('image/jpeg');
68
- expect(inferMimeType('sticker.webp')).toBe('image/webp');
69
- expect(inferMimeType('icon.gif')).toBe('image/gif');
70
- expect(inferMimeType('diagram.svg')).toBe('image/svg+xml');
63
+ describe("inferMimeType", () => {
64
+ test("infers image types", () => {
65
+ expect(inferMimeType("photo.png")).toBe("image/png");
66
+ expect(inferMimeType("photo.jpg")).toBe("image/jpeg");
67
+ expect(inferMimeType("photo.jpeg")).toBe("image/jpeg");
68
+ expect(inferMimeType("sticker.webp")).toBe("image/webp");
69
+ expect(inferMimeType("icon.gif")).toBe("image/gif");
70
+ expect(inferMimeType("diagram.svg")).toBe("image/svg+xml");
71
71
  });
72
72
 
73
- test('infers document types', () => {
74
- expect(inferMimeType('report.pdf')).toBe('application/pdf');
75
- expect(inferMimeType('data.json')).toBe('application/json');
76
- expect(inferMimeType('notes.txt')).toBe('text/plain');
77
- expect(inferMimeType('readme.md')).toBe('text/markdown');
78
- expect(inferMimeType('table.csv')).toBe('text/csv');
73
+ test("infers document types", () => {
74
+ expect(inferMimeType("report.pdf")).toBe("application/pdf");
75
+ expect(inferMimeType("data.json")).toBe("application/json");
76
+ expect(inferMimeType("notes.txt")).toBe("text/plain");
77
+ expect(inferMimeType("readme.md")).toBe("text/markdown");
78
+ expect(inferMimeType("table.csv")).toBe("text/csv");
79
79
  });
80
80
 
81
- test('is case-insensitive on extension', () => {
82
- expect(inferMimeType('PHOTO.PNG')).toBe('image/png');
83
- expect(inferMimeType('Report.PDF')).toBe('application/pdf');
81
+ test("is case-insensitive on extension", () => {
82
+ expect(inferMimeType("PHOTO.PNG")).toBe("image/png");
83
+ expect(inferMimeType("Report.PDF")).toBe("application/pdf");
84
84
  });
85
85
 
86
- test('returns octet-stream for unknown extension', () => {
87
- expect(inferMimeType('file.xyz')).toBe('application/octet-stream');
86
+ test("returns octet-stream for unknown extension", () => {
87
+ expect(inferMimeType("file.xyz")).toBe("application/octet-stream");
88
88
  });
89
89
 
90
- test('returns octet-stream for no extension', () => {
91
- expect(inferMimeType('Makefile')).toBe('application/octet-stream');
90
+ test("returns octet-stream for no extension", () => {
91
+ expect(inferMimeType("Makefile")).toBe("application/octet-stream");
92
92
  });
93
93
 
94
- test('uses last extension for double-dotted names', () => {
95
- expect(inferMimeType('archive.tar.gz')).toBe('application/gzip');
94
+ test("uses last extension for double-dotted names", () => {
95
+ expect(inferMimeType("archive.tar.gz")).toBe("application/gzip");
96
96
  });
97
97
  });
98
98
 
@@ -100,23 +100,23 @@ describe('inferMimeType', () => {
100
100
  // classifyKind
101
101
  // ---------------------------------------------------------------------------
102
102
 
103
- describe('classifyKind', () => {
104
- test('classifies image mime types as image', () => {
105
- expect(classifyKind('image/png')).toBe('image');
106
- expect(classifyKind('image/jpeg')).toBe('image');
107
- expect(classifyKind('image/webp')).toBe('image');
103
+ describe("classifyKind", () => {
104
+ test("classifies image mime types as image", () => {
105
+ expect(classifyKind("image/png")).toBe("image");
106
+ expect(classifyKind("image/jpeg")).toBe("image");
107
+ expect(classifyKind("image/webp")).toBe("image");
108
108
  });
109
109
 
110
- test('classifies video mime types as video', () => {
111
- expect(classifyKind('video/mp4')).toBe('video');
112
- expect(classifyKind('video/webm')).toBe('video');
113
- expect(classifyKind('video/quicktime')).toBe('video');
110
+ test("classifies video mime types as video", () => {
111
+ expect(classifyKind("video/mp4")).toBe("video");
112
+ expect(classifyKind("video/webm")).toBe("video");
113
+ expect(classifyKind("video/quicktime")).toBe("video");
114
114
  });
115
115
 
116
- test('classifies non-image non-video mime types as document', () => {
117
- expect(classifyKind('application/pdf')).toBe('document');
118
- expect(classifyKind('text/plain')).toBe('document');
119
- expect(classifyKind('application/octet-stream')).toBe('document');
116
+ test("classifies non-image non-video mime types as document", () => {
117
+ expect(classifyKind("application/pdf")).toBe("document");
118
+ expect(classifyKind("text/plain")).toBe("document");
119
+ expect(classifyKind("application/octet-stream")).toBe("document");
120
120
  });
121
121
  });
122
122
 
@@ -124,49 +124,65 @@ describe('classifyKind', () => {
124
124
  // validateDrafts
125
125
  // ---------------------------------------------------------------------------
126
126
 
127
- function makeDraft(overrides: Partial<AssistantAttachmentDraft> = {}): AssistantAttachmentDraft {
127
+ function makeDraft(
128
+ overrides: Partial<AssistantAttachmentDraft> = {},
129
+ ): AssistantAttachmentDraft {
128
130
  return {
129
- sourceType: 'sandbox_file',
130
- filename: 'test.txt',
131
- mimeType: 'text/plain',
132
- dataBase64: 'dGVzdA==',
131
+ sourceType: "sandbox_file",
132
+ filename: "test.txt",
133
+ mimeType: "text/plain",
134
+ dataBase64: "dGVzdA==",
133
135
  sizeBytes: 4,
134
- kind: 'document',
136
+ kind: "document",
135
137
  ...overrides,
136
138
  };
137
139
  }
138
140
 
139
- describe('validateDrafts', () => {
140
- test('accepts drafts within limits', () => {
141
- const drafts = [makeDraft({ filename: 'a.txt' }), makeDraft({ filename: 'b.txt' })];
141
+ describe("validateDrafts", () => {
142
+ test("accepts drafts within limits", () => {
143
+ const drafts = [
144
+ makeDraft({ filename: "a.txt" }),
145
+ makeDraft({ filename: "b.txt" }),
146
+ ];
142
147
  const result = validateDrafts(drafts);
143
148
  expect(result.accepted).toHaveLength(2);
144
149
  expect(result.warnings).toHaveLength(0);
145
150
  });
146
151
 
147
- test('rejects oversized attachments', () => {
148
- const drafts = [makeDraft({ filename: 'big.bin', sizeBytes: MAX_ASSISTANT_ATTACHMENT_BYTES + 1 })];
152
+ test("rejects oversized attachments", () => {
153
+ const drafts = [
154
+ makeDraft({
155
+ filename: "big.bin",
156
+ sizeBytes: MAX_ASSISTANT_ATTACHMENT_BYTES + 1,
157
+ }),
158
+ ];
149
159
  const result = validateDrafts(drafts);
150
160
  expect(result.accepted).toHaveLength(0);
151
161
  expect(result.warnings).toHaveLength(1);
152
- expect(result.warnings[0]).toContain('big.bin');
153
- expect(result.warnings[0]).toContain('exceeds');
162
+ expect(result.warnings[0]).toContain("big.bin");
163
+ expect(result.warnings[0]).toContain("exceeds");
154
164
  });
155
165
 
156
- test('truncates beyond max count', () => {
157
- const drafts = Array.from({ length: MAX_ASSISTANT_ATTACHMENTS + 2 }, (_, i) =>
158
- makeDraft({ filename: `file-${i}.txt` }),
166
+ test("truncates beyond max count", () => {
167
+ const drafts = Array.from(
168
+ { length: MAX_ASSISTANT_ATTACHMENTS + 2 },
169
+ (_, i) => makeDraft({ filename: `file-${i}.txt` }),
159
170
  );
160
171
  const result = validateDrafts(drafts);
161
172
  expect(result.accepted).toHaveLength(MAX_ASSISTANT_ATTACHMENTS);
162
173
  expect(result.warnings).toHaveLength(2);
163
- expect(result.warnings[0]).toContain(`file-${MAX_ASSISTANT_ATTACHMENTS}.txt`);
164
- expect(result.warnings[0]).toContain('exceeded maximum');
174
+ expect(result.warnings[0]).toContain(
175
+ `file-${MAX_ASSISTANT_ATTACHMENTS}.txt`,
176
+ );
177
+ expect(result.warnings[0]).toContain("exceeded maximum");
165
178
  });
166
179
 
167
- test('rejects oversized before applying count cap', () => {
180
+ test("rejects oversized before applying count cap", () => {
168
181
  const drafts = [
169
- makeDraft({ filename: 'big.bin', sizeBytes: MAX_ASSISTANT_ATTACHMENT_BYTES + 1 }),
182
+ makeDraft({
183
+ filename: "big.bin",
184
+ sizeBytes: MAX_ASSISTANT_ATTACHMENT_BYTES + 1,
185
+ }),
170
186
  ...Array.from({ length: MAX_ASSISTANT_ATTACHMENTS }, (_, i) =>
171
187
  makeDraft({ filename: `ok-${i}.txt` }),
172
188
  ),
@@ -175,10 +191,10 @@ describe('validateDrafts', () => {
175
191
  // big.bin rejected for size; all MAX_ASSISTANT_ATTACHMENTS ok files accepted
176
192
  expect(result.accepted).toHaveLength(MAX_ASSISTANT_ATTACHMENTS);
177
193
  expect(result.warnings).toHaveLength(1);
178
- expect(result.warnings[0]).toContain('big.bin');
194
+ expect(result.warnings[0]).toContain("big.bin");
179
195
  });
180
196
 
181
- test('returns empty accepted for empty input', () => {
197
+ test("returns empty accepted for empty input", () => {
182
198
  const result = validateDrafts([]);
183
199
  expect(result.accepted).toHaveLength(0);
184
200
  expect(result.warnings).toHaveLength(0);
@@ -189,22 +205,27 @@ describe('validateDrafts', () => {
189
205
  // cleanAssistantContent
190
206
  // ---------------------------------------------------------------------------
191
207
 
192
- describe('cleanAssistantContent', () => {
193
- test('strips directives from text blocks and returns them', () => {
208
+ describe("cleanAssistantContent", () => {
209
+ test("strips directives from text blocks and returns them", () => {
194
210
  const content = [
195
- { type: 'text', text: 'Here is the file:\n<vellum-attachment path="out.png" />' },
211
+ {
212
+ type: "text",
213
+ text: 'Here is the file:\n<vellum-attachment path="out.png" />',
214
+ },
196
215
  ];
197
216
  const result = cleanAssistantContent(content);
198
217
 
199
218
  expect(result.directives).toHaveLength(1);
200
- expect(result.directives[0].path).toBe('out.png');
201
- expect((result.cleanedContent[0] as { text: string }).text).toBe('Here is the file:');
219
+ expect(result.directives[0].path).toBe("out.png");
220
+ expect((result.cleanedContent[0] as { text: string }).text).toBe(
221
+ "Here is the file:",
222
+ );
202
223
  });
203
224
 
204
- test('leaves non-text blocks unchanged', () => {
225
+ test("leaves non-text blocks unchanged", () => {
205
226
  const content = [
206
- { type: 'tool_use', id: 't1', name: 'read', input: {} },
207
- { type: 'text', text: '<vellum-attachment path="x.pdf" />' },
227
+ { type: "tool_use", id: "t1", name: "read", input: {} },
228
+ { type: "text", text: '<vellum-attachment path="x.pdf" />' },
208
229
  ];
209
230
  const result = cleanAssistantContent(content);
210
231
 
@@ -212,33 +233,31 @@ describe('cleanAssistantContent', () => {
212
233
  expect(result.directives).toHaveLength(1);
213
234
  });
214
235
 
215
- test('accumulates warnings for malformed tags', () => {
236
+ test("accumulates warnings for malformed tags", () => {
216
237
  const content = [
217
- { type: 'text', text: '<vellum-attachment source="bad" path="x.txt" />' },
238
+ { type: "text", text: '<vellum-attachment source="bad" path="x.txt" />' },
218
239
  ];
219
240
  const result = cleanAssistantContent(content);
220
241
 
221
242
  expect(result.directives).toHaveLength(0);
222
243
  expect(result.warnings).toHaveLength(1);
223
- expect(result.warnings[0]).toContain('invalid source');
244
+ expect(result.warnings[0]).toContain("invalid source");
224
245
  });
225
246
 
226
- test('preserves whitespace in plain text blocks without directives', () => {
227
- const content = [
228
- { type: 'text', text: ' Hello\n\n\n\nWorld ' },
229
- ];
247
+ test("preserves whitespace in plain text blocks without directives", () => {
248
+ const content = [{ type: "text", text: " Hello\n\n\n\nWorld " }];
230
249
  const result = cleanAssistantContent(content);
231
250
 
232
251
  expect(result.directives).toHaveLength(0);
233
252
  expect(result.warnings).toHaveLength(0);
234
253
  // Text should be returned exactly as-is — no trimming or blank-line collapsing
235
- expect((result.cleanedContent[0] as { text: string }).text).toBe(' Hello\n\n\n\nWorld ');
254
+ expect((result.cleanedContent[0] as { text: string }).text).toBe(
255
+ " Hello\n\n\n\nWorld ",
256
+ );
236
257
  });
237
258
 
238
- test('handles content with no text blocks', () => {
239
- const content = [
240
- { type: 'thinking', thinking: 'hmm', signature: 'sig' },
241
- ];
259
+ test("handles content with no text blocks", () => {
260
+ const content = [{ type: "thinking", thinking: "hmm", signature: "sig" }];
242
261
  const result = cleanAssistantContent(content);
243
262
 
244
263
  expect(result.directives).toHaveLength(0);
@@ -251,72 +270,76 @@ describe('cleanAssistantContent', () => {
251
270
  // drainDirectiveDisplayBuffer
252
271
  // ---------------------------------------------------------------------------
253
272
 
254
- describe('drainDirectiveDisplayBuffer', () => {
255
- test('strips complete valid directives from streamed text', () => {
273
+ describe("drainDirectiveDisplayBuffer", () => {
274
+ test("strips complete valid directives from streamed text", () => {
256
275
  const input = 'Before <vellum-attachment path="out.png" /> After';
257
276
  const result = drainDirectiveDisplayBuffer(input);
258
- expect(result.emitText).toBe('Before After');
259
- expect(result.bufferedRemainder).toBe('');
277
+ expect(result.emitText).toBe("Before After");
278
+ expect(result.bufferedRemainder).toBe("");
260
279
  });
261
280
 
262
- test('preserves invalid directives as plain text', () => {
281
+ test("preserves invalid directives as plain text", () => {
263
282
  const input = 'Bad <vellum-attachment source="bad" path="x.txt" /> tag';
264
283
  const result = drainDirectiveDisplayBuffer(input);
265
284
  expect(result.emitText).toBe(input);
266
- expect(result.bufferedRemainder).toBe('');
285
+ expect(result.bufferedRemainder).toBe("");
267
286
  });
268
287
 
269
- test('buffers incomplete directives until completion', () => {
270
- const first = drainDirectiveDisplayBuffer('Start <vellum-attachment path="file');
271
- expect(first.emitText).toBe('Start ');
272
- expect(first.bufferedRemainder).toContain('<vellum-attachment');
288
+ test("buffers incomplete directives until completion", () => {
289
+ const first = drainDirectiveDisplayBuffer(
290
+ 'Start <vellum-attachment path="file',
291
+ );
292
+ expect(first.emitText).toBe("Start ");
293
+ expect(first.bufferedRemainder).toContain("<vellum-attachment");
273
294
 
274
- const second = drainDirectiveDisplayBuffer(first.bufferedRemainder + '.png" /> done');
275
- expect(second.emitText).toBe(' done');
276
- expect(second.bufferedRemainder).toBe('');
295
+ const second = drainDirectiveDisplayBuffer(
296
+ first.bufferedRemainder + '.png" /> done',
297
+ );
298
+ expect(second.emitText).toBe(" done");
299
+ expect(second.bufferedRemainder).toBe("");
277
300
  });
278
301
 
279
302
  test('buffers trailing partial prefix "<" split across chunks', () => {
280
- const result = drainDirectiveDisplayBuffer('Hello world<');
281
- expect(result.emitText).toBe('Hello world');
282
- expect(result.bufferedRemainder).toBe('<');
303
+ const result = drainDirectiveDisplayBuffer("Hello world<");
304
+ expect(result.emitText).toBe("Hello world");
305
+ expect(result.bufferedRemainder).toBe("<");
283
306
  });
284
307
 
285
308
  test('buffers trailing partial prefix "<vel" split across chunks', () => {
286
- const result = drainDirectiveDisplayBuffer('Some text<vel');
287
- expect(result.emitText).toBe('Some text');
288
- expect(result.bufferedRemainder).toBe('<vel');
309
+ const result = drainDirectiveDisplayBuffer("Some text<vel");
310
+ expect(result.emitText).toBe("Some text");
311
+ expect(result.bufferedRemainder).toBe("<vel");
289
312
  });
290
313
 
291
314
  test('buffers trailing partial prefix "<vellum-attachmen" (one char short)', () => {
292
- const result = drainDirectiveDisplayBuffer('Data <vellum-attachmen');
293
- expect(result.emitText).toBe('Data ');
294
- expect(result.bufferedRemainder).toBe('<vellum-attachmen');
315
+ const result = drainDirectiveDisplayBuffer("Data <vellum-attachmen");
316
+ expect(result.emitText).toBe("Data ");
317
+ expect(result.bufferedRemainder).toBe("<vellum-attachmen");
295
318
  });
296
319
 
297
320
  test('does not buffer trailing "<" that is not a prefix of the tag', () => {
298
321
  // "<x" does not match any prefix of "<vellum-attachment"
299
- const result = drainDirectiveDisplayBuffer('Hello<x');
300
- expect(result.emitText).toBe('Hello<x');
301
- expect(result.bufferedRemainder).toBe('');
322
+ const result = drainDirectiveDisplayBuffer("Hello<x");
323
+ expect(result.emitText).toBe("Hello<x");
324
+ expect(result.bufferedRemainder).toBe("");
302
325
  });
303
326
 
304
- test('partial prefix reassembles into a complete directive across chunks', () => {
305
- const first = drainDirectiveDisplayBuffer('Before <vellum-at');
306
- expect(first.emitText).toBe('Before ');
307
- expect(first.bufferedRemainder).toBe('<vellum-at');
327
+ test("partial prefix reassembles into a complete directive across chunks", () => {
328
+ const first = drainDirectiveDisplayBuffer("Before <vellum-at");
329
+ expect(first.emitText).toBe("Before ");
330
+ expect(first.bufferedRemainder).toBe("<vellum-at");
308
331
 
309
332
  const second = drainDirectiveDisplayBuffer(
310
333
  first.bufferedRemainder + 'tachment path="out.png" /> After',
311
334
  );
312
- expect(second.emitText).toBe(' After');
313
- expect(second.bufferedRemainder).toBe('');
335
+ expect(second.emitText).toBe(" After");
336
+ expect(second.bufferedRemainder).toBe("");
314
337
  });
315
338
 
316
- test('emits everything when text has no partial prefix', () => {
317
- const result = drainDirectiveDisplayBuffer('No special chars here');
318
- expect(result.emitText).toBe('No special chars here');
319
- expect(result.bufferedRemainder).toBe('');
339
+ test("emits everything when text has no partial prefix", () => {
340
+ const result = drainDirectiveDisplayBuffer("No special chars here");
341
+ expect(result.emitText).toBe("No special chars here");
342
+ expect(result.bufferedRemainder).toBe("");
320
343
  });
321
344
  });
322
345
 
@@ -324,50 +347,58 @@ describe('drainDirectiveDisplayBuffer', () => {
324
347
  // contentBlocksToDrafts
325
348
  // ---------------------------------------------------------------------------
326
349
 
327
- describe('contentBlocksToDrafts', () => {
328
- test('converts image content block to draft', () => {
350
+ describe("contentBlocksToDrafts", () => {
351
+ test("converts image content block to draft", () => {
329
352
  const blocks = [
330
353
  {
331
- type: 'image',
332
- source: { type: 'base64', media_type: 'image/png', data: 'iVBORw0K' },
354
+ type: "image",
355
+ source: { type: "base64", media_type: "image/png", data: "iVBORw0K" },
333
356
  },
334
357
  ];
335
358
  const drafts = contentBlocksToDrafts(blocks);
336
359
 
337
360
  expect(drafts).toHaveLength(1);
338
- expect(drafts[0].sourceType).toBe('tool_block');
339
- expect(drafts[0].filename).toBe('tool-output.png');
340
- expect(drafts[0].mimeType).toBe('image/png');
341
- expect(drafts[0].kind).toBe('image');
361
+ expect(drafts[0].sourceType).toBe("tool_block");
362
+ expect(drafts[0].filename).toBe("tool-output.png");
363
+ expect(drafts[0].mimeType).toBe("image/png");
364
+ expect(drafts[0].kind).toBe("image");
342
365
  });
343
366
 
344
- test('converts file content block to draft', () => {
367
+ test("converts file content block to draft", () => {
345
368
  const blocks = [
346
369
  {
347
- type: 'file',
348
- source: { type: 'base64', media_type: 'application/pdf', data: 'JVBER', filename: 'report.pdf' },
370
+ type: "file",
371
+ source: {
372
+ type: "base64",
373
+ media_type: "application/pdf",
374
+ data: "JVBER",
375
+ filename: "report.pdf",
376
+ },
349
377
  },
350
378
  ];
351
379
  const drafts = contentBlocksToDrafts(blocks);
352
380
 
353
381
  expect(drafts).toHaveLength(1);
354
- expect(drafts[0].sourceType).toBe('tool_block');
355
- expect(drafts[0].filename).toBe('report.pdf');
356
- expect(drafts[0].kind).toBe('document');
382
+ expect(drafts[0].sourceType).toBe("tool_block");
383
+ expect(drafts[0].filename).toBe("report.pdf");
384
+ expect(drafts[0].kind).toBe("document");
357
385
  });
358
386
 
359
- test('skips non-image/file blocks', () => {
387
+ test("skips non-image/file blocks", () => {
360
388
  const blocks = [
361
- { type: 'text', text: 'hello' },
362
- { type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: '/9j/' } },
389
+ { type: "text", text: "hello" },
390
+ {
391
+ type: "image",
392
+ source: { type: "base64", media_type: "image/jpeg", data: "/9j/" },
393
+ },
363
394
  ];
364
395
  const drafts = contentBlocksToDrafts(blocks);
365
396
 
366
397
  expect(drafts).toHaveLength(1);
367
- expect(drafts[0].mimeType).toBe('image/jpeg');
398
+ expect(drafts[0].mimeType).toBe("image/jpeg");
368
399
  });
369
400
 
370
- test('returns empty for empty input', () => {
401
+ test("returns empty for empty input", () => {
371
402
  expect(contentBlocksToDrafts([])).toHaveLength(0);
372
403
  });
373
404
  });
@@ -376,18 +407,18 @@ describe('contentBlocksToDrafts', () => {
376
407
  // deduplicateDrafts
377
408
  // ---------------------------------------------------------------------------
378
409
 
379
- describe('validateDrafts with reversed tool drafts', () => {
380
- test('most recent tool screenshots win the attachment cap when reversed before validation', () => {
410
+ describe("validateDrafts with reversed tool drafts", () => {
411
+ test("most recent tool screenshots win the attachment cap when reversed before validation", () => {
381
412
  // Simulate a browser session producing many screenshots chronologically.
382
413
  // After reversing, the most recent (highest index) should appear first
383
414
  // and win the MAX_ASSISTANT_ATTACHMENTS cap.
384
415
  const totalScreenshots = MAX_ASSISTANT_ATTACHMENTS + 3;
385
416
  const toolDrafts = Array.from({ length: totalScreenshots }, (_, i) =>
386
417
  makeDraft({
387
- sourceType: 'tool_block',
418
+ sourceType: "tool_block",
388
419
  filename: `screenshot-step-${i}.png`,
389
- mimeType: 'image/png',
390
- kind: 'image',
420
+ mimeType: "image/png",
421
+ kind: "image",
391
422
  dataBase64: `data${i}`,
392
423
  }),
393
424
  );
@@ -399,74 +430,111 @@ describe('validateDrafts with reversed tool drafts', () => {
399
430
  expect(result.accepted).toHaveLength(MAX_ASSISTANT_ATTACHMENTS);
400
431
 
401
432
  // The accepted drafts should be the most recent screenshots (highest step numbers)
402
- const acceptedFilenames = result.accepted.map(d => d.filename);
433
+ const acceptedFilenames = result.accepted.map((d) => d.filename);
403
434
  for (let i = 0; i < MAX_ASSISTANT_ATTACHMENTS; i++) {
404
- expect(acceptedFilenames[i]).toBe(`screenshot-step-${totalScreenshots - 1 - i}.png`);
435
+ expect(acceptedFilenames[i]).toBe(
436
+ `screenshot-step-${totalScreenshots - 1 - i}.png`,
437
+ );
405
438
  }
406
439
 
407
440
  // The oldest screenshots should be dropped
408
441
  expect(result.warnings).toHaveLength(3);
409
- expect(result.warnings[0]).toContain('screenshot-step-2.png');
442
+ expect(result.warnings[0]).toContain("screenshot-step-2.png");
410
443
  });
411
444
  });
412
445
 
413
- describe('deduplicateDrafts', () => {
414
- test('removes exact duplicates with same filename and content', () => {
415
- const d = makeDraft({ filename: 'same.txt', dataBase64: 'AAAA'.repeat(20) });
416
- const result = deduplicateDrafts([d, { ...d }, makeDraft({ filename: 'other.txt' })]);
446
+ describe("deduplicateDrafts", () => {
447
+ test("removes exact duplicates with same filename and content", () => {
448
+ const d = makeDraft({
449
+ filename: "same.txt",
450
+ dataBase64: "AAAA".repeat(20),
451
+ });
452
+ const result = deduplicateDrafts([
453
+ d,
454
+ { ...d },
455
+ makeDraft({ filename: "other.txt" }),
456
+ ]);
417
457
 
418
458
  expect(result).toHaveLength(2);
419
- expect(result[0].filename).toBe('same.txt');
420
- expect(result[1].filename).toBe('other.txt');
459
+ expect(result[0].filename).toBe("same.txt");
460
+ expect(result[1].filename).toBe("other.txt");
421
461
  });
422
462
 
423
- test('drops tool_block duplicate when directive has same content', () => {
463
+ test("drops tool_block duplicate when directive has same content", () => {
424
464
  // Simulates directive tag ("chart.png") + auto-converted tool block
425
465
  // ("tool-output.png") for the same file data — the directive draft
426
466
  // should win because it appears first in the merged array.
427
- const data = 'AAAA'.repeat(20);
428
- const directive = makeDraft({ sourceType: 'sandbox_file', filename: 'chart.png', dataBase64: data });
429
- const toolBlock = makeDraft({ sourceType: 'tool_block', filename: 'tool-output.png', dataBase64: data });
467
+ const data = "AAAA".repeat(20);
468
+ const directive = makeDraft({
469
+ sourceType: "sandbox_file",
470
+ filename: "chart.png",
471
+ dataBase64: data,
472
+ });
473
+ const toolBlock = makeDraft({
474
+ sourceType: "tool_block",
475
+ filename: "tool-output.png",
476
+ dataBase64: data,
477
+ });
430
478
  const result = deduplicateDrafts([directive, toolBlock]);
431
479
 
432
480
  expect(result).toHaveLength(1);
433
- expect(result[0].filename).toBe('chart.png');
481
+ expect(result[0].filename).toBe("chart.png");
434
482
  });
435
483
 
436
- test('keeps non-tool drafts with different filenames but identical content', () => {
484
+ test("keeps non-tool drafts with different filenames but identical content", () => {
437
485
  // Two distinct files that happen to have the same bytes (e.g. empty
438
486
  // files, or before.json / after.json with identical content) should
439
487
  // both be kept.
440
- const data = 'AAAA'.repeat(20);
441
- const d1 = makeDraft({ sourceType: 'sandbox_file', filename: 'before.json', dataBase64: data });
442
- const d2 = makeDraft({ sourceType: 'sandbox_file', filename: 'after.json', dataBase64: data });
488
+ const data = "AAAA".repeat(20);
489
+ const d1 = makeDraft({
490
+ sourceType: "sandbox_file",
491
+ filename: "before.json",
492
+ dataBase64: data,
493
+ });
494
+ const d2 = makeDraft({
495
+ sourceType: "sandbox_file",
496
+ filename: "after.json",
497
+ dataBase64: data,
498
+ });
443
499
  const result = deduplicateDrafts([d1, d2]);
444
500
 
445
501
  expect(result).toHaveLength(2);
446
- expect(result[0].filename).toBe('before.json');
447
- expect(result[1].filename).toBe('after.json');
448
- });
449
-
450
- test('keeps drafts with same filename but different content', () => {
451
- const d1 = makeDraft({ filename: 'file.txt', dataBase64: 'AAAA'.repeat(20) });
452
- const d2 = makeDraft({ filename: 'file.txt', dataBase64: 'BBBB'.repeat(20) });
502
+ expect(result[0].filename).toBe("before.json");
503
+ expect(result[1].filename).toBe("after.json");
504
+ });
505
+
506
+ test("keeps drafts with same filename but different content", () => {
507
+ const d1 = makeDraft({
508
+ filename: "file.txt",
509
+ dataBase64: "AAAA".repeat(20),
510
+ });
511
+ const d2 = makeDraft({
512
+ filename: "file.txt",
513
+ dataBase64: "BBBB".repeat(20),
514
+ });
453
515
  const result = deduplicateDrafts([d1, d2]);
454
516
 
455
517
  expect(result).toHaveLength(2);
456
518
  });
457
519
 
458
- test('keeps drafts with same filename and same prefix but different content', () => {
520
+ test("keeps drafts with same filename and same prefix but different content", () => {
459
521
  // Shared 64-char prefix but different suffix — old prefix-based dedup
460
522
  // would incorrectly drop the second draft.
461
- const sharedPrefix = 'A'.repeat(64);
462
- const d1 = makeDraft({ filename: 'tool-output.png', dataBase64: sharedPrefix + 'XXXX' });
463
- const d2 = makeDraft({ filename: 'tool-output.png', dataBase64: sharedPrefix + 'YYYY' });
523
+ const sharedPrefix = "A".repeat(64);
524
+ const d1 = makeDraft({
525
+ filename: "tool-output.png",
526
+ dataBase64: sharedPrefix + "XXXX",
527
+ });
528
+ const d2 = makeDraft({
529
+ filename: "tool-output.png",
530
+ dataBase64: sharedPrefix + "YYYY",
531
+ });
464
532
  const result = deduplicateDrafts([d1, d2]);
465
533
 
466
534
  expect(result).toHaveLength(2);
467
535
  });
468
536
 
469
- test('returns empty for empty input', () => {
537
+ test("returns empty for empty input", () => {
470
538
  expect(deduplicateDrafts([])).toHaveLength(0);
471
539
  });
472
540
  });