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,373 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { buildTemporalContext } from '../daemon/date-context.js';
3
+
4
+ // Fixed timestamps for deterministic assertions (all UTC midday to avoid DST edge cases).
5
+
6
+ /** Wednesday 2026-02-18 12:00 UTC */
7
+ const WED_FEB_18 = Date.UTC(2026, 1, 18, 12, 0, 0);
8
+
9
+ /** Saturday 2026-02-21 12:00 UTC */
10
+ const SAT_FEB_21 = Date.UTC(2026, 1, 21, 12, 0, 0);
11
+
12
+ /** Sunday 2026-02-22 12:00 UTC */
13
+ const SUN_FEB_22 = Date.UTC(2026, 1, 22, 12, 0, 0);
14
+
15
+ /** Tuesday 2026-12-29 12:00 UTC — year boundary */
16
+ const TUE_DEC_29 = Date.UTC(2026, 11, 29, 12, 0, 0);
17
+
18
+ /** Friday 2026-02-27 12:00 UTC */
19
+ const FRI_FEB_27 = Date.UTC(2026, 1, 27, 12, 0, 0);
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Basic structure
23
+ // ---------------------------------------------------------------------------
24
+
25
+ describe('buildTemporalContext', () => {
26
+ test('returns output wrapped in <temporal_context> tags', () => {
27
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
28
+ expect(result).toStartWith('<temporal_context>');
29
+ expect(result).toEndWith('</temporal_context>');
30
+ });
31
+
32
+ test('includes today date and weekday', () => {
33
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
34
+ expect(result).toContain('Today: 2026-02-18 (Wednesday)');
35
+ });
36
+
37
+ test('includes timezone', () => {
38
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'America/New_York' });
39
+ expect(result).toContain('Timezone: America/New_York');
40
+ });
41
+
42
+ test('includes week definitions', () => {
43
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
44
+ expect(result).toContain('work week = Monday–Friday');
45
+ expect(result).toContain('weekend = Saturday–Sunday');
46
+ });
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Weekday baseline — today is Wednesday
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe('weekday baseline (Wednesday)', () => {
54
+ test('next weekend is the upcoming Saturday-Sunday', () => {
55
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
56
+ // Wednesday Feb 18 → next Saturday is Feb 21, Sunday is Feb 22
57
+ expect(result).toContain('Next weekend: 2026-02-21 – 2026-02-22');
58
+ });
59
+
60
+ test('next work week is the following Monday-Friday', () => {
61
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
62
+ // Wednesday Feb 18 → next Monday is Feb 23, Friday is Feb 27
63
+ expect(result).toContain('Next work week: 2026-02-23 – 2026-02-27');
64
+ });
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Weekend baseline — today is Saturday
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe('weekend baseline (Saturday)', () => {
72
+ test('next weekend is the *following* Saturday-Sunday, not today', () => {
73
+ const result = buildTemporalContext({ nowMs: SAT_FEB_21, timeZone: 'UTC' });
74
+ // Saturday Feb 21 → next Saturday is Feb 28, Sunday is Mar 1
75
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
76
+ });
77
+
78
+ test('next work week is the upcoming Monday-Friday', () => {
79
+ const result = buildTemporalContext({ nowMs: SAT_FEB_21, timeZone: 'UTC' });
80
+ // Saturday Feb 21 → next Monday is Feb 23, Friday is Feb 27
81
+ expect(result).toContain('Next work week: 2026-02-23 – 2026-02-27');
82
+ });
83
+ });
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Weekend baseline — today is Sunday
87
+ // ---------------------------------------------------------------------------
88
+
89
+ describe('weekend baseline (Sunday)', () => {
90
+ test('next weekend is the following Saturday-Sunday', () => {
91
+ const result = buildTemporalContext({ nowMs: SUN_FEB_22, timeZone: 'UTC' });
92
+ // Sunday Feb 22 → next Saturday is Feb 28, Sunday is Mar 1
93
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
94
+ });
95
+
96
+ test('next work week is the upcoming Monday-Friday', () => {
97
+ const result = buildTemporalContext({ nowMs: SUN_FEB_22, timeZone: 'UTC' });
98
+ // Sunday Feb 22 → next Monday is Feb 23, Friday is Feb 27
99
+ expect(result).toContain('Next work week: 2026-02-23 – 2026-02-27');
100
+ });
101
+ });
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Friday baseline
105
+ // ---------------------------------------------------------------------------
106
+
107
+ describe('Friday baseline', () => {
108
+ test('next weekend is tomorrow (Saturday) and Sunday', () => {
109
+ const result = buildTemporalContext({ nowMs: FRI_FEB_27, timeZone: 'UTC' });
110
+ // Friday Feb 27 → next Saturday is Feb 28, Sunday is Mar 1
111
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
112
+ });
113
+
114
+ test('next work week is the following Monday-Friday', () => {
115
+ const result = buildTemporalContext({ nowMs: FRI_FEB_27, timeZone: 'UTC' });
116
+ // Friday Feb 27 → next Monday is Mar 2, Friday is Mar 6
117
+ expect(result).toContain('Next work week: 2026-03-02 – 2026-03-06');
118
+ });
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Month / year boundary
123
+ // ---------------------------------------------------------------------------
124
+
125
+ describe('month/year boundary', () => {
126
+ test('handles year boundary correctly', () => {
127
+ const result = buildTemporalContext({ nowMs: TUE_DEC_29, timeZone: 'UTC' });
128
+ expect(result).toContain('Today: 2026-12-29 (Tuesday)');
129
+ // Tuesday Dec 29 → next Saturday is Jan 2 2027
130
+ expect(result).toContain('Next weekend: 2027-01-02 – 2027-01-03');
131
+ // Next Monday is Jan 4 2027 (skips current work week)
132
+ // Wait — Dec 29 is Tuesday, so next Monday = Jan 4? Let me think:
133
+ // Dec 29 Tue → Mon is (1-2+7)%7 = 6 days → Jan 4 Mon
134
+ expect(result).toContain('Next work week: 2027-01-04 – 2027-01-08');
135
+ });
136
+
137
+ test('horizon entries cross year boundary', () => {
138
+ const result = buildTemporalContext({ nowMs: TUE_DEC_29, timeZone: 'UTC', horizonDays: 5 });
139
+ expect(result).toContain('2026-12-30 Wednesday');
140
+ expect(result).toContain('2026-12-31 Thursday');
141
+ expect(result).toContain('2027-01-01 Friday');
142
+ expect(result).toContain('2027-01-02 Saturday');
143
+ expect(result).toContain('2027-01-03 Sunday');
144
+ });
145
+ });
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // Output size caps
149
+ // ---------------------------------------------------------------------------
150
+
151
+ describe('output size caps', () => {
152
+ test('output is at most 1500 characters', () => {
153
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC', horizonDays: 14 });
154
+ expect(result.length).toBeLessThanOrEqual(1500);
155
+ });
156
+
157
+ test('horizon entries are capped at 14 even if more requested', () => {
158
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC', horizonDays: 30 });
159
+ const horizonMatches = result.match(/^\s+\d{4}-\d{2}-\d{2} \w+$/gm);
160
+ expect(horizonMatches).not.toBeNull();
161
+ expect(horizonMatches!.length).toBeLessThanOrEqual(14);
162
+ });
163
+
164
+ test('default horizon is 14 days', () => {
165
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
166
+ const horizonMatches = result.match(/^\s+\d{4}-\d{2}-\d{2} \w+$/gm);
167
+ expect(horizonMatches).not.toBeNull();
168
+ expect(horizonMatches!.length).toBe(14);
169
+ });
170
+
171
+ test('respects smaller horizonDays', () => {
172
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC', horizonDays: 3 });
173
+ const horizonMatches = result.match(/^\s+\d{4}-\d{2}-\d{2} \w+$/gm);
174
+ expect(horizonMatches).not.toBeNull();
175
+ expect(horizonMatches!.length).toBe(3);
176
+ });
177
+ });
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // DST-safe timezone behavior
181
+ // ---------------------------------------------------------------------------
182
+
183
+ describe('DST-safe timezone behavior', () => {
184
+ test('date labels are correct in US Eastern timezone', () => {
185
+ // Feb 18 12:00 UTC = Feb 18 07:00 EST (same calendar date)
186
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'America/New_York' });
187
+ expect(result).toContain('Today: 2026-02-18 (Wednesday)');
188
+ });
189
+
190
+ test('date labels are correct in timezone ahead of UTC', () => {
191
+ // Use a timestamp near midnight UTC so the local date differs
192
+ // Feb 18 23:00 UTC = Feb 19 08:00 JST
193
+ const nearMidnight = Date.UTC(2026, 1, 18, 23, 0, 0);
194
+ const result = buildTemporalContext({ nowMs: nearMidnight, timeZone: 'Asia/Tokyo' });
195
+ expect(result).toContain('Today: 2026-02-19 (Thursday)');
196
+ });
197
+
198
+ test('addDays is correct across DST spring-forward boundary', () => {
199
+ // 2026-03-08 is spring-forward day in America/New_York (clocks jump 2:00→3:00 AM).
200
+ // Use a timestamp at local 23:30 on Friday March 6 (04:30 UTC March 7).
201
+ const preDST = Date.UTC(2026, 2, 7, 4, 30, 0); // local: Fri Mar 6 23:30 EST
202
+ const result = buildTemporalContext({ nowMs: preDST, timeZone: 'America/New_York', horizonDays: 5 });
203
+ // Today should be Friday March 6
204
+ expect(result).toContain('Today: 2026-03-06 (Friday)');
205
+ // Horizon should have 5 consecutive days with no duplicates/skips
206
+ expect(result).toContain('2026-03-07 Saturday');
207
+ expect(result).toContain('2026-03-08 Sunday');
208
+ expect(result).toContain('2026-03-09 Monday');
209
+ expect(result).toContain('2026-03-10 Tuesday');
210
+ expect(result).toContain('2026-03-11 Wednesday');
211
+ });
212
+
213
+ test('addDays is correct across DST fall-back boundary', () => {
214
+ // 2026-11-01 is fall-back day in America/New_York (clocks jump 2:00→1:00 AM).
215
+ // Use a timestamp at local 00:30 on Sunday Nov 1 (04:30 UTC Nov 1).
216
+ const preFallback = Date.UTC(2026, 10, 1, 4, 30, 0); // local: Sun Nov 1 00:30 EDT
217
+ const result = buildTemporalContext({ nowMs: preFallback, timeZone: 'America/New_York', horizonDays: 3 });
218
+ // Today should be Sunday Nov 1
219
+ expect(result).toContain('Today: 2026-11-01 (Sunday)');
220
+ // Horizon should have 3 consecutive days
221
+ expect(result).toContain('2026-11-02 Monday');
222
+ expect(result).toContain('2026-11-03 Tuesday');
223
+ expect(result).toContain('2026-11-04 Wednesday');
224
+ });
225
+
226
+ test('dates are correct in far-east UTC+13 timezone (Pacific/Auckland NZDT)', () => {
227
+ // Feb 18 12:00 UTC = Feb 19 01:00 NZDT (UTC+13 during daylight saving)
228
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'Pacific/Auckland', horizonDays: 3 });
229
+ // In Auckland, Feb 18 12:00 UTC is already Feb 19 (Thursday)
230
+ expect(result).toContain('Today: 2026-02-19 (Thursday)');
231
+ // Horizon should show consecutive days without +1 shift
232
+ expect(result).toContain('2026-02-20 Friday');
233
+ expect(result).toContain('2026-02-21 Saturday');
234
+ expect(result).toContain('2026-02-22 Sunday');
235
+ });
236
+ });
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Trip-planning regression: "next weekend" resolution
240
+ // ---------------------------------------------------------------------------
241
+
242
+ describe('trip-planning: next weekend resolution', () => {
243
+ test('Wednesday → "next weekend" anchors resolve to upcoming Sat-Sun', () => {
244
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
245
+ // A user asking "plan a trip for next weekend" on Wednesday Feb 18
246
+ // expects Sat Feb 21 – Sun Feb 22.
247
+ expect(result).toContain('Next weekend: 2026-02-21 – 2026-02-22');
248
+ // Both dates must appear in the horizon so the model can reference them.
249
+ expect(result).toContain('2026-02-21 Saturday');
250
+ expect(result).toContain('2026-02-22 Sunday');
251
+ });
252
+
253
+ test('Saturday → "next weekend" skips current weekend', () => {
254
+ const result = buildTemporalContext({ nowMs: SAT_FEB_21, timeZone: 'UTC' });
255
+ // Already on Saturday → "next weekend" means the *following* weekend.
256
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
257
+ });
258
+
259
+ test('Sunday → "next weekend" skips current weekend', () => {
260
+ const result = buildTemporalContext({ nowMs: SUN_FEB_22, timeZone: 'UTC' });
261
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
262
+ });
263
+
264
+ test('Friday → "next weekend" is tomorrow', () => {
265
+ const result = buildTemporalContext({ nowMs: FRI_FEB_27, timeZone: 'UTC' });
266
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
267
+ });
268
+ });
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Trip-planning regression: "next work week" resolution
272
+ // ---------------------------------------------------------------------------
273
+
274
+ describe('trip-planning: next work week resolution', () => {
275
+ test('Wednesday → "next work week" skips remainder of current week', () => {
276
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
277
+ expect(result).toContain('Next work week: 2026-02-23 – 2026-02-27');
278
+ });
279
+
280
+ test('Monday → "next work week" is the following Monday-Friday', () => {
281
+ /** Monday 2026-02-23 12:00 UTC */
282
+ const MON_FEB_23 = Date.UTC(2026, 1, 23, 12, 0, 0);
283
+ const result = buildTemporalContext({ nowMs: MON_FEB_23, timeZone: 'UTC' });
284
+ expect(result).toContain('Next work week: 2026-03-02 – 2026-03-06');
285
+ });
286
+
287
+ test('Saturday → "next work week" is the upcoming Monday-Friday', () => {
288
+ const result = buildTemporalContext({ nowMs: SAT_FEB_21, timeZone: 'UTC' });
289
+ expect(result).toContain('Next work week: 2026-02-23 – 2026-02-27');
290
+ });
291
+ });
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Trip-planning regression: month-without-year disambiguation
295
+ // ---------------------------------------------------------------------------
296
+
297
+ describe('trip-planning: month-without-year disambiguation via temporal anchors', () => {
298
+ test('Today line includes full YYYY-MM-DD format with year for month disambiguation', () => {
299
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC' });
300
+ // The Today line must include the full year so the model can resolve bare
301
+ // month names (e.g. "May" → May 2026 because today is Feb 2026).
302
+ // Regex ensures YYYY-MM-DD format is present (regression if year is dropped).
303
+ expect(result).toMatch(/Today: \d{4}-\d{2}-\d{2} \(\w+\)/);
304
+ expect(result).toContain('2026-02-18');
305
+ });
306
+
307
+ test('future-month anchors: horizon dates are all in the future relative to today', () => {
308
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'UTC', horizonDays: 14 });
309
+ // Extract all horizon dates (indented YYYY-MM-DD lines)
310
+ const horizonDates = result.match(/^\s+(\d{4}-\d{2}-\d{2}) \w+$/gm);
311
+ expect(horizonDates).not.toBeNull();
312
+ // All horizon dates must be after today (2026-02-18)
313
+ for (const line of horizonDates!) {
314
+ const dateStr = line.trim().split(' ')[0];
315
+ expect(dateStr > '2026-02-18').toBe(true);
316
+ }
317
+ });
318
+
319
+ test('year-end context: horizon spans into next year for Dec disambiguation', () => {
320
+ const result = buildTemporalContext({ nowMs: TUE_DEC_29, timeZone: 'UTC', horizonDays: 14 });
321
+ // Today is Dec 29 2026 — horizon must include 2027 dates so the model can
322
+ // distinguish "January" (Jan 2027) from past January (Jan 2026).
323
+ expect(result).toContain('Today: 2026-12-29');
324
+ expect(result).toMatch(/2027-01-\d{2} \w+/); // At least one January 2027 date
325
+ });
326
+
327
+ test('timezone is always present for correct local-month resolution', () => {
328
+ const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: 'America/New_York' });
329
+ // Timezone must be present so the model resolves months in the user's
330
+ // local calendar, not UTC.
331
+ expect(result).toMatch(/Timezone: .+/);
332
+ expect(result).toContain('America/New_York');
333
+ });
334
+ });
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Trip-planning regression: cross-month weekend resolution
338
+ // ---------------------------------------------------------------------------
339
+
340
+ describe('trip-planning: cross-month weekend resolution', () => {
341
+ test('weekend that spans a month boundary (Feb → Mar)', () => {
342
+ const result = buildTemporalContext({ nowMs: FRI_FEB_27, timeZone: 'UTC' });
343
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
344
+ });
345
+
346
+ test('year-boundary weekend (Dec 2026 → Jan 2027)', () => {
347
+ const result = buildTemporalContext({ nowMs: TUE_DEC_29, timeZone: 'UTC' });
348
+ expect(result).toContain('Next weekend: 2027-01-02 – 2027-01-03');
349
+ });
350
+ });
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // Trip-planning regression: timezone-shifted weekend anchors
354
+ // ---------------------------------------------------------------------------
355
+
356
+ describe('trip-planning: timezone-shifted weekend anchors', () => {
357
+ test('late Friday UTC is already Saturday in Auckland → skips to next weekend', () => {
358
+ // Friday Feb 27 23:00 UTC = Saturday Feb 28 12:00 NZDT
359
+ const lateFriUTC = Date.UTC(2026, 1, 27, 23, 0, 0);
360
+ const result = buildTemporalContext({ nowMs: lateFriUTC, timeZone: 'Pacific/Auckland' });
361
+ expect(result).toContain('Today: 2026-02-28 (Saturday)');
362
+ // "Next weekend" skips current weekend → Mar 7-8.
363
+ expect(result).toContain('Next weekend: 2026-03-07 – 2026-03-08');
364
+ });
365
+
366
+ test('early Saturday UTC is still Friday in US Pacific → next weekend is tomorrow', () => {
367
+ // Saturday Feb 28 02:00 UTC = Friday Feb 27 18:00 PST
368
+ const earlySatUTC = Date.UTC(2026, 1, 28, 2, 0, 0);
369
+ const result = buildTemporalContext({ nowMs: earlySatUTC, timeZone: 'America/Los_Angeles' });
370
+ expect(result).toContain('Today: 2026-02-27 (Friday)');
371
+ expect(result).toContain('Next weekend: 2026-02-28 – 2026-03-01');
372
+ });
373
+ });
@@ -0,0 +1,129 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { Database } from 'bun:sqlite';
3
+ import { drizzle } from 'drizzle-orm/bun-sqlite';
4
+ import * as schema from '../memory/schema.js';
5
+ import { scheduleJobs } from '../memory/schema.js';
6
+ import { eq } from 'drizzle-orm';
7
+
8
+ function createTestDb() {
9
+ const sqlite = new Database(':memory:');
10
+ sqlite.exec('PRAGMA journal_mode=WAL');
11
+ sqlite.exec('PRAGMA foreign_keys = ON');
12
+ return drizzle(sqlite, { schema });
13
+ }
14
+
15
+ function getRawSqlite(db: ReturnType<typeof drizzle<typeof schema>>): Database {
16
+ return (db as unknown as { $client: Database }).$client;
17
+ }
18
+
19
+ describe('schedule_syntax column migration', () => {
20
+ test('fresh DB includes schedule_syntax with default cron', () => {
21
+ const db = createTestDb();
22
+ const raw = getRawSqlite(db);
23
+
24
+ raw.exec(/*sql*/ `
25
+ CREATE TABLE IF NOT EXISTS cron_jobs (
26
+ id TEXT PRIMARY KEY,
27
+ name TEXT NOT NULL,
28
+ enabled INTEGER NOT NULL DEFAULT 1,
29
+ cron_expression TEXT NOT NULL,
30
+ schedule_syntax TEXT NOT NULL DEFAULT 'cron',
31
+ timezone TEXT,
32
+ message TEXT NOT NULL,
33
+ next_run_at INTEGER NOT NULL,
34
+ last_run_at INTEGER,
35
+ last_status TEXT,
36
+ retry_count INTEGER NOT NULL DEFAULT 0,
37
+ created_by TEXT NOT NULL,
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL
40
+ )
41
+ `);
42
+
43
+ const now = Date.now();
44
+ db.insert(scheduleJobs).values({
45
+ id: 'test-1',
46
+ name: 'Test Job',
47
+ enabled: true,
48
+ cronExpression: '0 9 * * *',
49
+ timezone: null,
50
+ message: 'hello',
51
+ nextRunAt: now + 60000,
52
+ lastRunAt: null,
53
+ lastStatus: null,
54
+ retryCount: 0,
55
+ createdBy: 'agent',
56
+ createdAt: now,
57
+ updatedAt: now,
58
+ }).run();
59
+
60
+ const row = db.select().from(scheduleJobs).where(eq(scheduleJobs.id, 'test-1')).get();
61
+ expect(row).toBeTruthy();
62
+ expect(row!.scheduleSyntax).toBe('cron');
63
+ });
64
+
65
+ test('upgraded DB gains schedule_syntax column via ALTER TABLE', () => {
66
+ const db = createTestDb();
67
+ const raw = getRawSqlite(db);
68
+
69
+ // Old schema without schedule_syntax
70
+ raw.exec(/*sql*/ `
71
+ CREATE TABLE IF NOT EXISTS cron_jobs (
72
+ id TEXT PRIMARY KEY,
73
+ name TEXT NOT NULL,
74
+ enabled INTEGER NOT NULL DEFAULT 1,
75
+ cron_expression TEXT NOT NULL,
76
+ timezone TEXT,
77
+ message TEXT NOT NULL,
78
+ next_run_at INTEGER NOT NULL,
79
+ last_run_at INTEGER,
80
+ last_status TEXT,
81
+ retry_count INTEGER NOT NULL DEFAULT 0,
82
+ created_by TEXT NOT NULL,
83
+ created_at INTEGER NOT NULL,
84
+ updated_at INTEGER NOT NULL
85
+ )
86
+ `);
87
+
88
+ const now = Date.now();
89
+ raw.exec(`INSERT INTO cron_jobs (id, name, enabled, cron_expression, timezone, message, next_run_at, last_run_at, last_status, retry_count, created_by, created_at, updated_at) VALUES ('old-1', 'Old Job', 1, '0 9 * * *', NULL, 'hello', ${now + 60000}, NULL, NULL, 0, 'agent', ${now}, ${now})`);
90
+
91
+ // Run the migration
92
+ try { raw.exec(`ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`); } catch { /* already exists */ }
93
+
94
+ const row = db.select().from(scheduleJobs).where(eq(scheduleJobs.id, 'old-1')).get();
95
+ expect(row).toBeTruthy();
96
+ expect(row!.scheduleSyntax).toBe('cron');
97
+ });
98
+
99
+ test('migration is idempotent', () => {
100
+ const db = createTestDb();
101
+ const raw = getRawSqlite(db);
102
+
103
+ raw.exec(/*sql*/ `
104
+ CREATE TABLE IF NOT EXISTS cron_jobs (
105
+ id TEXT PRIMARY KEY,
106
+ name TEXT NOT NULL,
107
+ enabled INTEGER NOT NULL DEFAULT 1,
108
+ cron_expression TEXT NOT NULL,
109
+ timezone TEXT,
110
+ message TEXT NOT NULL,
111
+ next_run_at INTEGER NOT NULL,
112
+ last_run_at INTEGER,
113
+ last_status TEXT,
114
+ retry_count INTEGER NOT NULL DEFAULT 0,
115
+ created_by TEXT NOT NULL,
116
+ created_at INTEGER NOT NULL,
117
+ updated_at INTEGER NOT NULL
118
+ )
119
+ `);
120
+
121
+ try { raw.exec(`ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`); } catch { /* ok */ }
122
+ try { raw.exec(`ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`); } catch { /* ok */ }
123
+
124
+ const now = Date.now();
125
+ raw.exec(`INSERT INTO cron_jobs (id, name, enabled, cron_expression, timezone, message, next_run_at, retry_count, created_by, created_at, updated_at) VALUES ('idem-1', 'Test', 1, '0 9 * * *', NULL, 'hi', ${now + 60000}, 0, 'agent', ${now}, ${now})`);
126
+ const row = db.select().from(scheduleJobs).where(eq(scheduleJobs.id, 'idem-1')).get();
127
+ expect(row!.scheduleSyntax).toBe('cron');
128
+ });
129
+ });
@@ -31,33 +31,33 @@ const NOW = 1700000000000;
31
31
  /** A fake selfie image attachment with deterministic IDs. */
