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
@@ -0,0 +1,482 @@
1
+ import { describe, test, expect, beforeEach, afterAll } from 'bun:test';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { mock } from 'bun:test';
6
+
7
+ const testDir = mkdtempSync(join(tmpdir(), 'schedule-store-test-'));
8
+
9
+ mock.module('../util/platform.js', () => ({
10
+ getDataDir: () => testDir,
11
+ isMacOS: () => process.platform === 'darwin',
12
+ isLinux: () => process.platform === 'linux',
13
+ isWindows: () => process.platform === 'win32',
14
+ getSocketPath: () => join(testDir, 'test.sock'),
15
+ getPidPath: () => join(testDir, 'test.pid'),
16
+ getDbPath: () => join(testDir, 'test.db'),
17
+ getLogPath: () => join(testDir, 'test.log'),
18
+ ensureDataDir: () => {},
19
+ migrateToDataLayout: () => {},
20
+ migrateToWorkspaceLayout: () => {},
21
+ }));
22
+
23
+ mock.module('../util/logger.js', () => ({
24
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
25
+ get: () => () => {},
26
+ }),
27
+ }));
28
+
29
+ import { initializeDb, getDb, resetDb } from '../memory/db.js';
30
+ import {
31
+ createSchedule,
32
+ getSchedule,
33
+ updateSchedule,
34
+ claimDueSchedules,
35
+ } from '../schedule/schedule-store.js';
36
+
37
+ initializeDb();
38
+
39
+ /** Access the underlying bun:sqlite Database for raw parameterized queries. */
40
+ function getRawDb(): import('bun:sqlite').Database {
41
+ return (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
42
+ }
43
+
44
+ afterAll(() => {
45
+ resetDb();
46
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
47
+ });
48
+
49
+ // ── Cron backward compatibility ─────────────────────────────────────
50
+
51
+ describe('createSchedule (cron, legacy API)', () => {
52
+ beforeEach(() => {
53
+ const db = getDb();
54
+ db.run('DELETE FROM cron_runs');
55
+ db.run('DELETE FROM cron_jobs');
56
+ });
57
+
58
+ test('creates a cron schedule using only cronExpression', () => {
59
+ const job = createSchedule({
60
+ name: 'Morning ping',
61
+ cronExpression: '0 9 * * *',
62
+ message: 'good morning',
63
+ });
64
+
65
+ expect(job.syntax).toBe('cron');
66
+ expect(job.expression).toBe('0 9 * * *');
67
+ expect(job.cronExpression).toBe('0 9 * * *');
68
+ expect(job.nextRunAt).toBeGreaterThan(Date.now() - 1000);
69
+ expect(job.enabled).toBe(true);
70
+ });
71
+
72
+ test('persisted cron schedule is retrievable with new fields', () => {
73
+ const job = createSchedule({
74
+ name: 'Hourly',
75
+ cronExpression: '0 * * * *',
76
+ message: 'hourly check',
77
+ });
78
+
79
+ const retrieved = getSchedule(job.id);
80
+ expect(retrieved).not.toBeNull();
81
+ expect(retrieved!.syntax).toBe('cron');
82
+ expect(retrieved!.expression).toBe('0 * * * *');
83
+ expect(retrieved!.cronExpression).toBe('0 * * * *');
84
+ });
85
+
86
+ test('stores schedule_syntax in the DB row', () => {
87
+ const job = createSchedule({
88
+ name: 'Syntax check',
89
+ cronExpression: '*/5 * * * *',
90
+ message: 'test',
91
+ });
92
+
93
+ const raw = getRawDb().query('SELECT schedule_syntax FROM cron_jobs WHERE id = ?').get(job.id) as { schedule_syntax: string } | null;
94
+ expect(raw).not.toBeNull();
95
+ expect(raw!.schedule_syntax).toBe('cron');
96
+ });
97
+
98
+ test('rejects invalid cron expression', () => {
99
+ expect(() => createSchedule({
100
+ name: 'Bad cron',
101
+ cronExpression: 'not-a-cron',
102
+ message: 'fail',
103
+ })).toThrow();
104
+ });
105
+ });
106
+
107
+ // ── RRULE schedule creation ──────────────────────────────────────────
108
+
109
+ describe('createSchedule (RRULE)', () => {
110
+ beforeEach(() => {
111
+ const db = getDb();
112
+ db.run('DELETE FROM cron_runs');
113
+ db.run('DELETE FROM cron_jobs');
114
+ });
115
+
116
+ test('creates an RRULE schedule with syntax + expression', () => {
117
+ const rrule = 'DTSTART:20260101T090000Z\nRRULE:FREQ=DAILY;INTERVAL=1';
118
+ const job = createSchedule({
119
+ name: 'Daily RRULE',
120
+ cronExpression: rrule,
121
+ message: 'rrule test',
122
+ syntax: 'rrule',
123
+ expression: rrule,
124
+ });
125
+
126
+ expect(job.syntax).toBe('rrule');
127
+ expect(job.expression).toBe(rrule);
128
+ expect(job.cronExpression).toBe(rrule);
129
+ expect(job.nextRunAt).toBeGreaterThan(0);
130
+ });
131
+
132
+ test('stores rrule syntax in DB', () => {
133
+ const rrule = 'DTSTART:20260101T090000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO';
134
+ const job = createSchedule({
135
+ name: 'Weekly RRULE',
136
+ cronExpression: rrule,
137
+ message: 'weekly',
138
+ syntax: 'rrule',
139
+ expression: rrule,
140
+ });
141
+
142
+ const raw = getRawDb().query('SELECT schedule_syntax, cron_expression FROM cron_jobs WHERE id = ?').get(job.id) as { schedule_syntax: string; cron_expression: string } | null;
143
+ expect(raw).not.toBeNull();
144
+ expect(raw!.schedule_syntax).toBe('rrule');
145
+ expect(raw!.cron_expression).toBe(rrule);
146
+ });
147
+
148
+ test('rejects RRULE without DTSTART', () => {
149
+ expect(() => createSchedule({
150
+ name: 'No dtstart',
151
+ cronExpression: 'RRULE:FREQ=DAILY',
152
+ message: 'fail',
153
+ syntax: 'rrule',
154
+ expression: 'RRULE:FREQ=DAILY',
155
+ })).toThrow();
156
+ });
157
+ });
158
+
159
+ // ── RRULE set expressions (RDATE, EXDATE, multi-RRULE) ──────────────
160
+
161
+ describe('createSchedule (RRULE set)', () => {
162
+ beforeEach(() => {
163
+ const db = getDb();
164
+ db.run('DELETE FROM cron_runs');
165
+ db.run('DELETE FROM cron_jobs');
166
+ });
167
+
168
+ test('creates schedule with RRULE + EXDATE set expression', () => {
169
+ const expression = [
170
+ 'DTSTART:20250101T090000Z',
171
+ 'RRULE:FREQ=DAILY;INTERVAL=1',
172
+ 'EXDATE:20250102T090000Z',
173
+ ].join('\n');
174
+
175
+ const job = createSchedule({
176
+ name: 'Daily with exclusion',
177
+ cronExpression: expression,
178
+ message: 'set test',
179
+ syntax: 'rrule',
180
+ expression,
181
+ });
182
+
183
+ expect(job.syntax).toBe('rrule');
184
+ expect(job.expression).toContain('EXDATE');
185
+ expect(job.nextRunAt).toBeGreaterThan(0);
186
+ });
187
+
188
+ test('creates schedule with RRULE + RDATE set expression', () => {
189
+ const expression = [
190
+ 'DTSTART:20250101T090000Z',
191
+ 'RRULE:FREQ=WEEKLY;BYDAY=MO',
192
+ 'RDATE:20250115T090000Z',
193
+ ].join('\n');
194
+
195
+ const job = createSchedule({
196
+ name: 'Weekly with extra dates',
197
+ cronExpression: expression,
198
+ message: 'rdate test',
199
+ syntax: 'rrule',
200
+ expression,
201
+ });
202
+
203
+ expect(job.syntax).toBe('rrule');
204
+ expect(job.expression).toContain('RDATE');
205
+ });
206
+
207
+ test('preserves full set expression text in DB without collapsing', () => {
208
+ const expression = [
209
+ 'DTSTART:20250101T090000Z',
210
+ 'RRULE:FREQ=DAILY;INTERVAL=1',
211
+ 'EXDATE:20250102T090000Z',
212
+ 'EXDATE:20250103T090000Z',
213
+ ].join('\n');
214
+
215
+ const job = createSchedule({
216
+ name: 'Multi-EXDATE',
217
+ cronExpression: expression,
218
+ message: 'preserve test',
219
+ syntax: 'rrule',
220
+ expression,
221
+ });
222
+
223
+ const raw = getRawDb().query('SELECT cron_expression FROM cron_jobs WHERE id = ?').get(job.id) as { cron_expression: string };
224
+ // The full expression including all EXDATE lines should be stored
225
+ expect(raw.cron_expression).toContain('EXDATE:20250102T090000Z');
226
+ expect(raw.cron_expression).toContain('EXDATE:20250103T090000Z');
227
+ });
228
+
229
+ test('retrieved set schedule matches what was stored', () => {
230
+ const expression = [
231
+ 'DTSTART:20250101T090000Z',
232
+ 'RRULE:FREQ=DAILY;INTERVAL=1',
233
+ 'EXDATE:20250105T090000Z',
234
+ ].join('\n');
235
+
236
+ const job = createSchedule({
237
+ name: 'Retrieve set',
238
+ cronExpression: expression,
239
+ message: 'retrieve test',
240
+ syntax: 'rrule',
241
+ expression,
242
+ });
243
+
244
+ const retrieved = getSchedule(job.id);
245
+ expect(retrieved).not.toBeNull();
246
+ expect(retrieved!.syntax).toBe('rrule');
247
+ expect(retrieved!.expression).toBe(expression);
248
+ expect(retrieved!.expression).toContain('EXDATE');
249
+ });
250
+ });
251
+
252
+ // ── claimDueSchedules with RRULE sets ────────────────────────────────
253
+
254
+ describe('claimDueSchedules (RRULE set)', () => {
255
+ beforeEach(() => {
256
+ const db = getDb();
257
+ db.run('DELETE FROM cron_runs');
258
+ db.run('DELETE FROM cron_jobs');
259
+ });
260
+
261
+ test('claims RRULE set schedule and correctly advances nextRunAt past exclusions', () => {
262
+ const expression = [
263
+ 'DTSTART:20250101T000000Z',
264
+ 'RRULE:FREQ=MINUTELY;INTERVAL=1',
265
+ 'EXDATE:20250101T000100Z',
266
+ ].join('\n');
267
+
268
+ const job = createSchedule({
269
+ name: 'Claim set test',
270
+ cronExpression: expression,
271
+ message: 'claim set',
272
+ syntax: 'rrule',
273
+ expression,
274
+ });
275
+
276
+ // Force due
277
+ getRawDb().run('UPDATE cron_jobs SET next_run_at = ? WHERE id = ?', [Date.now() - 1000, job.id]);
278
+
279
+ const now = Date.now();
280
+ const claimed = claimDueSchedules(now);
281
+ expect(claimed.length).toBe(1);
282
+ expect(claimed[0].syntax).toBe('rrule');
283
+ // nextRunAt should advance to a future time
284
+ expect(claimed[0].nextRunAt).toBeGreaterThanOrEqual(now);
285
+ });
286
+ });
287
+
288
+ // ── updateSchedule with syntax/expression ────────────────────────────
289
+
290
+ describe('updateSchedule', () => {
291
+ beforeEach(() => {
292
+ const db = getDb();
293
+ db.run('DELETE FROM cron_runs');
294
+ db.run('DELETE FROM cron_jobs');
295
+ });
296
+
297
+ test('updating cronExpression (legacy path) still works', () => {
298
+ const job = createSchedule({
299
+ name: 'Update test',
300
+ cronExpression: '0 9 * * *',
301
+ message: 'update me',
302
+ });
303
+
304
+ const updated = updateSchedule(job.id, { cronExpression: '0 10 * * *' });
305
+ expect(updated).not.toBeNull();
306
+ expect(updated!.cronExpression).toBe('0 10 * * *');
307
+ expect(updated!.expression).toBe('0 10 * * *');
308
+ expect(updated!.syntax).toBe('cron');
309
+ // nextRunAt should have been recomputed
310
+ expect(updated!.nextRunAt).not.toBe(job.nextRunAt);
311
+ });
312
+
313
+ test('updating syntax + expression switches to RRULE', () => {
314
+ const job = createSchedule({
315
+ name: 'Switch to RRULE',
316
+ cronExpression: '0 9 * * *',
317
+ message: 'switching',
318
+ });
319
+
320
+ const rrule = 'DTSTART:20260101T090000Z\nRRULE:FREQ=DAILY;INTERVAL=2';
321
+ const updated = updateSchedule(job.id, {
322
+ syntax: 'rrule',
323
+ expression: rrule,
324
+ });
325
+
326
+ expect(updated).not.toBeNull();
327
+ expect(updated!.syntax).toBe('rrule');
328
+ expect(updated!.expression).toBe(rrule);
329
+ expect(updated!.cronExpression).toBe(rrule);
330
+ expect(updated!.nextRunAt).toBeGreaterThan(0);
331
+
332
+ // Confirm DB has the right syntax
333
+ const raw = getRawDb().query('SELECT schedule_syntax FROM cron_jobs WHERE id = ?').get(job.id) as { schedule_syntax: string } | null;
334
+ expect(raw!.schedule_syntax).toBe('rrule');
335
+ });
336
+
337
+ test('updating to RRULE set expression preserves full text', () => {
338
+ const job = createSchedule({
339
+ name: 'Update to set',
340
+ cronExpression: '0 9 * * *',
341
+ message: 'update to set',
342
+ });
343
+
344
+ const setExpr = [
345
+ 'DTSTART:20250101T090000Z',
346
+ 'RRULE:FREQ=DAILY;INTERVAL=1',
347
+ 'EXDATE:20250102T090000Z',
348
+ ].join('\n');
349
+
350
+ const updated = updateSchedule(job.id, {
351
+ syntax: 'rrule',
352
+ expression: setExpr,
353
+ });
354
+
355
+ expect(updated).not.toBeNull();
356
+ expect(updated!.syntax).toBe('rrule');
357
+ expect(updated!.expression).toBe(setExpr);
358
+ expect(updated!.expression).toContain('EXDATE');
359
+ expect(updated!.nextRunAt).toBeGreaterThan(0);
360
+ });
361
+
362
+ test('rejects invalid expression on update', () => {
363
+ const job = createSchedule({
364
+ name: 'Reject bad update',
365
+ cronExpression: '0 9 * * *',
366
+ message: 'nope',
367
+ });
368
+
369
+ expect(() => updateSchedule(job.id, {
370
+ syntax: 'rrule',
371
+ expression: 'RRULE:FREQ=DAILY',
372
+ })).toThrow();
373
+ });
374
+ });
375
+
376
+ // ── claimDueSchedules ────────────────────────────────────────────────
377
+
378
+ describe('claimDueSchedules', () => {
379
+ beforeEach(() => {
380
+ const db = getDb();
381
+ db.run('DELETE FROM cron_runs');
382
+ db.run('DELETE FROM cron_jobs');
383
+ });
384
+
385
+ test('claims due cron schedules and advances nextRunAt', () => {
386
+ const job = createSchedule({
387
+ name: 'Claim cron',
388
+ cronExpression: '* * * * *',
389
+ message: 'cron claim test',
390
+ });
391
+
392
+ // Force the schedule to be due
393
+ getRawDb().run('UPDATE cron_jobs SET next_run_at = ? WHERE id = ?', [Date.now() - 1000, job.id]);
394
+
395
+ const claimed = claimDueSchedules(Date.now());
396
+ expect(claimed.length).toBe(1);
397
+ expect(claimed[0].id).toBe(job.id);
398
+ expect(claimed[0].syntax).toBe('cron');
399
+ expect(claimed[0].nextRunAt).toBeGreaterThan(Date.now() - 1000);
400
+ });
401
+
402
+ test('claims due RRULE schedules and advances nextRunAt', () => {
403
+ // Use an RRULE that fires every minute (roughly), with a DTSTART in the past
404
+ const rrule = 'DTSTART:20250101T000000Z\nRRULE:FREQ=MINUTELY;INTERVAL=1';
405
+ const job = createSchedule({
406
+ name: 'Claim RRULE',
407
+ cronExpression: rrule,
408
+ message: 'rrule claim test',
409
+ syntax: 'rrule',
410
+ expression: rrule,
411
+ });
412
+
413
+ // Force the schedule to be due
414
+ const pastTs = Date.now() - 60_000;
415
+ getRawDb().run('UPDATE cron_jobs SET next_run_at = ? WHERE id = ?', [pastTs, job.id]);
416
+
417
+ const now = Date.now();
418
+ const claimed = claimDueSchedules(now);
419
+ expect(claimed.length).toBe(1);
420
+ expect(claimed[0].id).toBe(job.id);
421
+ expect(claimed[0].syntax).toBe('rrule');
422
+ // nextRunAt should be in the future (at or after now)
423
+ expect(claimed[0].nextRunAt).toBeGreaterThanOrEqual(now);
424
+ });
425
+
426
+ test('does not claim schedules that are not yet due', () => {
427
+ createSchedule({
428
+ name: 'Not due yet',
429
+ cronExpression: '0 9 * * *',
430
+ message: 'future schedule',
431
+ });
432
+
433
+ const claimed = claimDueSchedules(0); // timestamp 0 means nothing is due
434
+ expect(claimed.length).toBe(0);
435
+ });
436
+
437
+ test('claims exhausted RRULE schedule and disables it', () => {
438
+ // COUNT=1 means only one occurrence — the DTSTART itself
439
+ const rrule = 'DTSTART:20250101T000000Z\nRRULE:FREQ=DAILY;COUNT=1';
440
+ const job = createSchedule({
441
+ name: 'Finite RRULE',
442
+ cronExpression: rrule,
443
+ message: 'one-shot',
444
+ syntax: 'rrule',
445
+ expression: rrule,
446
+ });
447
+
448
+ // Force the schedule to be due (past the only occurrence)
449
+ getRawDb().run('UPDATE cron_jobs SET next_run_at = ? WHERE id = ?', [Date.now() - 1000, job.id]);
450
+
451
+ const claimed = claimDueSchedules(Date.now());
452
+ expect(claimed.length).toBe(1);
453
+ expect(claimed[0].id).toBe(job.id);
454
+ expect(claimed[0].enabled).toBe(false);
455
+ expect(claimed[0].nextRunAt).toBe(0);
456
+
457
+ // Verify the schedule is disabled in the DB
458
+ const persisted = getSchedule(job.id);
459
+ expect(persisted!.enabled).toBe(false);
460
+
461
+ // A subsequent claim should not pick it up
462
+ const again = claimDueSchedules(Date.now());
463
+ expect(again.length).toBe(0);
464
+ });
465
+
466
+ test('optimistic lock prevents double-claiming', () => {
467
+ const job = createSchedule({
468
+ name: 'Double claim',
469
+ cronExpression: '* * * * *',
470
+ message: 'no double',
471
+ });
472
+
473
+ getRawDb().run('UPDATE cron_jobs SET next_run_at = ? WHERE id = ?', [Date.now() - 1000, job.id]);
474
+
475
+ const first = claimDueSchedules(Date.now());
476
+ expect(first.length).toBe(1);
477
+
478
+ // Second claim should find nothing since nextRunAt was advanced
479
+ const second = claimDueSchedules(Date.now() - 500);
480
+ expect(second.length).toBe(0);
481
+ });
482
+ });