vellum 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -3,8 +3,10 @@ import type { Message } from '../providers/types.js';
3
3
  import {
4
4
  applyRuntimeInjections,
5
5
  injectChannelCapabilityContext,
6
+ injectTemporalContext,
6
7
  resolveChannelCapabilities,
7
8
  stripChannelCapabilityContext,
9
+ stripTemporalContext,
8
10
  } from '../daemon/session-runtime-assembly.js';
9
11
  import type { ChannelCapabilities } from '../daemon/session-runtime-assembly.js';
10
12
  import { buildChannelAwarenessSection } from '../config/system-prompt.js';
@@ -313,3 +315,162 @@ describe('trust-gating via channel capabilities', () => {
313
315
  expect(injected).toContain('desktop app');
314
316
  });
315
317
  });
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // injectTemporalContext
321
+ // ---------------------------------------------------------------------------
322
+
323
+ describe('injectTemporalContext', () => {
324
+ const baseUserMessage: Message = {
325
+ role: 'user',
326
+ content: [{ type: 'text', text: 'Plan a trip for next weekend' }],
327
+ };
328
+
329
+ const sampleContext = '<temporal_context>\nToday: 2026-02-18 (Wednesday)\nTimezone: UTC\n</temporal_context>';
330
+
331
+ test('prepends temporal context block to user message', () => {
332
+ const result = injectTemporalContext(baseUserMessage, sampleContext);
333
+ expect(result.content.length).toBe(2);
334
+ const injected = result.content[0];
335
+ expect(injected.type).toBe('text');
336
+ expect((injected as { type: 'text'; text: string }).text).toContain('<temporal_context>');
337
+ expect((injected as { type: 'text'; text: string }).text).toContain('2026-02-18');
338
+ });
339
+
340
+ test('preserves original message content', () => {
341
+ const result = injectTemporalContext(baseUserMessage, sampleContext);
342
+ const lastBlock = result.content[result.content.length - 1];
343
+ expect((lastBlock as { type: 'text'; text: string }).text).toBe('Plan a trip for next weekend');
344
+ });
345
+ });
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // stripTemporalContext
349
+ // ---------------------------------------------------------------------------
350
+
351
+ describe('stripTemporalContext', () => {
352
+ test('strips temporal_context blocks from user messages', () => {
353
+ const messages: Message[] = [
354
+ {
355
+ role: 'user',
356
+ content: [
357
+ { type: 'text', text: '<temporal_context>\nToday: 2026-02-18\n</temporal_context>' },
358
+ { type: 'text', text: 'Hello' },
359
+ ],
360
+ },
361
+ {
362
+ role: 'assistant',
363
+ content: [{ type: 'text', text: 'Hi there' }],
364
+ },
365
+ ];
366
+
367
+ const result = stripTemporalContext(messages);
368
+
369
+ expect(result.length).toBe(2);
370
+ expect(result[0].content.length).toBe(1);
371
+ expect((result[0].content[0] as { type: 'text'; text: string }).text).toBe('Hello');
372
+ // Assistant message untouched
373
+ expect(result[1].content.length).toBe(1);
374
+ });
375
+
376
+ test('removes user messages that only contain temporal_context', () => {
377
+ const messages: Message[] = [
378
+ {
379
+ role: 'user',
380
+ content: [
381
+ { type: 'text', text: '<temporal_context>\nToday: 2026-02-18\n</temporal_context>' },
382
+ ],
383
+ },
384
+ ];
385
+
386
+ const result = stripTemporalContext(messages);
387
+ expect(result.length).toBe(0);
388
+ });
389
+
390
+ test('does not touch unrelated blocks', () => {
391
+ const messages: Message[] = [
392
+ {
393
+ role: 'user',
394
+ content: [
395
+ { type: 'text', text: '<channel_capabilities>\nchannel: dashboard\n</channel_capabilities>' },
396
+ { type: 'text', text: 'Hello' },
397
+ ],
398
+ },
399
+ ];
400
+
401
+ const result = stripTemporalContext(messages);
402
+ expect(result.length).toBe(1);
403
+ expect(result[0]).toBe(messages[0]); // Same reference — untouched
404
+ });
405
+
406
+ test('leaves messages without temporal_context untouched', () => {
407
+ const messages: Message[] = [
408
+ {
409
+ role: 'user',
410
+ content: [{ type: 'text', text: 'Normal message' }],
411
+ },
412
+ ];
413
+
414
+ const result = stripTemporalContext(messages);
415
+ expect(result.length).toBe(1);
416
+ expect(result[0]).toBe(messages[0]);
417
+ });
418
+
419
+ test('preserves user-authored text that starts with <temporal_context> but not the injected prefix', () => {
420
+ const messages: Message[] = [
421
+ {
422
+ role: 'user',
423
+ content: [
424
+ { type: 'text', text: '<temporal_context>some user XML content</temporal_context>' },
425
+ { type: 'text', text: 'Hello' },
426
+ ],
427
+ },
428
+ ];
429
+
430
+ const result = stripTemporalContext(messages);
431
+ expect(result.length).toBe(1);
432
+ expect(result[0]).toBe(messages[0]); // Same reference — untouched
433
+ });
434
+ });
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // applyRuntimeInjections with temporalContext
438
+ // ---------------------------------------------------------------------------
439
+
440
+ describe('applyRuntimeInjections with temporalContext', () => {
441
+ const baseMessages: Message[] = [
442
+ {
443
+ role: 'user',
444
+ content: [{ type: 'text', text: 'When is next weekend?' }],
445
+ },
446
+ ];
447
+
448
+ const sampleContext = '<temporal_context>\nToday: 2026-02-18 (Wednesday)\n</temporal_context>';
449
+
450
+ test('injects temporal context when provided', () => {
451
+ const result = applyRuntimeInjections(baseMessages, {
452
+ temporalContext: sampleContext,
453
+ });
454
+
455
+ expect(result.length).toBe(1);
456
+ expect(result[0].content.length).toBe(2);
457
+ const injected = result[0].content[0];
458
+ expect((injected as { type: 'text'; text: string }).text).toContain('<temporal_context>');
459
+ });
460
+
461
+ test('does not inject when temporalContext is null', () => {
462
+ const result = applyRuntimeInjections(baseMessages, {
463
+ temporalContext: null,
464
+ });
465
+
466
+ expect(result.length).toBe(1);
467
+ expect(result[0].content.length).toBe(1);
468
+ });
469
+
470
+ test('does not inject when temporalContext is omitted', () => {
471
+ const result = applyRuntimeInjections(baseMessages, {});
472
+
473
+ expect(result.length).toBe(1);
474
+ expect(result[0].content.length).toBe(1);
475
+ });
476
+ });
@@ -0,0 +1,104 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type {
3
+ CardSurfaceData,
4
+ ServerMessage,
5
+ SurfaceData,
6
+ SurfaceType,
7
+ UiSurfaceShow,
8
+ UiSurfaceUpdate,
9
+ } from '../daemon/ipc-protocol.js';
10
+ import {
11
+ surfaceProxyResolver,
12
+ type SurfaceSessionContext,
13
+ } from '../daemon/session-surfaces.js';
14
+
15
+ function makeContext(sent: ServerMessage[] = []): SurfaceSessionContext {
16
+ return {
17
+ conversationId: 'session-1',
18
+ traceEmitter: { emit: () => {} },
19
+ sendToClient: (msg) => sent.push(msg),
20
+ pendingSurfaceActions: new Map<string, { surfaceType: SurfaceType }>(),
21
+ lastSurfaceAction: new Map<string, { actionId: string; data?: Record<string, unknown> }>(),
22
+ surfaceState: new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>(),
23
+ surfaceUndoStacks: new Map<string, string[]>(),
24
+ currentTurnSurfaces: [],
25
+ isProcessing: () => false,
26
+ enqueueMessage: () => ({ queued: false, requestId: 'req-1' }),
27
+ getQueueDepth: () => 0,
28
+ processMessage: async () => 'ok',
29
+ };
30
+ }
31
+
32
+ describe('task_progress surface compatibility', () => {
33
+ test('ui_show maps legacy top-level task_progress fields into card data', async () => {
34
+ const sent: ServerMessage[] = [];
35
+ const ctx = makeContext(sent);
36
+
37
+ const result = await surfaceProxyResolver(ctx, 'ui_show', {
38
+ surface_type: 'card',
39
+ title: 'Ordering from DoorDash',
40
+ data: {},
41
+ template: 'task_progress',
42
+ templateData: {
43
+ status: 'in_progress',
44
+ steps: [
45
+ { label: 'Search restaurants', status: 'in_progress' },
46
+ { label: 'Browse menu', status: 'pending' },
47
+ ],
48
+ },
49
+ });
50
+
51
+ expect(result.isError).toBe(false);
52
+
53
+ const showMessage = sent.find((msg): msg is UiSurfaceShow => msg.type === 'ui_surface_show');
54
+ expect(showMessage).toBeDefined();
55
+ if (!showMessage || showMessage.surfaceType !== 'card') return;
56
+
57
+ const card = showMessage.data as CardSurfaceData;
58
+ expect(card.template).toBe('task_progress');
59
+ expect(card.title).toBe('Ordering from DoorDash');
60
+ expect(card.body).toBe('');
61
+ expect((card.templateData as Record<string, unknown>).status).toBe('in_progress');
62
+ });
63
+
64
+ test('ui_update normalizes top-level task_progress fields into templateData', async () => {
65
+ const sent: ServerMessage[] = [];
66
+ const ctx = makeContext(sent);
67
+ const existingCard: CardSurfaceData = {
68
+ title: 'Ordering from DoorDash',
69
+ body: '',
70
+ template: 'task_progress',
71
+ templateData: {
72
+ title: 'Ordering from DoorDash',
73
+ status: 'in_progress',
74
+ steps: [
75
+ { label: 'Search restaurants', status: 'completed' },
76
+ { label: 'Browse menu', status: 'in_progress' },
77
+ { label: 'Add to cart', status: 'pending' },
78
+ ],
79
+ },
80
+ };
81
+
82
+ ctx.surfaceState.set('surface-1', { surfaceType: 'card', data: existingCard });
83
+
84
+ const result = await surfaceProxyResolver(ctx, 'ui_update', {
85
+ surface_id: 'surface-1',
86
+ data: {
87
+ status: 'completed',
88
+ },
89
+ });
90
+
91
+ expect(result.isError).toBe(false);
92
+
93
+ const updateMessage = sent.find((msg): msg is UiSurfaceUpdate => msg.type === 'ui_surface_update');
94
+ expect(updateMessage).toBeDefined();
95
+ if (!updateMessage) return;
96
+
97
+ const updatedCard = updateMessage.data as CardSurfaceData & Record<string, unknown>;
98
+ expect(updatedCard.template).toBe('task_progress');
99
+ expect('status' in updatedCard).toBe(false);
100
+ const templateData = updatedCard.templateData as Record<string, unknown>;
101
+ expect(templateData.status).toBe('completed');
102
+ expect(Array.isArray(templateData.steps)).toBe(true);
103
+ });
104
+ });
@@ -48,7 +48,7 @@ const STORE_PATH = join(testDir, 'keys.enc');
48
48
  // ── Imports (after mocks) ───────────────────────────────────────────