32
32
  export const FAKE_SELFIE_ATTACHMENT: StoredAttachment = {
33
33
  id: 'att-selfie-001',
34
- assistantId: 'asst-test-001',
35
34
  originalFilename: 'selfie.png',
36
35
  mimeType: 'image/png',
37
36
  sizeBytes: Buffer.from(TINY_PNG_BASE64, 'base64').length,
38
37
  kind: 'image',
38
+ thumbnailBase64: null,
39
39
  createdAt: NOW,
40
40
  };
41
41
 
42
42
  /** A fake document attachment. */
43
43
  export const FAKE_DOCUMENT_ATTACHMENT: StoredAttachment = {
44
44
  id: 'att-doc-001',
45
- assistantId: 'asst-test-001',
46
45
  originalFilename: 'report.pdf',
47
46
  mimeType: 'application/pdf',
48
47
  sizeBytes: 4096,
49
48
  kind: 'document',
49
+ thumbnailBase64: null,
50
50
  createdAt: NOW,
51
51
  };
52
52
 
53
53
  /** A fake JPEG photo attachment. */
54
54
  export const FAKE_PHOTO_ATTACHMENT: StoredAttachment = {
55
55
  id: 'att-photo-001',
56
- assistantId: 'asst-test-001',
57
56
  originalFilename: 'photo.jpg',
58
57
  mimeType: 'image/jpeg',
59
58
  sizeBytes: Buffer.from(TINY_JPEG_BASE64, 'base64').length,
60
59
  kind: 'image',
60
+ thumbnailBase64: null,
61
61
  createdAt: NOW,
62
62
  };
63
63