49
49
 
50
50
  import { createMockSignupServer, type MockSignupServer } from './fixtures/mock-signup-server.js';
51
- import { initializeDb, getDb } from '../memory/db.js';
51
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
52
52
  import {
53
53
  createAccount,
54
54
  listAccounts,
@@ -97,6 +97,7 @@ beforeAll(async () => {
97
97
  });
98
98
 
99
99
  afterAll(async () => {
100
+ resetDb();
100
101
  await executeBrowserClose({ close_all_pages: true }, ctx);
101
102
  await server.stop();
102
103
  _setMetadataPath(null);
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Skill Projection Benchmark
3
+ *
4
+ * Measures projectSkillTools() latency across conversation sizes and caching scenarios.
5
+ *
6
+ * Baseline targets:
7
+ * - Cold projection (100 msgs / 3 skills): < 50ms
8
+ * - Cached projection (no change): < 10ms
9
+ * - Cold projection (1000 msgs / 5 skills): < 100ms
10
+ * - Incremental scan (10 new msgs): < 20ms
11
+ */
12
+ import { describe, test, expect, mock } from 'bun:test';
13
+ import type { Message } from '../providers/types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Mocks — must be registered before importing the module under test
17
+ // ---------------------------------------------------------------------------
18
+
19
+ mock.module('../util/logger.js', () => ({
20
+ getLogger: () =>
21
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
22
+ }));
23
+
24
+ // Skill catalog: returns a configurable list of fake skills
25
+ let catalogSkillIds: string[] = [];
26
+ mock.module('../config/skills.js', () => ({
27
+ loadSkillCatalog: () =>
28
+ catalogSkillIds.map((id) => ({
29
+ id,
30
+ name: id,
31
+ description: `Mock skill ${id}`,
32
+ directoryPath: `/tmp/fake-skills/${id}`,
33
+ skillFilePath: `/tmp/fake-skills/${id}/SKILL.md`,
34
+ bundled: false,
35
+ userInvocable: false,
36
+ })),
37
+ }));
38
+
39
+ mock.module('../skills/tool-manifest.js', () => ({
40
+ parseToolManifestFile: (path: string) => {
41
+ // Extract skill id from the path /tmp/fake-skills/<id>/TOOLS.json
42
+ const parts = path.split('/');
43
+ const skillId = parts[parts.length - 2];
44
+ return {
45
+ version: 1,
46
+ tools: [
47
+ {
48
+ name: `${skillId}_tool_a`,
49
+ description: `Tool A for ${skillId}`,
50
+ input_schema: { type: 'object', properties: {} },
51
+ },
52
+ {
53
+ name: `${skillId}_tool_b`,
54
+ description: `Tool B for ${skillId}`,
55
+ input_schema: { type: 'object', properties: {} },
56
+ },
57
+ ],
58
+ };
59
+ },
60
+ }));
61
+
62
+ mock.module('../skills/version-hash.js', () => ({
63
+ computeSkillVersionHash: () => 'v1:deadbeef',
64
+ }));
65
+
66
+ // Mock createSkillToolsFromManifest to return lightweight Tool-like objects
67
+ mock.module('../tools/skills/skill-tool-factory.js', () => ({
68
+ createSkillToolsFromManifest: (
69
+ entries: Array<{ name: string; description: string; input_schema: object }>,
70
+ skillId: string,
71
+ _skillDir: string,
72
+ versionHash: string,
73
+ bundled?: boolean,
74
+ ) =>
75
+ entries.map((e) => ({
76
+ name: e.name,
77
+ description: e.description,
78
+ category: 'skill',
79
+ defaultRiskLevel: 'low',
80
+ origin: 'skill' as const,
81
+ ownerSkillId: skillId,
82
+ ownerSkillVersionHash: versionHash,
83
+ ownerSkillBundled: bundled,
84
+ getDefinition: () => ({
85
+ name: e.name,
86
+ description: e.description,
87
+ input_schema: e.input_schema,
88
+ }),
89
+ execute: async () => ({ content: '', isError: false }),
90
+ })),
91
+ }));
92
+
93
+ // existsSync mock — TOOLS.json always exists for fake skills
94
+ mock.module('node:fs', () => ({
95
+ existsSync: () => true,
96
+ }));
97
+
98
+ mock.module('../tools/registry.js', () => ({
99
+ registerSkillTools: () => {},
100
+ unregisterSkillTools: () => {},
101
+ getTool: () => undefined,
102
+ }));
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Import module under test (after mocks)
106
+ // ---------------------------------------------------------------------------
107
+
108
+ const { projectSkillTools } = await import('../daemon/session-skill-tools.js');
109
+ type SkillProjectionCache = import('../daemon/session-skill-tools.js').SkillProjectionCache;
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Helpers
113
+ // ---------------------------------------------------------------------------
114
+
115
+ /**
116
+ * Build a synthetic conversation history with interleaved user/assistant
117
+ * messages and skill_load tool-use markers for the given skill IDs.
118
+ *
119
+ * Skill activations are spread evenly across the history.
120
+ */
121
+ function buildHistory(messageCount: number, skillIds: string[]): Message[] {
122
+ const msgs: Message[] = [];
123
+ const activationInterval = Math.max(1, Math.floor(messageCount / skillIds.length));
124
+
125
+ for (let i = 0; i < messageCount; i++) {
126
+ // Every other message is user/assistant
127
+ if (i % 2 === 0) {
128
+ msgs.push({
129
+ role: 'user',
130
+ content: [
131
+ { type: 'text', text: `User message ${i} about project tasks.` },
132
+ ],
133
+ });
134
+ } else {
135
+ const blocks: Message['content'] = [
136
+ { type: 'text', text: `Assistant response ${i} with analysis.` },
137
+ ];
138
+
139
+ // Inject a skill_load tool_use at the activation point
140
+ const skillIndex = Math.floor(i / activationInterval);
141
+ if (skillIndex < skillIds.length) {
142
+ const skillId = skillIds[skillIndex];
143
+ const toolUseId = `tu-${skillId}-${i}`;
144
+ blocks.push({
145
+ type: 'tool_use',
146
+ id: toolUseId,
147
+ name: 'skill_load',
148
+ input: { skill_id: skillId },
149
+ });
150
+ }
151
+
152
+ msgs.push({ role: 'assistant', content: blocks });
153
+ }
154
+ }
155
+
156
+ // Add matching tool_result messages for each skill_load tool_use
157
+ for (const msg of [...msgs]) {
158
+ for (const block of msg.content) {
159
+ if (block.type === 'tool_use' && block.name === 'skill_load') {
160
+ const skillId = (block.input as Record<string, string>).skill_id;
161
+ msgs.push({
162
+ role: 'user',
163
+ content: [
164
+ {
165
+ type: 'tool_result',
166
+ tool_use_id: block.id,
167
+ content: `<loaded_skill id="${skillId}" version="v1:deadbeef" />`,
168
+ },
169
+ ],
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ return msgs;
176
+ }
177
+
178
+ function timeMs(fn: () => void): number {
179
+ const start = performance.now();
180
+ fn();
181
+ return performance.now() - start;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Benchmarks
186
+ // ---------------------------------------------------------------------------
187
+
188
+ describe('Skill projection benchmark', () => {
189
+ test('cold projection: 100 messages / 3 skills < 50ms', () => {
190
+ const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
191
+ catalogSkillIds = skillIds;
192
+ const history = buildHistory(100, skillIds);
193
+
194
+ const elapsed = timeMs(() => {
195
+ const result = projectSkillTools(history);
196
+ expect(result.toolDefinitions.length).toBeGreaterThan(0);
197
+ expect(result.allowedToolNames.size).toBeGreaterThan(0);
198
+ });
199
+
200
+ console.log(` Cold projection (100 msgs / 3 skills): ${elapsed.toFixed(2)}ms`);
201
+ expect(elapsed).toBeLessThan(50);
202
+ });
203
+
204
+ test('cached projection (no change) < 10ms', () => {
205
+ const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
206
+ catalogSkillIds = skillIds;
207
+ const history = buildHistory(100, skillIds);
208
+ const cache: SkillProjectionCache = {};
209
+ const prevActive = new Map<string, string>();
210
+
211
+ // Warm the cache
212
+ const warmResult = projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
213
+
214
+ // Snapshot cache object references after warm-up
215
+ const derivedAfterWarm = cache.derived!;
216
+ const entriesAfterWarm = cache.derived!.entries;
217
+ const seenIdsAfterWarm = cache.derived!.seenIds;
218
+
219
+ // Second call with identical history — should hit cache fast path
220
+ let cachedResult: ReturnType<typeof projectSkillTools> | undefined;
221
+ const elapsed = timeMs(() => {
222
+ cachedResult = projectSkillTools(history, {
223
+ cache,
224
+ previouslyActiveSkillIds: prevActive,
225
+ });
226
+ });
227
+
228
+ // Assert cache was populated and covers all messages
229
+ expect(cache.derived).toBeDefined();
230
+ expect(cache.derived!.messageCount).toBe(history.length);
231
+
232
+ // Assert the cache object is reused (same reference, not rebuilt)
233
+ expect(cache.derived).toBe(derivedAfterWarm);
234
+ expect(cache.derived!.entries).toBe(entriesAfterWarm);
235
+ expect(cache.derived!.seenIds).toBe(seenIdsAfterWarm);
236
+
237
+ // Assert tool definitions are identical between warm and cached calls
238
+ expect(cachedResult!.toolDefinitions.length).toBe(warmResult.toolDefinitions.length);
239
+ const warmNames = warmResult.toolDefinitions.map((t) => t.name).sort();
240
+ const cachedNames = cachedResult!.toolDefinitions.map((t) => t.name).sort();
241
+ expect(cachedNames).toEqual(warmNames);
242
+
243
+ console.log(` Cached projection (no change): ${elapsed.toFixed(2)}ms`);
244
+ expect(elapsed).toBeLessThan(10);
245
+ });
246
+
247
+ test('cache hit rate is 100% when history unchanged', () => {
248
+ const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
249
+ catalogSkillIds = skillIds;
250
+ const history = buildHistory(100, skillIds);
251
+ const cache: SkillProjectionCache = {};
252
+ const prevActive = new Map<string, string>();
253
+
254
+ // First call populates the cache
255
+ const firstResult = projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
256
+ expect(cache.derived).toBeDefined();
257
+ const snapshotDerived = cache.derived!;
258
+ const snapshotEntries = cache.derived!.entries;
259
+ const snapshotSeenIds = cache.derived!.seenIds;
260
+
261
+ // Run multiple subsequent calls with unchanged history
262
+ for (let i = 0; i < 5; i++) {
263
+ const result = projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
264
+
265
+ // Cache objects must be the same references (reused, not rebuilt)
266
+ expect(cache.derived).toBe(snapshotDerived);
267
+ expect(cache.derived!.entries).toBe(snapshotEntries);
268
+ expect(cache.derived!.seenIds).toBe(snapshotSeenIds);
269
+
270
+ // Tool definitions must match the first call exactly
271
+ expect(result.toolDefinitions.length).toBe(firstResult.toolDefinitions.length);
272
+ expect(result.toolDefinitions.map((t) => t.name).sort()).toEqual(
273
+ firstResult.toolDefinitions.map((t) => t.name).sort(),
274
+ );
275
+ }
276
+ });
277
+
278
+ test('cold projection: 1000 messages / 5 skills < 100ms', () => {
279
+ const skillIds = [
280
+ 'skill-alpha',
281
+ 'skill-beta',
282
+ 'skill-gamma',
283
+ 'skill-delta',
284
+ 'skill-epsilon',
285
+ ];
286
+ catalogSkillIds = skillIds;
287
+ const history = buildHistory(1000, skillIds);
288
+
289
+ const elapsed = timeMs(() => {
290
+ const result = projectSkillTools(history);
291
+ expect(result.toolDefinitions.length).toBeGreaterThan(0);
292
+ expect(result.allowedToolNames.size).toBeGreaterThan(0);
293
+ });
294
+
295
+ console.log(` Cold projection (1000 msgs / 5 skills): ${elapsed.toFixed(2)}ms`);
296
+ expect(elapsed).toBeLessThan(100);
297
+ });
298
+
299
+ test('incremental scan (10 new messages appended) < 20ms', () => {
300
+ const skillIds = ['skill-alpha', 'skill-beta', 'skill-gamma'];
301
+ catalogSkillIds = skillIds;
302
+ const history = buildHistory(100, skillIds);
303
+ const cache: SkillProjectionCache = {};
304
+ const prevActive = new Map<string, string>();
305
+
306
+ // Warm the cache
307
+ projectSkillTools(history, { cache, previouslyActiveSkillIds: prevActive });
308
+
309
+ // Append 10 new plain messages (no new skill activations)
310
+ for (let i = 0; i < 10; i++) {
311
+ history.push({
312
+ role: i % 2 === 0 ? 'user' : 'assistant',
313
+ content: [{ type: 'text', text: `Follow-up message ${i}.` }],
314
+ });
315
+ }
316
+
317
+ const elapsed = timeMs(() => {
318
+ const result = projectSkillTools(history, {
319
+ cache,
320
+ previouslyActiveSkillIds: prevActive,
321
+ });
322
+ expect(result.toolDefinitions.length).toBeGreaterThan(0);
323
+ });
324
+
325
+ console.log(` Incremental scan (10 new msgs): ${elapsed.toFixed(2)}ms`);
326
+ expect(elapsed).toBeLessThan(20);
327
+ });
328
+ });