@herdctl/core 0.0.1

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 (520) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +219 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/coverage-final.json +51 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +251 -0
  9. package/coverage/prettify.css +1 -0
  10. package/coverage/prettify.js +2 -0
  11. package/coverage/sort-arrow-sprite.png +0 -0
  12. package/coverage/sorter.js +210 -0
  13. package/coverage/src/config/index.html +191 -0
  14. package/coverage/src/config/index.ts.html +442 -0
  15. package/coverage/src/config/interpolate.ts.html +652 -0
  16. package/coverage/src/config/loader.ts.html +1501 -0
  17. package/coverage/src/config/merge.ts.html +823 -0
  18. package/coverage/src/config/parser.ts.html +1213 -0
  19. package/coverage/src/config/schema.ts.html +1123 -0
  20. package/coverage/src/fleet-manager/errors.ts.html +2326 -0
  21. package/coverage/src/fleet-manager/event-types.ts.html +1219 -0
  22. package/coverage/src/fleet-manager/fleet-manager.ts.html +7030 -0
  23. package/coverage/src/fleet-manager/index.html +206 -0
  24. package/coverage/src/fleet-manager/index.ts.html +469 -0
  25. package/coverage/src/fleet-manager/job-manager.ts.html +2074 -0
  26. package/coverage/src/fleet-manager/job-queue.ts.html +2479 -0
  27. package/coverage/src/fleet-manager/types.ts.html +2602 -0
  28. package/coverage/src/index.html +116 -0
  29. package/coverage/src/index.ts.html +181 -0
  30. package/coverage/src/runner/errors.ts.html +1006 -0
  31. package/coverage/src/runner/index.html +191 -0
  32. package/coverage/src/runner/index.ts.html +256 -0
  33. package/coverage/src/runner/job-executor.ts.html +1429 -0
  34. package/coverage/src/runner/message-processor.ts.html +1150 -0
  35. package/coverage/src/runner/sdk-adapter.ts.html +658 -0
  36. package/coverage/src/runner/types.ts.html +559 -0
  37. package/coverage/src/scheduler/errors.ts.html +388 -0
  38. package/coverage/src/scheduler/index.html +206 -0
  39. package/coverage/src/scheduler/index.ts.html +244 -0
  40. package/coverage/src/scheduler/interval.ts.html +652 -0
  41. package/coverage/src/scheduler/schedule-runner.ts.html +1411 -0
  42. package/coverage/src/scheduler/schedule-state.ts.html +718 -0
  43. package/coverage/src/scheduler/scheduler.ts.html +1795 -0
  44. package/coverage/src/scheduler/types.ts.html +733 -0
  45. package/coverage/src/state/directory.ts.html +736 -0
  46. package/coverage/src/state/errors.ts.html +376 -0
  47. package/coverage/src/state/fleet-state.ts.html +937 -0
  48. package/coverage/src/state/index.html +221 -0
  49. package/coverage/src/state/index.ts.html +322 -0
  50. package/coverage/src/state/job-metadata.ts.html +1420 -0
  51. package/coverage/src/state/job-output.ts.html +1033 -0
  52. package/coverage/src/state/schemas/fleet-state.ts.html +445 -0
  53. package/coverage/src/state/schemas/index.html +176 -0
  54. package/coverage/src/state/schemas/index.ts.html +286 -0
  55. package/coverage/src/state/schemas/job-metadata.ts.html +628 -0
  56. package/coverage/src/state/schemas/job-output.ts.html +616 -0
  57. package/coverage/src/state/schemas/session-info.ts.html +361 -0
  58. package/coverage/src/state/session.ts.html +844 -0
  59. package/coverage/src/state/types.ts.html +262 -0
  60. package/coverage/src/state/utils/atomic.ts.html +748 -0
  61. package/coverage/src/state/utils/index.html +146 -0
  62. package/coverage/src/state/utils/index.ts.html +103 -0
  63. package/coverage/src/state/utils/reads.ts.html +1621 -0
  64. package/coverage/src/work-sources/adapters/github.ts.html +3583 -0
  65. package/coverage/src/work-sources/adapters/index.html +131 -0
  66. package/coverage/src/work-sources/adapters/index.ts.html +277 -0
  67. package/coverage/src/work-sources/errors.ts.html +298 -0
  68. package/coverage/src/work-sources/index.html +176 -0
  69. package/coverage/src/work-sources/index.ts.html +529 -0
  70. package/coverage/src/work-sources/manager.ts.html +1324 -0
  71. package/coverage/src/work-sources/registry.ts.html +619 -0
  72. package/coverage/src/work-sources/types.ts.html +568 -0
  73. package/dist/config/__tests__/agent.test.d.ts +2 -0
  74. package/dist/config/__tests__/agent.test.d.ts.map +1 -0
  75. package/dist/config/__tests__/agent.test.js +752 -0
  76. package/dist/config/__tests__/agent.test.js.map +1 -0
  77. package/dist/config/__tests__/interpolate.test.d.ts +2 -0
  78. package/dist/config/__tests__/interpolate.test.d.ts.map +1 -0
  79. package/dist/config/__tests__/interpolate.test.js +509 -0
  80. package/dist/config/__tests__/interpolate.test.js.map +1 -0
  81. package/dist/config/__tests__/loader.test.d.ts +2 -0
  82. package/dist/config/__tests__/loader.test.d.ts.map +1 -0
  83. package/dist/config/__tests__/loader.test.js +631 -0
  84. package/dist/config/__tests__/loader.test.js.map +1 -0
  85. package/dist/config/__tests__/merge.test.d.ts +2 -0
  86. package/dist/config/__tests__/merge.test.d.ts.map +1 -0
  87. package/dist/config/__tests__/merge.test.js +672 -0
  88. package/dist/config/__tests__/merge.test.js.map +1 -0
  89. package/dist/config/__tests__/parser.test.d.ts +2 -0
  90. package/dist/config/__tests__/parser.test.d.ts.map +1 -0
  91. package/dist/config/__tests__/parser.test.js +476 -0
  92. package/dist/config/__tests__/parser.test.js.map +1 -0
  93. package/dist/config/__tests__/schema.test.d.ts +2 -0
  94. package/dist/config/__tests__/schema.test.d.ts.map +1 -0
  95. package/dist/config/__tests__/schema.test.js +776 -0
  96. package/dist/config/__tests__/schema.test.js.map +1 -0
  97. package/dist/config/index.d.ts +11 -0
  98. package/dist/config/index.d.ts.map +1 -0
  99. package/dist/config/index.js +26 -0
  100. package/dist/config/index.js.map +1 -0
  101. package/dist/config/interpolate.d.ts +76 -0
  102. package/dist/config/interpolate.d.ts.map +1 -0
  103. package/dist/config/interpolate.js +143 -0
  104. package/dist/config/interpolate.js.map +1 -0
  105. package/dist/config/loader.d.ts +147 -0
  106. package/dist/config/loader.d.ts.map +1 -0
  107. package/dist/config/loader.js +336 -0
  108. package/dist/config/loader.js.map +1 -0
  109. package/dist/config/merge.d.ts +84 -0
  110. package/dist/config/merge.d.ts.map +1 -0
  111. package/dist/config/merge.js +138 -0
  112. package/dist/config/merge.js.map +1 -0
  113. package/dist/config/parser.d.ts +143 -0
  114. package/dist/config/parser.d.ts.map +1 -0
  115. package/dist/config/parser.js +316 -0
  116. package/dist/config/parser.js.map +1 -0
  117. package/dist/config/schema.d.ts +1906 -0
  118. package/dist/config/schema.d.ts.map +1 -0
  119. package/dist/config/schema.js +268 -0
  120. package/dist/config/schema.js.map +1 -0
  121. package/dist/fleet-manager/__tests__/coverage.test.d.ts +13 -0
  122. package/dist/fleet-manager/__tests__/coverage.test.d.ts.map +1 -0
  123. package/dist/fleet-manager/__tests__/coverage.test.js +2282 -0
  124. package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -0
  125. package/dist/fleet-manager/__tests__/errors.test.d.ts +7 -0
  126. package/dist/fleet-manager/__tests__/errors.test.d.ts.map +1 -0
  127. package/dist/fleet-manager/__tests__/errors.test.js +557 -0
  128. package/dist/fleet-manager/__tests__/errors.test.js.map +1 -0
  129. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +7 -0
  130. package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +1 -0
  131. package/dist/fleet-manager/__tests__/event-helpers.test.js +368 -0
  132. package/dist/fleet-manager/__tests__/event-helpers.test.js.map +1 -0
  133. package/dist/fleet-manager/__tests__/integration.test.d.ts +11 -0
  134. package/dist/fleet-manager/__tests__/integration.test.d.ts.map +1 -0
  135. package/dist/fleet-manager/__tests__/integration.test.js +949 -0
  136. package/dist/fleet-manager/__tests__/integration.test.js.map +1 -0
  137. package/dist/fleet-manager/__tests__/job-control.test.d.ts +7 -0
  138. package/dist/fleet-manager/__tests__/job-control.test.d.ts.map +1 -0
  139. package/dist/fleet-manager/__tests__/job-control.test.js +215 -0
  140. package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -0
  141. package/dist/fleet-manager/__tests__/job-manager.test.d.ts +7 -0
  142. package/dist/fleet-manager/__tests__/job-manager.test.d.ts.map +1 -0
  143. package/dist/fleet-manager/__tests__/job-manager.test.js +659 -0
  144. package/dist/fleet-manager/__tests__/job-manager.test.js.map +1 -0
  145. package/dist/fleet-manager/__tests__/job-queue.test.d.ts +5 -0
  146. package/dist/fleet-manager/__tests__/job-queue.test.d.ts.map +1 -0
  147. package/dist/fleet-manager/__tests__/job-queue.test.js +315 -0
  148. package/dist/fleet-manager/__tests__/job-queue.test.js.map +1 -0
  149. package/dist/fleet-manager/__tests__/reload.test.d.ts +7 -0
  150. package/dist/fleet-manager/__tests__/reload.test.d.ts.map +1 -0
  151. package/dist/fleet-manager/__tests__/reload.test.js +609 -0
  152. package/dist/fleet-manager/__tests__/reload.test.js.map +1 -0
  153. package/dist/fleet-manager/__tests__/status-queries.test.d.ts +7 -0
  154. package/dist/fleet-manager/__tests__/status-queries.test.d.ts.map +1 -0
  155. package/dist/fleet-manager/__tests__/status-queries.test.js +488 -0
  156. package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -0
  157. package/dist/fleet-manager/__tests__/trigger.test.d.ts +7 -0
  158. package/dist/fleet-manager/__tests__/trigger.test.d.ts.map +1 -0
  159. package/dist/fleet-manager/__tests__/trigger.test.js +471 -0
  160. package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -0
  161. package/dist/fleet-manager/errors.d.ts +407 -0
  162. package/dist/fleet-manager/errors.d.ts.map +1 -0
  163. package/dist/fleet-manager/errors.js +569 -0
  164. package/dist/fleet-manager/errors.js.map +1 -0
  165. package/dist/fleet-manager/event-types.d.ts +302 -0
  166. package/dist/fleet-manager/event-types.d.ts.map +1 -0
  167. package/dist/fleet-manager/event-types.js +9 -0
  168. package/dist/fleet-manager/event-types.js.map +1 -0
  169. package/dist/fleet-manager/fleet-manager.d.ts +699 -0
  170. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -0
  171. package/dist/fleet-manager/fleet-manager.js +1906 -0
  172. package/dist/fleet-manager/fleet-manager.js.map +1 -0
  173. package/dist/fleet-manager/index.d.ts +17 -0
  174. package/dist/fleet-manager/index.d.ts.map +1 -0
  175. package/dist/fleet-manager/index.js +29 -0
  176. package/dist/fleet-manager/index.js.map +1 -0
  177. package/dist/fleet-manager/job-manager.d.ts +271 -0
  178. package/dist/fleet-manager/job-manager.d.ts.map +1 -0
  179. package/dist/fleet-manager/job-manager.js +443 -0
  180. package/dist/fleet-manager/job-manager.js.map +1 -0
  181. package/dist/fleet-manager/job-queue.d.ts +422 -0
  182. package/dist/fleet-manager/job-queue.d.ts.map +1 -0
  183. package/dist/fleet-manager/job-queue.js +448 -0
  184. package/dist/fleet-manager/job-queue.js.map +1 -0
  185. package/dist/fleet-manager/types.d.ts +680 -0
  186. package/dist/fleet-manager/types.d.ts.map +1 -0
  187. package/dist/fleet-manager/types.js +8 -0
  188. package/dist/fleet-manager/types.js.map +1 -0
  189. package/dist/index.d.ts +20 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +26 -0
  192. package/dist/index.js.map +1 -0
  193. package/dist/runner/__tests__/errors.test.d.ts +2 -0
  194. package/dist/runner/__tests__/errors.test.d.ts.map +1 -0
  195. package/dist/runner/__tests__/errors.test.js +264 -0
  196. package/dist/runner/__tests__/errors.test.js.map +1 -0
  197. package/dist/runner/__tests__/job-executor.test.d.ts +2 -0
  198. package/dist/runner/__tests__/job-executor.test.d.ts.map +1 -0
  199. package/dist/runner/__tests__/job-executor.test.js +1345 -0
  200. package/dist/runner/__tests__/job-executor.test.js.map +1 -0
  201. package/dist/runner/__tests__/message-processor.test.d.ts +2 -0
  202. package/dist/runner/__tests__/message-processor.test.d.ts.map +1 -0
  203. package/dist/runner/__tests__/message-processor.test.js +768 -0
  204. package/dist/runner/__tests__/message-processor.test.js.map +1 -0
  205. package/dist/runner/__tests__/sdk-adapter.test.d.ts +2 -0
  206. package/dist/runner/__tests__/sdk-adapter.test.d.ts.map +1 -0
  207. package/dist/runner/__tests__/sdk-adapter.test.js +554 -0
  208. package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -0
  209. package/dist/runner/errors.d.ts +121 -0
  210. package/dist/runner/errors.d.ts.map +1 -0
  211. package/dist/runner/errors.js +212 -0
  212. package/dist/runner/errors.js.map +1 -0
  213. package/dist/runner/index.d.ts +12 -0
  214. package/dist/runner/index.d.ts.map +1 -0
  215. package/dist/runner/index.js +15 -0
  216. package/dist/runner/index.js.map +1 -0
  217. package/dist/runner/job-executor.d.ts +98 -0
  218. package/dist/runner/job-executor.d.ts.map +1 -0
  219. package/dist/runner/job-executor.js +333 -0
  220. package/dist/runner/job-executor.js.map +1 -0
  221. package/dist/runner/message-processor.d.ts +45 -0
  222. package/dist/runner/message-processor.d.ts.map +1 -0
  223. package/dist/runner/message-processor.js +294 -0
  224. package/dist/runner/message-processor.js.map +1 -0
  225. package/dist/runner/sdk-adapter.d.ts +60 -0
  226. package/dist/runner/sdk-adapter.d.ts.map +1 -0
  227. package/dist/runner/sdk-adapter.js +138 -0
  228. package/dist/runner/sdk-adapter.js.map +1 -0
  229. package/dist/runner/types.d.ts +135 -0
  230. package/dist/runner/types.d.ts.map +1 -0
  231. package/dist/runner/types.js +7 -0
  232. package/dist/runner/types.js.map +1 -0
  233. package/dist/scheduler/__tests__/errors.test.d.ts +2 -0
  234. package/dist/scheduler/__tests__/errors.test.d.ts.map +1 -0
  235. package/dist/scheduler/__tests__/errors.test.js +101 -0
  236. package/dist/scheduler/__tests__/errors.test.js.map +1 -0
  237. package/dist/scheduler/__tests__/interval.test.d.ts +2 -0
  238. package/dist/scheduler/__tests__/interval.test.d.ts.map +1 -0
  239. package/dist/scheduler/__tests__/interval.test.js +419 -0
  240. package/dist/scheduler/__tests__/interval.test.js.map +1 -0
  241. package/dist/scheduler/__tests__/schedule-runner.test.d.ts +2 -0
  242. package/dist/scheduler/__tests__/schedule-runner.test.d.ts.map +1 -0
  243. package/dist/scheduler/__tests__/schedule-runner.test.js +634 -0
  244. package/dist/scheduler/__tests__/schedule-runner.test.js.map +1 -0
  245. package/dist/scheduler/__tests__/schedule-state.test.d.ts +2 -0
  246. package/dist/scheduler/__tests__/schedule-state.test.d.ts.map +1 -0
  247. package/dist/scheduler/__tests__/schedule-state.test.js +572 -0
  248. package/dist/scheduler/__tests__/schedule-state.test.js.map +1 -0
  249. package/dist/scheduler/__tests__/scheduler.test.d.ts +2 -0
  250. package/dist/scheduler/__tests__/scheduler.test.d.ts.map +1 -0
  251. package/dist/scheduler/__tests__/scheduler.test.js +987 -0
  252. package/dist/scheduler/__tests__/scheduler.test.js.map +1 -0
  253. package/dist/scheduler/errors.d.ts +61 -0
  254. package/dist/scheduler/errors.d.ts.map +1 -0
  255. package/dist/scheduler/errors.js +81 -0
  256. package/dist/scheduler/errors.js.map +1 -0
  257. package/dist/scheduler/index.d.ts +13 -0
  258. package/dist/scheduler/index.d.ts.map +1 -0
  259. package/dist/scheduler/index.js +17 -0
  260. package/dist/scheduler/index.js.map +1 -0
  261. package/dist/scheduler/interval.d.ts +64 -0
  262. package/dist/scheduler/interval.d.ts.map +1 -0
  263. package/dist/scheduler/interval.js +139 -0
  264. package/dist/scheduler/interval.js.map +1 -0
  265. package/dist/scheduler/schedule-runner.d.ts +149 -0
  266. package/dist/scheduler/schedule-runner.d.ts.map +1 -0
  267. package/dist/scheduler/schedule-runner.js +277 -0
  268. package/dist/scheduler/schedule-runner.js.map +1 -0
  269. package/dist/scheduler/schedule-state.d.ts +105 -0
  270. package/dist/scheduler/schedule-state.d.ts.map +1 -0
  271. package/dist/scheduler/schedule-state.js +151 -0
  272. package/dist/scheduler/schedule-state.js.map +1 -0
  273. package/dist/scheduler/scheduler.d.ts +138 -0
  274. package/dist/scheduler/scheduler.d.ts.map +1 -0
  275. package/dist/scheduler/scheduler.js +423 -0
  276. package/dist/scheduler/scheduler.js.map +1 -0
  277. package/dist/scheduler/types.d.ts +160 -0
  278. package/dist/scheduler/types.d.ts.map +1 -0
  279. package/dist/scheduler/types.js +8 -0
  280. package/dist/scheduler/types.js.map +1 -0
  281. package/dist/state/__tests__/directory.test.d.ts +2 -0
  282. package/dist/state/__tests__/directory.test.d.ts.map +1 -0
  283. package/dist/state/__tests__/directory.test.js +414 -0
  284. package/dist/state/__tests__/directory.test.js.map +1 -0
  285. package/dist/state/__tests__/fleet-state.test.d.ts +2 -0
  286. package/dist/state/__tests__/fleet-state.test.d.ts.map +1 -0
  287. package/dist/state/__tests__/fleet-state.test.js +696 -0
  288. package/dist/state/__tests__/fleet-state.test.js.map +1 -0
  289. package/dist/state/__tests__/job-metadata-schema.test.d.ts +2 -0
  290. package/dist/state/__tests__/job-metadata-schema.test.d.ts.map +1 -0
  291. package/dist/state/__tests__/job-metadata-schema.test.js +329 -0
  292. package/dist/state/__tests__/job-metadata-schema.test.js.map +1 -0
  293. package/dist/state/__tests__/job-metadata.test.d.ts +2 -0
  294. package/dist/state/__tests__/job-metadata.test.d.ts.map +1 -0
  295. package/dist/state/__tests__/job-metadata.test.js +667 -0
  296. package/dist/state/__tests__/job-metadata.test.js.map +1 -0
  297. package/dist/state/__tests__/job-output.test.d.ts +2 -0
  298. package/dist/state/__tests__/job-output.test.d.ts.map +1 -0
  299. package/dist/state/__tests__/job-output.test.js +672 -0
  300. package/dist/state/__tests__/job-output.test.js.map +1 -0
  301. package/dist/state/__tests__/session-schema.test.d.ts +2 -0
  302. package/dist/state/__tests__/session-schema.test.d.ts.map +1 -0
  303. package/dist/state/__tests__/session-schema.test.js +323 -0
  304. package/dist/state/__tests__/session-schema.test.js.map +1 -0
  305. package/dist/state/__tests__/session.test.d.ts +2 -0
  306. package/dist/state/__tests__/session.test.d.ts.map +1 -0
  307. package/dist/state/__tests__/session.test.js +468 -0
  308. package/dist/state/__tests__/session.test.js.map +1 -0
  309. package/dist/state/directory.d.ts +42 -0
  310. package/dist/state/directory.d.ts.map +1 -0
  311. package/dist/state/directory.js +170 -0
  312. package/dist/state/directory.js.map +1 -0
  313. package/dist/state/errors.d.ts +44 -0
  314. package/dist/state/errors.d.ts.map +1 -0
  315. package/dist/state/errors.js +82 -0
  316. package/dist/state/errors.js.map +1 -0
  317. package/dist/state/fleet-state.d.ts +126 -0
  318. package/dist/state/fleet-state.d.ts.map +1 -0
  319. package/dist/state/fleet-state.js +196 -0
  320. package/dist/state/fleet-state.js.map +1 -0
  321. package/dist/state/index.d.ts +21 -0
  322. package/dist/state/index.d.ts.map +1 -0
  323. package/dist/state/index.js +30 -0
  324. package/dist/state/index.js.map +1 -0
  325. package/dist/state/job-metadata.d.ts +151 -0
  326. package/dist/state/job-metadata.d.ts.map +1 -0
  327. package/dist/state/job-metadata.js +287 -0
  328. package/dist/state/job-metadata.js.map +1 -0
  329. package/dist/state/job-output.d.ts +116 -0
  330. package/dist/state/job-output.d.ts.map +1 -0
  331. package/dist/state/job-output.js +218 -0
  332. package/dist/state/job-output.js.map +1 -0
  333. package/dist/state/schemas/__tests__/job-output.test.d.ts +2 -0
  334. package/dist/state/schemas/__tests__/job-output.test.d.ts.map +1 -0
  335. package/dist/state/schemas/__tests__/job-output.test.js +279 -0
  336. package/dist/state/schemas/__tests__/job-output.test.js.map +1 -0
  337. package/dist/state/schemas/fleet-state.d.ts +249 -0
  338. package/dist/state/schemas/fleet-state.d.ts.map +1 -0
  339. package/dist/state/schemas/fleet-state.js +97 -0
  340. package/dist/state/schemas/fleet-state.js.map +1 -0
  341. package/dist/state/schemas/index.d.ts +10 -0
  342. package/dist/state/schemas/index.d.ts.map +1 -0
  343. package/dist/state/schemas/index.js +10 -0
  344. package/dist/state/schemas/index.js.map +1 -0
  345. package/dist/state/schemas/job-metadata.d.ts +118 -0
  346. package/dist/state/schemas/job-metadata.d.ts.map +1 -0
  347. package/dist/state/schemas/job-metadata.js +123 -0
  348. package/dist/state/schemas/job-metadata.js.map +1 -0
  349. package/dist/state/schemas/job-output.d.ts +291 -0
  350. package/dist/state/schemas/job-output.d.ts.map +1 -0
  351. package/dist/state/schemas/job-output.js +132 -0
  352. package/dist/state/schemas/job-output.js.map +1 -0
  353. package/dist/state/schemas/session-info.d.ts +65 -0
  354. package/dist/state/schemas/session-info.d.ts.map +1 -0
  355. package/dist/state/schemas/session-info.js +58 -0
  356. package/dist/state/schemas/session-info.js.map +1 -0
  357. package/dist/state/session.d.ts +92 -0
  358. package/dist/state/session.d.ts.map +1 -0
  359. package/dist/state/session.js +173 -0
  360. package/dist/state/session.js.map +1 -0
  361. package/dist/state/types.d.ts +54 -0
  362. package/dist/state/types.d.ts.map +1 -0
  363. package/dist/state/types.js +18 -0
  364. package/dist/state/types.js.map +1 -0
  365. package/dist/state/utils/__tests__/atomic.test.d.ts +2 -0
  366. package/dist/state/utils/__tests__/atomic.test.d.ts.map +1 -0
  367. package/dist/state/utils/__tests__/atomic.test.js +537 -0
  368. package/dist/state/utils/__tests__/atomic.test.js.map +1 -0
  369. package/dist/state/utils/__tests__/reads.test.d.ts +2 -0
  370. package/dist/state/utils/__tests__/reads.test.d.ts.map +1 -0
  371. package/dist/state/utils/__tests__/reads.test.js +792 -0
  372. package/dist/state/utils/__tests__/reads.test.js.map +1 -0
  373. package/dist/state/utils/atomic.d.ts +89 -0
  374. package/dist/state/utils/atomic.d.ts.map +1 -0
  375. package/dist/state/utils/atomic.js +157 -0
  376. package/dist/state/utils/atomic.js.map +1 -0
  377. package/dist/state/utils/index.d.ts +6 -0
  378. package/dist/state/utils/index.d.ts.map +1 -0
  379. package/dist/state/utils/index.js +6 -0
  380. package/dist/state/utils/index.js.map +1 -0
  381. package/dist/state/utils/reads.d.ts +196 -0
  382. package/dist/state/utils/reads.d.ts.map +1 -0
  383. package/dist/state/utils/reads.js +346 -0
  384. package/dist/state/utils/reads.js.map +1 -0
  385. package/dist/work-sources/__tests__/github.test.d.ts +2 -0
  386. package/dist/work-sources/__tests__/github.test.d.ts.map +1 -0
  387. package/dist/work-sources/__tests__/github.test.js +1334 -0
  388. package/dist/work-sources/__tests__/github.test.js.map +1 -0
  389. package/dist/work-sources/__tests__/manager.test.d.ts +2 -0
  390. package/dist/work-sources/__tests__/manager.test.d.ts.map +1 -0
  391. package/dist/work-sources/__tests__/manager.test.js +424 -0
  392. package/dist/work-sources/__tests__/manager.test.js.map +1 -0
  393. package/dist/work-sources/__tests__/registry.test.d.ts +2 -0
  394. package/dist/work-sources/__tests__/registry.test.d.ts.map +1 -0
  395. package/dist/work-sources/__tests__/registry.test.js +381 -0
  396. package/dist/work-sources/__tests__/registry.test.js.map +1 -0
  397. package/dist/work-sources/__tests__/types.test.d.ts +2 -0
  398. package/dist/work-sources/__tests__/types.test.d.ts.map +1 -0
  399. package/dist/work-sources/__tests__/types.test.js +406 -0
  400. package/dist/work-sources/__tests__/types.test.js.map +1 -0
  401. package/dist/work-sources/adapters/github.d.ts +290 -0
  402. package/dist/work-sources/adapters/github.d.ts.map +1 -0
  403. package/dist/work-sources/adapters/github.js +803 -0
  404. package/dist/work-sources/adapters/github.js.map +1 -0
  405. package/dist/work-sources/adapters/index.d.ts +10 -0
  406. package/dist/work-sources/adapters/index.d.ts.map +1 -0
  407. package/dist/work-sources/adapters/index.js +31 -0
  408. package/dist/work-sources/adapters/index.js.map +1 -0
  409. package/dist/work-sources/errors.d.ts +40 -0
  410. package/dist/work-sources/errors.d.ts.map +1 -0
  411. package/dist/work-sources/errors.js +54 -0
  412. package/dist/work-sources/errors.js.map +1 -0
  413. package/dist/work-sources/index.d.ts +105 -0
  414. package/dist/work-sources/index.d.ts.map +1 -0
  415. package/dist/work-sources/index.js +24 -0
  416. package/dist/work-sources/index.js.map +1 -0
  417. package/dist/work-sources/manager.d.ts +370 -0
  418. package/dist/work-sources/manager.d.ts.map +1 -0
  419. package/dist/work-sources/manager.js +61 -0
  420. package/dist/work-sources/manager.js.map +1 -0
  421. package/dist/work-sources/registry.d.ts +128 -0
  422. package/dist/work-sources/registry.d.ts.map +1 -0
  423. package/dist/work-sources/registry.js +132 -0
  424. package/dist/work-sources/registry.js.map +1 -0
  425. package/dist/work-sources/types.d.ts +127 -0
  426. package/dist/work-sources/types.d.ts.map +1 -0
  427. package/dist/work-sources/types.js +8 -0
  428. package/dist/work-sources/types.js.map +1 -0
  429. package/package.json +23 -0
  430. package/src/config/__tests__/agent.test.ts +864 -0
  431. package/src/config/__tests__/interpolate.test.ts +644 -0
  432. package/src/config/__tests__/loader.test.ts +784 -0
  433. package/src/config/__tests__/merge.test.ts +751 -0
  434. package/src/config/__tests__/parser.test.ts +533 -0
  435. package/src/config/__tests__/schema.test.ts +873 -0
  436. package/src/config/index.ts +119 -0
  437. package/src/config/interpolate.ts +189 -0
  438. package/src/config/loader.ts +472 -0
  439. package/src/config/merge.ts +246 -0
  440. package/src/config/parser.ts +376 -0
  441. package/src/config/schema.ts +346 -0
  442. package/src/fleet-manager/__tests__/coverage.test.ts +2869 -0
  443. package/src/fleet-manager/__tests__/errors.test.ts +660 -0
  444. package/src/fleet-manager/__tests__/event-helpers.test.ts +448 -0
  445. package/src/fleet-manager/__tests__/integration.test.ts +1209 -0
  446. package/src/fleet-manager/__tests__/job-control.test.ts +283 -0
  447. package/src/fleet-manager/__tests__/job-manager.test.ts +869 -0
  448. package/src/fleet-manager/__tests__/job-queue.test.ts +401 -0
  449. package/src/fleet-manager/__tests__/reload.test.ts +751 -0
  450. package/src/fleet-manager/__tests__/status-queries.test.ts +595 -0
  451. package/src/fleet-manager/__tests__/trigger.test.ts +601 -0
  452. package/src/fleet-manager/errors.ts +747 -0
  453. package/src/fleet-manager/event-types.ts +378 -0
  454. package/src/fleet-manager/fleet-manager.ts +2315 -0
  455. package/src/fleet-manager/index.ts +128 -0
  456. package/src/fleet-manager/job-manager.ts +663 -0
  457. package/src/fleet-manager/job-queue.ts +798 -0
  458. package/src/fleet-manager/types.ts +839 -0
  459. package/src/index.ts +32 -0
  460. package/src/runner/__tests__/errors.test.ts +382 -0
  461. package/src/runner/__tests__/job-executor.test.ts +1708 -0
  462. package/src/runner/__tests__/message-processor.test.ts +960 -0
  463. package/src/runner/__tests__/sdk-adapter.test.ts +626 -0
  464. package/src/runner/errors.ts +307 -0
  465. package/src/runner/index.ts +57 -0
  466. package/src/runner/job-executor.ts +448 -0
  467. package/src/runner/message-processor.ts +355 -0
  468. package/src/runner/sdk-adapter.ts +191 -0
  469. package/src/runner/types.ts +158 -0
  470. package/src/scheduler/__tests__/errors.test.ts +159 -0
  471. package/src/scheduler/__tests__/interval.test.ts +515 -0
  472. package/src/scheduler/__tests__/schedule-runner.test.ts +798 -0
  473. package/src/scheduler/__tests__/schedule-state.test.ts +671 -0
  474. package/src/scheduler/__tests__/scheduler.test.ts +1280 -0
  475. package/src/scheduler/errors.ts +101 -0
  476. package/src/scheduler/index.ts +53 -0
  477. package/src/scheduler/interval.ts +189 -0
  478. package/src/scheduler/schedule-runner.ts +442 -0
  479. package/src/scheduler/schedule-state.ts +211 -0
  480. package/src/scheduler/scheduler.ts +570 -0
  481. package/src/scheduler/types.ts +216 -0
  482. package/src/state/__tests__/directory.test.ts +595 -0
  483. package/src/state/__tests__/fleet-state.test.ts +868 -0
  484. package/src/state/__tests__/job-metadata-schema.test.ts +414 -0
  485. package/src/state/__tests__/job-metadata.test.ts +831 -0
  486. package/src/state/__tests__/job-output.test.ts +856 -0
  487. package/src/state/__tests__/session-schema.test.ts +378 -0
  488. package/src/state/__tests__/session.test.ts +604 -0
  489. package/src/state/directory.ts +217 -0
  490. package/src/state/errors.ts +97 -0
  491. package/src/state/fleet-state.ts +284 -0
  492. package/src/state/index.ts +79 -0
  493. package/src/state/job-metadata.ts +445 -0
  494. package/src/state/job-output.ts +316 -0
  495. package/src/state/schemas/__tests__/job-output.test.ts +338 -0
  496. package/src/state/schemas/fleet-state.ts +120 -0
  497. package/src/state/schemas/index.ts +67 -0
  498. package/src/state/schemas/job-metadata.ts +181 -0
  499. package/src/state/schemas/job-output.ts +177 -0
  500. package/src/state/schemas/session-info.ts +92 -0
  501. package/src/state/session.ts +253 -0
  502. package/src/state/types.ts +59 -0
  503. package/src/state/utils/__tests__/atomic.test.ts +723 -0
  504. package/src/state/utils/__tests__/reads.test.ts +1071 -0
  505. package/src/state/utils/atomic.ts +221 -0
  506. package/src/state/utils/index.ts +6 -0
  507. package/src/state/utils/reads.ts +512 -0
  508. package/src/work-sources/__tests__/github.test.ts +1800 -0
  509. package/src/work-sources/__tests__/manager.test.ts +529 -0
  510. package/src/work-sources/__tests__/registry.test.ts +477 -0
  511. package/src/work-sources/__tests__/types.test.ts +479 -0
  512. package/src/work-sources/adapters/github.ts +1166 -0
  513. package/src/work-sources/adapters/index.ts +64 -0
  514. package/src/work-sources/errors.ts +71 -0
  515. package/src/work-sources/index.ts +148 -0
  516. package/src/work-sources/manager.ts +413 -0
  517. package/src/work-sources/registry.ts +178 -0
  518. package/src/work-sources/types.ts +161 -0
  519. package/tsconfig.json +9 -0
  520. package/vitest.config.ts +19 -0
@@ -0,0 +1,1280 @@
1
+ import {
2
+ describe,
3
+ it,
4
+ expect,
5
+ beforeEach,
6
+ afterEach,
7
+ vi,
8
+ type MockInstance,
9
+ } from "vitest";
10
+ import { mkdir, rm, realpath } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { Scheduler } from "../scheduler.js";
14
+ import { SchedulerShutdownError } from "../errors.js";
15
+ import type {
16
+ SchedulerOptions,
17
+ SchedulerLogger,
18
+ TriggerInfo,
19
+ StopOptions,
20
+ } from "../types.js";
21
+ import type { ResolvedAgent } from "../../config/index.js";
22
+ import { writeFleetState, readFleetState } from "../../state/fleet-state.js";
23
+ import type { FleetState } from "../../state/schemas/fleet-state.js";
24
+
25
+ // Helper to create a temp directory
26
+ async function createTempDir(): Promise<string> {
27
+ const baseDir = join(
28
+ tmpdir(),
29
+ `herdctl-scheduler-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
30
+ );
31
+ await mkdir(baseDir, { recursive: true });
32
+ // Resolve to real path to handle macOS /var -> /private/var symlink
33
+ return await realpath(baseDir);
34
+ }
35
+
36
+ // Helper to create a mock logger
37
+ function createMockLogger(): SchedulerLogger & {
38
+ debugs: string[];
39
+ infos: string[];
40
+ warnings: string[];
41
+ errors: string[];
42
+ } {
43
+ const debugs: string[] = [];
44
+ const infos: string[] = [];
45
+ const warnings: string[] = [];
46
+ const errors: string[] = [];
47
+ return {
48
+ debugs,
49
+ infos,
50
+ warnings,
51
+ errors,
52
+ debug: (message: string) => debugs.push(message),
53
+ info: (message: string) => infos.push(message),
54
+ warn: (message: string) => warnings.push(message),
55
+ error: (message: string) => errors.push(message),
56
+ };
57
+ }
58
+
59
+ // Helper to create a test agent
60
+ function createTestAgent(
61
+ name: string,
62
+ schedules?: Record<string, { type: string; interval?: string; prompt?: string }>
63
+ ): ResolvedAgent {
64
+ return {
65
+ name,
66
+ configPath: `/fake/path/${name}.yaml`,
67
+ schedules,
68
+ } as ResolvedAgent;
69
+ }
70
+
71
+ // Helper to wait for a short period
72
+ function wait(ms: number): Promise<void> {
73
+ return new Promise((resolve) => setTimeout(resolve, ms));
74
+ }
75
+
76
+ describe("Scheduler", () => {
77
+ let tempDir: string;
78
+ let mockLogger: ReturnType<typeof createMockLogger>;
79
+
80
+ beforeEach(async () => {
81
+ tempDir = await createTempDir();
82
+ mockLogger = createMockLogger();
83
+ });
84
+
85
+ afterEach(async () => {
86
+ await rm(tempDir, { recursive: true, force: true });
87
+ });
88
+
89
+ describe("constructor", () => {
90
+ it("creates scheduler with default check interval", () => {
91
+ const scheduler = new Scheduler({
92
+ stateDir: tempDir,
93
+ logger: mockLogger,
94
+ });
95
+
96
+ expect(scheduler.getStatus()).toBe("stopped");
97
+ expect(scheduler.isRunning()).toBe(false);
98
+ });
99
+
100
+ it("creates scheduler with custom check interval", () => {
101
+ const scheduler = new Scheduler({
102
+ stateDir: tempDir,
103
+ checkInterval: 500,
104
+ logger: mockLogger,
105
+ });
106
+
107
+ expect(scheduler.getStatus()).toBe("stopped");
108
+ });
109
+ });
110
+
111
+ describe("isRunning", () => {
112
+ it("returns false when stopped", () => {
113
+ const scheduler = new Scheduler({
114
+ stateDir: tempDir,
115
+ logger: mockLogger,
116
+ });
117
+
118
+ expect(scheduler.isRunning()).toBe(false);
119
+ });
120
+
121
+ it("returns true when running", async () => {
122
+ const scheduler = new Scheduler({
123
+ stateDir: tempDir,
124
+ checkInterval: 100,
125
+ logger: mockLogger,
126
+ });
127
+
128
+ // Start in background
129
+ const startPromise = scheduler.start([]);
130
+
131
+ // Wait a tick for status to update
132
+ await wait(10);
133
+ expect(scheduler.isRunning()).toBe(true);
134
+
135
+ // Stop the scheduler
136
+ await scheduler.stop();
137
+ await startPromise;
138
+ });
139
+ });
140
+
141
+ describe("getStatus", () => {
142
+ it("returns stopped initially", () => {
143
+ const scheduler = new Scheduler({
144
+ stateDir: tempDir,
145
+ logger: mockLogger,
146
+ });
147
+
148
+ expect(scheduler.getStatus()).toBe("stopped");
149
+ });
150
+
151
+ it("returns running when started", async () => {
152
+ const scheduler = new Scheduler({
153
+ stateDir: tempDir,
154
+ checkInterval: 100,
155
+ logger: mockLogger,
156
+ });
157
+
158
+ const startPromise = scheduler.start([]);
159
+ await wait(10);
160
+
161
+ expect(scheduler.getStatus()).toBe("running");
162
+
163
+ await scheduler.stop();
164
+ await startPromise;
165
+ });
166
+
167
+ it("returns stopped after stop", async () => {
168
+ const scheduler = new Scheduler({
169
+ stateDir: tempDir,
170
+ checkInterval: 100,
171
+ logger: mockLogger,
172
+ });
173
+
174
+ const startPromise = scheduler.start([]);
175
+ await wait(10);
176
+ await scheduler.stop();
177
+ await startPromise;
178
+
179
+ expect(scheduler.getStatus()).toBe("stopped");
180
+ });
181
+ });
182
+
183
+ describe("getState", () => {
184
+ it("returns initial state", () => {
185
+ const scheduler = new Scheduler({
186
+ stateDir: tempDir,
187
+ logger: mockLogger,
188
+ });
189
+
190
+ const state = scheduler.getState();
191
+
192
+ expect(state.status).toBe("stopped");
193
+ expect(state.startedAt).toBeNull();
194
+ expect(state.checkCount).toBe(0);
195
+ expect(state.triggerCount).toBe(0);
196
+ expect(state.lastCheckAt).toBeNull();
197
+ });
198
+
199
+ it("updates startedAt when started", async () => {
200
+ const scheduler = new Scheduler({
201
+ stateDir: tempDir,
202
+ checkInterval: 100,
203
+ logger: mockLogger,
204
+ });
205
+
206
+ const beforeStart = new Date().toISOString();
207
+ const startPromise = scheduler.start([]);
208
+ await wait(10);
209
+
210
+ const state = scheduler.getState();
211
+ expect(state.startedAt).not.toBeNull();
212
+ expect(new Date(state.startedAt!).getTime()).toBeGreaterThanOrEqual(
213
+ new Date(beforeStart).getTime()
214
+ );
215
+
216
+ await scheduler.stop();
217
+ await startPromise;
218
+ });
219
+
220
+ it("increments checkCount", async () => {
221
+ const scheduler = new Scheduler({
222
+ stateDir: tempDir,
223
+ checkInterval: 50,
224
+ logger: mockLogger,
225
+ });
226
+
227
+ const startPromise = scheduler.start([]);
228
+
229
+ // Wait for a few checks
230
+ await wait(180);
231
+
232
+ const state = scheduler.getState();
233
+ expect(state.checkCount).toBeGreaterThan(0);
234
+
235
+ await scheduler.stop();
236
+ await startPromise;
237
+ });
238
+ });
239
+
240
+ describe("start", () => {
241
+ it("starts the scheduler", async () => {
242
+ const scheduler = new Scheduler({
243
+ stateDir: tempDir,
244
+ checkInterval: 100,
245
+ logger: mockLogger,
246
+ });
247
+
248
+ const startPromise = scheduler.start([]);
249
+ await wait(10);
250
+
251
+ expect(scheduler.isRunning()).toBe(true);
252
+ expect(mockLogger.infos.some((m) => m.includes("started"))).toBe(true);
253
+
254
+ await scheduler.stop();
255
+ await startPromise;
256
+ });
257
+
258
+ it("throws if already running", async () => {
259
+ const scheduler = new Scheduler({
260
+ stateDir: tempDir,
261
+ checkInterval: 100,
262
+ logger: mockLogger,
263
+ });
264
+
265
+ const startPromise = scheduler.start([]);
266
+ await wait(10);
267
+
268
+ await expect(scheduler.start([])).rejects.toThrow("already running");
269
+
270
+ await scheduler.stop();
271
+ await startPromise;
272
+ });
273
+
274
+ it("logs the number of agents and check interval", async () => {
275
+ const scheduler = new Scheduler({
276
+ stateDir: tempDir,
277
+ checkInterval: 500,
278
+ logger: mockLogger,
279
+ });
280
+
281
+ const agents = [
282
+ createTestAgent("agent-1"),
283
+ createTestAgent("agent-2"),
284
+ ];
285
+
286
+ const startPromise = scheduler.start(agents);
287
+ await wait(10);
288
+
289
+ expect(
290
+ mockLogger.infos.some(
291
+ (m) => m.includes("2 agents") && m.includes("500ms")
292
+ )
293
+ ).toBe(true);
294
+
295
+ await scheduler.stop();
296
+ await startPromise;
297
+ });
298
+ });
299
+
300
+ describe("stop", () => {
301
+ it("stops the scheduler", async () => {
302
+ const scheduler = new Scheduler({
303
+ stateDir: tempDir,
304
+ checkInterval: 100,
305
+ logger: mockLogger,
306
+ });
307
+
308
+ const startPromise = scheduler.start([]);
309
+ await wait(10);
310
+
311
+ await scheduler.stop();
312
+ await startPromise;
313
+
314
+ expect(scheduler.isRunning()).toBe(false);
315
+ expect(mockLogger.infos.some((m) => m.includes("stopped"))).toBe(true);
316
+ });
317
+
318
+ it("does nothing if already stopped", async () => {
319
+ const scheduler = new Scheduler({
320
+ stateDir: tempDir,
321
+ logger: mockLogger,
322
+ });
323
+
324
+ await scheduler.stop(); // Should not throw
325
+
326
+ expect(scheduler.getStatus()).toBe("stopped");
327
+ });
328
+ });
329
+
330
+ describe("setAgents", () => {
331
+ it("updates the agents list", async () => {
332
+ const scheduler = new Scheduler({
333
+ stateDir: tempDir,
334
+ checkInterval: 100,
335
+ logger: mockLogger,
336
+ });
337
+
338
+ const startPromise = scheduler.start([]);
339
+ await wait(10);
340
+
341
+ const newAgents = [createTestAgent("new-agent")];
342
+ scheduler.setAgents(newAgents);
343
+
344
+ expect(
345
+ mockLogger.debugs.some((m) => m.includes("Updated agents list"))
346
+ ).toBe(true);
347
+
348
+ await scheduler.stop();
349
+ await startPromise;
350
+ });
351
+ });
352
+
353
+ describe("schedule checking", () => {
354
+ it("skips agents without schedules", async () => {
355
+ const scheduler = new Scheduler({
356
+ stateDir: tempDir,
357
+ checkInterval: 50,
358
+ logger: mockLogger,
359
+ });
360
+
361
+ const agents = [createTestAgent("no-schedules-agent")];
362
+
363
+ const startPromise = scheduler.start(agents);
364
+ await wait(100);
365
+
366
+ // Should complete check without errors
367
+ const state = scheduler.getState();
368
+ expect(state.checkCount).toBeGreaterThan(0);
369
+ expect(state.triggerCount).toBe(0);
370
+
371
+ await scheduler.stop();
372
+ await startPromise;
373
+ });
374
+
375
+ it("skips non-interval schedule types", async () => {
376
+ const scheduler = new Scheduler({
377
+ stateDir: tempDir,
378
+ checkInterval: 50,
379
+ logger: mockLogger,
380
+ });
381
+
382
+ const agents = [
383
+ createTestAgent("cron-agent", {
384
+ hourly: { type: "cron", interval: "0 * * * *" },
385
+ }),
386
+ createTestAgent("webhook-agent", {
387
+ webhook: { type: "webhook" },
388
+ }),
389
+ createTestAgent("chat-agent", {
390
+ chat: { type: "chat" },
391
+ }),
392
+ ];
393
+
394
+ const startPromise = scheduler.start(agents);
395
+ await wait(100);
396
+
397
+ // Should not trigger any schedules
398
+ const state = scheduler.getState();
399
+ expect(state.triggerCount).toBe(0);
400
+
401
+ await scheduler.stop();
402
+ await startPromise;
403
+ });
404
+
405
+ it("skips disabled schedules", async () => {
406
+ const stateFile = join(tempDir, "state.yaml");
407
+ const initialState: FleetState = {
408
+ fleet: {},
409
+ agents: {
410
+ "test-agent": {
411
+ status: "idle",
412
+ schedules: {
413
+ hourly: {
414
+ last_run_at: null,
415
+ next_run_at: null,
416
+ status: "disabled",
417
+ last_error: null,
418
+ },
419
+ },
420
+ },
421
+ },
422
+ };
423
+ await writeFleetState(stateFile, initialState);
424
+
425
+ const scheduler = new Scheduler({
426
+ stateDir: tempDir,
427
+ checkInterval: 50,
428
+ logger: mockLogger,
429
+ });
430
+
431
+ const agents = [
432
+ createTestAgent("test-agent", {
433
+ hourly: { type: "interval", interval: "1s" },
434
+ }),
435
+ ];
436
+
437
+ const startPromise = scheduler.start(agents);
438
+ await wait(100);
439
+
440
+ expect(
441
+ mockLogger.debugs.some((m) => m.includes("disabled"))
442
+ ).toBe(true);
443
+ expect(scheduler.getState().triggerCount).toBe(0);
444
+
445
+ await scheduler.stop();
446
+ await startPromise;
447
+ });
448
+
449
+ it("skips schedules missing interval value", async () => {
450
+ const scheduler = new Scheduler({
451
+ stateDir: tempDir,
452
+ checkInterval: 50,
453
+ logger: mockLogger,
454
+ });
455
+
456
+ const agents = [
457
+ createTestAgent("test-agent", {
458
+ broken: { type: "interval" }, // Missing interval value
459
+ }),
460
+ ];
461
+
462
+ const startPromise = scheduler.start(agents);
463
+ await wait(100);
464
+
465
+ expect(
466
+ mockLogger.warnings.some((m) => m.includes("missing interval value"))
467
+ ).toBe(true);
468
+ expect(scheduler.getState().triggerCount).toBe(0);
469
+
470
+ await scheduler.stop();
471
+ await startPromise;
472
+ });
473
+
474
+ it("triggers due interval schedules", async () => {
475
+ const triggers: TriggerInfo[] = [];
476
+
477
+ const scheduler = new Scheduler({
478
+ stateDir: tempDir,
479
+ checkInterval: 50,
480
+ logger: mockLogger,
481
+ onTrigger: async (info) => {
482
+ triggers.push(info);
483
+ },
484
+ });
485
+
486
+ const agents = [
487
+ createTestAgent("test-agent", {
488
+ hourly: { type: "interval", interval: "1s", prompt: "test prompt" },
489
+ }),
490
+ ];
491
+
492
+ const startPromise = scheduler.start(agents);
493
+ await wait(150);
494
+
495
+ // First run should trigger immediately (no last_run_at)
496
+ expect(triggers.length).toBeGreaterThan(0);
497
+ expect(triggers[0].agent.name).toBe("test-agent");
498
+ expect(triggers[0].scheduleName).toBe("hourly");
499
+
500
+ await scheduler.stop();
501
+ await startPromise;
502
+ });
503
+
504
+ it("updates schedule state on trigger", async () => {
505
+ const scheduler = new Scheduler({
506
+ stateDir: tempDir,
507
+ checkInterval: 50,
508
+ logger: mockLogger,
509
+ onTrigger: async () => {
510
+ // Simulate work
511
+ await wait(10);
512
+ },
513
+ });
514
+
515
+ const agents = [
516
+ createTestAgent("test-agent", {
517
+ hourly: { type: "interval", interval: "1s" },
518
+ }),
519
+ ];
520
+
521
+ const startPromise = scheduler.start(agents);
522
+ await wait(150);
523
+
524
+ // Check state was updated
525
+ const stateFile = join(tempDir, "state.yaml");
526
+ const fleetState = await readFleetState(stateFile);
527
+ const scheduleState = fleetState.agents["test-agent"]?.schedules?.hourly;
528
+
529
+ expect(scheduleState).toBeDefined();
530
+ expect(scheduleState?.last_run_at).not.toBeNull();
531
+ expect(scheduleState?.status).toBe("idle"); // Should be idle after completion
532
+
533
+ await scheduler.stop();
534
+ await startPromise;
535
+ });
536
+
537
+ it("records error in schedule state on trigger failure", async () => {
538
+ const scheduler = new Scheduler({
539
+ stateDir: tempDir,
540
+ checkInterval: 50,
541
+ logger: mockLogger,
542
+ onTrigger: async () => {
543
+ throw new Error("Trigger failed!");
544
+ },
545
+ });
546
+
547
+ const agents = [
548
+ createTestAgent("test-agent", {
549
+ hourly: { type: "interval", interval: "1s" },
550
+ }),
551
+ ];
552
+
553
+ const startPromise = scheduler.start(agents);
554
+ await wait(150);
555
+
556
+ // Check error was recorded
557
+ const stateFile = join(tempDir, "state.yaml");
558
+ const fleetState = await readFleetState(stateFile);
559
+ const scheduleState = fleetState.agents["test-agent"]?.schedules?.hourly;
560
+
561
+ expect(scheduleState?.last_error).toBe("Trigger failed!");
562
+ expect(mockLogger.errors.some((m) => m.includes("Trigger failed!"))).toBe(
563
+ true
564
+ );
565
+
566
+ await scheduler.stop();
567
+ await startPromise;
568
+ });
569
+
570
+ it("does not trigger already running schedule", async () => {
571
+ let triggerCount = 0;
572
+ let isRunning = false;
573
+
574
+ const scheduler = new Scheduler({
575
+ stateDir: tempDir,
576
+ checkInterval: 30,
577
+ logger: mockLogger,
578
+ onTrigger: async () => {
579
+ if (isRunning) {
580
+ throw new Error("Should not trigger while running!");
581
+ }
582
+ isRunning = true;
583
+ triggerCount++;
584
+ // Simulate long-running job
585
+ await wait(100);
586
+ isRunning = false;
587
+ },
588
+ });
589
+
590
+ const agents = [
591
+ createTestAgent("test-agent", {
592
+ hourly: { type: "interval", interval: "1s" },
593
+ }),
594
+ ];
595
+
596
+ const startPromise = scheduler.start(agents);
597
+
598
+ // Wait long enough for multiple checks but only one trigger should complete
599
+ await wait(150);
600
+
601
+ // Should only have triggered once due to running check
602
+ expect(triggerCount).toBe(1);
603
+
604
+ await scheduler.stop();
605
+ await startPromise;
606
+ });
607
+ });
608
+
609
+ describe("error handling", () => {
610
+ it("continues checking after error in check cycle", async () => {
611
+ // Create scheduler that will encounter an error reading state
612
+ // by using a non-existent state dir initially
613
+ const scheduler = new Scheduler({
614
+ stateDir: "/nonexistent/path",
615
+ checkInterval: 50,
616
+ logger: mockLogger,
617
+ });
618
+
619
+ const agents = [
620
+ createTestAgent("test-agent", {
621
+ hourly: { type: "interval", interval: "1s" },
622
+ }),
623
+ ];
624
+
625
+ const startPromise = scheduler.start(agents);
626
+ await wait(150);
627
+
628
+ // Should have logged errors but kept running
629
+ expect(scheduler.isRunning()).toBe(true);
630
+ expect(scheduler.getState().checkCount).toBeGreaterThan(0);
631
+
632
+ await scheduler.stop();
633
+ await startPromise;
634
+ });
635
+ });
636
+
637
+ describe("concurrent execution", () => {
638
+ it("tracks running schedules per agent", async () => {
639
+ const runningAgents = new Set<string>();
640
+ let maxConcurrent = 0;
641
+
642
+ // Use a barrier to ensure both triggers are running at the same time
643
+ let triggered = 0;
644
+ let resolveBarrier: () => void;
645
+ const bothStarted = new Promise<void>((resolve) => {
646
+ resolveBarrier = resolve;
647
+ });
648
+
649
+ const checkBoth = () => {
650
+ triggered++;
651
+ if (triggered >= 2) resolveBarrier();
652
+ };
653
+
654
+ const scheduler = new Scheduler({
655
+ stateDir: tempDir,
656
+ checkInterval: 30,
657
+ logger: mockLogger,
658
+ onTrigger: async (info) => {
659
+ runningAgents.add(`${info.agent.name}/${info.scheduleName}`);
660
+ maxConcurrent = Math.max(maxConcurrent, runningAgents.size);
661
+ // Signal that this trigger started
662
+ checkBoth();
663
+ // Wait long enough for both to be running
664
+ await wait(100);
665
+ runningAgents.delete(`${info.agent.name}/${info.scheduleName}`);
666
+ },
667
+ });
668
+
669
+ const agents = [
670
+ createTestAgent("agent-1", {
671
+ schedule1: { type: "interval", interval: "1s" },
672
+ }),
673
+ createTestAgent("agent-2", {
674
+ schedule2: { type: "interval", interval: "1s" },
675
+ }),
676
+ ];
677
+
678
+ const startPromise = scheduler.start(agents);
679
+
680
+ // Wait for both to have started running
681
+ await Promise.race([bothStarted, wait(300)]);
682
+
683
+ // Check max concurrent - may be 1 or 2 depending on timing
684
+ // The important thing is both agents were triggered
685
+ expect(maxConcurrent).toBeGreaterThanOrEqual(1);
686
+
687
+ await scheduler.stop();
688
+ await startPromise;
689
+ });
690
+ });
691
+
692
+ describe("max_concurrent limit", () => {
693
+ it("respects max_concurrent from agent instances config", async () => {
694
+ const triggerCounts = new Map<string, number>();
695
+ let concurrentForAgent2 = 0;
696
+ let maxConcurrentForAgent2 = 0;
697
+
698
+ const scheduler = new Scheduler({
699
+ stateDir: tempDir,
700
+ checkInterval: 30,
701
+ logger: mockLogger,
702
+ onTrigger: async (info) => {
703
+ const key = info.agent.name;
704
+ const count = (triggerCounts.get(key) || 0) + 1;
705
+ triggerCounts.set(key, count);
706
+
707
+ if (info.agent.name === "agent-2") {
708
+ concurrentForAgent2++;
709
+ maxConcurrentForAgent2 = Math.max(
710
+ maxConcurrentForAgent2,
711
+ concurrentForAgent2
712
+ );
713
+ }
714
+
715
+ // Simulate work
716
+ await wait(80);
717
+
718
+ if (info.agent.name === "agent-2") {
719
+ concurrentForAgent2--;
720
+ }
721
+ },
722
+ });
723
+
724
+ // Agent with max_concurrent: 2
725
+ const agentWithMaxConcurrent2 = {
726
+ ...createTestAgent("agent-2", {
727
+ schedule1: { type: "interval", interval: "1s" },
728
+ schedule2: { type: "interval", interval: "1s" },
729
+ schedule3: { type: "interval", interval: "1s" },
730
+ }),
731
+ instances: { max_concurrent: 2 },
732
+ } as ResolvedAgent;
733
+
734
+ const startPromise = scheduler.start([agentWithMaxConcurrent2]);
735
+
736
+ // Wait enough time for multiple checks
737
+ await wait(200);
738
+
739
+ // Should not exceed max_concurrent of 2
740
+ expect(maxConcurrentForAgent2).toBeLessThanOrEqual(2);
741
+
742
+ await scheduler.stop();
743
+ await startPromise;
744
+ });
745
+
746
+ it("defaults max_concurrent to 1 when not specified", async () => {
747
+ let concurrentForAgent = 0;
748
+ let maxConcurrentForAgent = 0;
749
+
750
+ const scheduler = new Scheduler({
751
+ stateDir: tempDir,
752
+ checkInterval: 30,
753
+ logger: mockLogger,
754
+ onTrigger: async () => {
755
+ concurrentForAgent++;
756
+ maxConcurrentForAgent = Math.max(
757
+ maxConcurrentForAgent,
758
+ concurrentForAgent
759
+ );
760
+
761
+ await wait(80);
762
+
763
+ concurrentForAgent--;
764
+ },
765
+ });
766
+
767
+ // Agent without instances config - should default to max_concurrent: 1
768
+ const agent = createTestAgent("test-agent", {
769
+ schedule1: { type: "interval", interval: "1s" },
770
+ schedule2: { type: "interval", interval: "1s" },
771
+ });
772
+
773
+ const startPromise = scheduler.start([agent]);
774
+
775
+ await wait(200);
776
+
777
+ // Should not exceed default max_concurrent of 1
778
+ expect(maxConcurrentForAgent).toBe(1);
779
+
780
+ await scheduler.stop();
781
+ await startPromise;
782
+ });
783
+
784
+ it("skips second schedule when agent is at capacity", async () => {
785
+ // This test verifies that with max_concurrent: 1 and one schedule already
786
+ // running, additional schedules are not triggered until the first completes.
787
+ //
788
+ // Note: Due to how the scheduler iterates through schedules synchronously
789
+ // within a single check cycle, both schedules will be checked and triggered
790
+ // if they're both due at the same time before either is marked as running.
791
+ // The capacity check applies WITHIN each check cycle iteration, so the
792
+ // second schedule is only skipped if a trigger is already in progress
793
+ // from a PREVIOUS check cycle.
794
+
795
+ let triggerCount = 0;
796
+
797
+ const scheduler = new Scheduler({
798
+ stateDir: tempDir,
799
+ checkInterval: 30,
800
+ logger: mockLogger,
801
+ onTrigger: async () => {
802
+ triggerCount++;
803
+ // Long running job to span multiple check cycles
804
+ await wait(150);
805
+ },
806
+ });
807
+
808
+ // Agent with max_concurrent: 1 and one schedule
809
+ // A second schedule that becomes due AFTER the first is running
810
+ // will be skipped
811
+ const agent = createTestAgent("test-agent", {
812
+ schedule1: { type: "interval", interval: "1s" },
813
+ });
814
+
815
+ const startPromise = scheduler.start([agent]);
816
+
817
+ // Wait for first trigger to happen and start running
818
+ await wait(100);
819
+
820
+ // With 1 schedule, it should trigger once initially
821
+ // Then subsequent checks should show "already_running" until it completes
822
+ expect(triggerCount).toBe(1);
823
+ expect(scheduler.getRunningJobCount("test-agent")).toBe(1);
824
+
825
+ // Wait for the job to complete
826
+ await wait(200);
827
+
828
+ // After completion, running count should be 0
829
+ expect(scheduler.getRunningJobCount("test-agent")).toBe(0);
830
+
831
+ await scheduler.stop();
832
+ await startPromise;
833
+ });
834
+ });
835
+
836
+ describe("getRunningJobCount", () => {
837
+ it("returns 0 for agents with no running jobs", () => {
838
+ const scheduler = new Scheduler({
839
+ stateDir: tempDir,
840
+ logger: mockLogger,
841
+ });
842
+
843
+ expect(scheduler.getRunningJobCount("non-existent-agent")).toBe(0);
844
+ });
845
+
846
+ it("returns correct count during job execution", async () => {
847
+ let runningCountDuringExecution = -1;
848
+
849
+ const scheduler = new Scheduler({
850
+ stateDir: tempDir,
851
+ checkInterval: 30,
852
+ logger: mockLogger,
853
+ onTrigger: async (info) => {
854
+ // Check count while job is running
855
+ runningCountDuringExecution = scheduler.getRunningJobCount(
856
+ info.agent.name
857
+ );
858
+ await wait(50);
859
+ },
860
+ });
861
+
862
+ const agent = createTestAgent("test-agent", {
863
+ hourly: { type: "interval", interval: "1s" },
864
+ });
865
+
866
+ const startPromise = scheduler.start([agent]);
867
+
868
+ await wait(100);
869
+
870
+ // Count should have been 1 during execution
871
+ expect(runningCountDuringExecution).toBe(1);
872
+
873
+ await scheduler.stop();
874
+ await startPromise;
875
+ });
876
+
877
+ it("returns 0 after job completes", async () => {
878
+ const scheduler = new Scheduler({
879
+ stateDir: tempDir,
880
+ checkInterval: 50,
881
+ logger: mockLogger,
882
+ onTrigger: async () => {
883
+ await wait(20);
884
+ },
885
+ });
886
+
887
+ const agent = createTestAgent("test-agent", {
888
+ hourly: { type: "interval", interval: "10s" }, // Long interval to prevent re-trigger
889
+ });
890
+
891
+ const startPromise = scheduler.start([agent]);
892
+
893
+ // Wait for trigger and completion
894
+ await wait(150);
895
+
896
+ // Count should be 0 after completion
897
+ expect(scheduler.getRunningJobCount("test-agent")).toBe(0);
898
+
899
+ await scheduler.stop();
900
+ await startPromise;
901
+ });
902
+
903
+ it("tracks multiple schedules for the same agent", async () => {
904
+ let maxCount = 0;
905
+
906
+ const scheduler = new Scheduler({
907
+ stateDir: tempDir,
908
+ checkInterval: 20,
909
+ logger: mockLogger,
910
+ onTrigger: async (info) => {
911
+ const count = scheduler.getRunningJobCount(info.agent.name);
912
+ maxCount = Math.max(maxCount, count);
913
+ await wait(100);
914
+ },
915
+ });
916
+
917
+ // Agent with max_concurrent: 3 and multiple schedules
918
+ const agent = {
919
+ ...createTestAgent("test-agent", {
920
+ schedule1: { type: "interval", interval: "1s" },
921
+ schedule2: { type: "interval", interval: "1s" },
922
+ schedule3: { type: "interval", interval: "1s" },
923
+ }),
924
+ instances: { max_concurrent: 3 },
925
+ } as ResolvedAgent;
926
+
927
+ const startPromise = scheduler.start([agent]);
928
+
929
+ await wait(150);
930
+
931
+ // Should have tracked multiple concurrent jobs
932
+ expect(maxCount).toBeGreaterThanOrEqual(1);
933
+
934
+ await scheduler.stop();
935
+ await startPromise;
936
+ });
937
+
938
+ it("decrements count on job failure", async () => {
939
+ let countAfterError = -1;
940
+
941
+ const scheduler = new Scheduler({
942
+ stateDir: tempDir,
943
+ checkInterval: 50,
944
+ logger: mockLogger,
945
+ onTrigger: async () => {
946
+ throw new Error("Job failed!");
947
+ },
948
+ });
949
+
950
+ const agent = createTestAgent("test-agent", {
951
+ hourly: { type: "interval", interval: "10s" },
952
+ });
953
+
954
+ const startPromise = scheduler.start([agent]);
955
+
956
+ // Wait for trigger and error handling
957
+ await wait(150);
958
+
959
+ countAfterError = scheduler.getRunningJobCount("test-agent");
960
+
961
+ // Count should be 0 even after error
962
+ expect(countAfterError).toBe(0);
963
+
964
+ await scheduler.stop();
965
+ await startPromise;
966
+ });
967
+ });
968
+
969
+ describe("graceful shutdown", () => {
970
+ it("waits for running jobs to complete by default", async () => {
971
+ let jobStarted = false;
972
+ let jobCompleted = false;
973
+ let resolveJob: () => void;
974
+ const jobPromise = new Promise<void>((resolve) => {
975
+ resolveJob = resolve;
976
+ });
977
+
978
+ const scheduler = new Scheduler({
979
+ stateDir: tempDir,
980
+ checkInterval: 30,
981
+ logger: mockLogger,
982
+ onTrigger: async () => {
983
+ jobStarted = true;
984
+ await jobPromise;
985
+ jobCompleted = true;
986
+ },
987
+ });
988
+
989
+ const agent = createTestAgent("test-agent", {
990
+ hourly: { type: "interval", interval: "1s" },
991
+ });
992
+
993
+ const startPromise = scheduler.start([agent]);
994
+ await wait(100);
995
+
996
+ // Job should be running
997
+ expect(jobStarted).toBe(true);
998
+ expect(jobCompleted).toBe(false);
999
+
1000
+ // Start shutdown (don't await yet)
1001
+ const stopPromise = scheduler.stop();
1002
+
1003
+ // Give stop a moment to set status
1004
+ await wait(10);
1005
+
1006
+ // Scheduler should be in "stopping" state, waiting for job
1007
+ expect(scheduler.getStatus()).toBe("stopping");
1008
+
1009
+ // Complete the job
1010
+ resolveJob!();
1011
+
1012
+ // Now stop should complete
1013
+ await stopPromise;
1014
+ await startPromise;
1015
+
1016
+ expect(jobCompleted).toBe(true);
1017
+ expect(scheduler.getStatus()).toBe("stopped");
1018
+ expect(mockLogger.infos.some((m) => m.includes("All running jobs completed"))).toBe(true);
1019
+ });
1020
+
1021
+ it("does not wait for jobs when waitForJobs is false", async () => {
1022
+ let jobStarted = false;
1023
+ let jobCompleted = false;
1024
+
1025
+ const scheduler = new Scheduler({
1026
+ stateDir: tempDir,
1027
+ checkInterval: 30,
1028
+ logger: mockLogger,
1029
+ onTrigger: async () => {
1030
+ jobStarted = true;
1031
+ await wait(500); // Long-running job
1032
+ jobCompleted = true;
1033
+ },
1034
+ });
1035
+
1036
+ const agent = createTestAgent("test-agent", {
1037
+ hourly: { type: "interval", interval: "1s" },
1038
+ });
1039
+
1040
+ const startPromise = scheduler.start([agent]);
1041
+ await wait(100);
1042
+
1043
+ // Job should be running
1044
+ expect(jobStarted).toBe(true);
1045
+ expect(jobCompleted).toBe(false);
1046
+
1047
+ // Stop without waiting for jobs
1048
+ await scheduler.stop({ waitForJobs: false });
1049
+
1050
+ // Scheduler should be stopped immediately, job may still be running
1051
+ expect(scheduler.getStatus()).toBe("stopped");
1052
+
1053
+ // Clean up the start promise
1054
+ await startPromise;
1055
+ });
1056
+
1057
+ it("throws SchedulerShutdownError on timeout", async () => {
1058
+ let resolveJob: () => void;
1059
+ const jobBlocker = new Promise<void>((resolve) => {
1060
+ resolveJob = resolve;
1061
+ });
1062
+
1063
+ const scheduler = new Scheduler({
1064
+ stateDir: tempDir,
1065
+ checkInterval: 30,
1066
+ logger: mockLogger,
1067
+ onTrigger: async () => {
1068
+ // Job that blocks until we release it
1069
+ await jobBlocker;
1070
+ },
1071
+ });
1072
+
1073
+ const agent = createTestAgent("test-agent", {
1074
+ hourly: { type: "interval", interval: "1s" },
1075
+ });
1076
+
1077
+ const startPromise = scheduler.start([agent]);
1078
+ await wait(100);
1079
+
1080
+ // Stop with a short timeout
1081
+ let shutdownError: SchedulerShutdownError | null = null;
1082
+ try {
1083
+ await scheduler.stop({ timeout: 50 });
1084
+ } catch (error) {
1085
+ if (error instanceof SchedulerShutdownError) {
1086
+ shutdownError = error;
1087
+ }
1088
+ }
1089
+
1090
+ expect(shutdownError).not.toBeNull();
1091
+ expect(shutdownError!.timedOut).toBe(true);
1092
+ expect(shutdownError!.runningJobCount).toBe(1);
1093
+ expect(shutdownError!.name).toBe("SchedulerShutdownError");
1094
+ expect(mockLogger.errors.some((m) => m.includes("timed out"))).toBe(true);
1095
+
1096
+ // Clean up - release the job so the test can complete cleanly
1097
+ resolveJob!();
1098
+ await startPromise;
1099
+ });
1100
+
1101
+ it("stops new triggers immediately when stop is called", async () => {
1102
+ let triggerCount = 0;
1103
+
1104
+ const scheduler = new Scheduler({
1105
+ stateDir: tempDir,
1106
+ checkInterval: 20,
1107
+ logger: mockLogger,
1108
+ onTrigger: async () => {
1109
+ triggerCount++;
1110
+ await wait(200); // Long-running job
1111
+ },
1112
+ });
1113
+
1114
+ const agent = createTestAgent("test-agent", {
1115
+ hourly: { type: "interval", interval: "1s" },
1116
+ });
1117
+
1118
+ const startPromise = scheduler.start([agent]);
1119
+ await wait(50);
1120
+
1121
+ // First trigger should start
1122
+ const countAtStop = triggerCount;
1123
+ expect(countAtStop).toBe(1);
1124
+
1125
+ // Stop scheduler (don't wait for jobs to test that no new triggers happen)
1126
+ await scheduler.stop({ waitForJobs: false });
1127
+ await startPromise;
1128
+
1129
+ // Wait a bit to see if any new triggers would have happened
1130
+ await wait(100);
1131
+
1132
+ // No additional triggers should have started after stop
1133
+ expect(triggerCount).toBe(countAtStop);
1134
+ });
1135
+
1136
+ it("handles multiple concurrent running jobs during shutdown", async () => {
1137
+ let runningCount = 0;
1138
+ let maxRunningDuringShutdown = 0;
1139
+ let resolveAll: () => void;
1140
+ const allJobsCanComplete = new Promise<void>((resolve) => {
1141
+ resolveAll = resolve;
1142
+ });
1143
+
1144
+ const scheduler = new Scheduler({
1145
+ stateDir: tempDir,
1146
+ checkInterval: 20,
1147
+ logger: mockLogger,
1148
+ onTrigger: async () => {
1149
+ runningCount++;
1150
+ maxRunningDuringShutdown = Math.max(maxRunningDuringShutdown, runningCount);
1151
+ await allJobsCanComplete;
1152
+ runningCount--;
1153
+ },
1154
+ });
1155
+
1156
+ // Agent with multiple concurrent schedules
1157
+ const agent = {
1158
+ ...createTestAgent("test-agent", {
1159
+ schedule1: { type: "interval", interval: "1s" },
1160
+ schedule2: { type: "interval", interval: "1s" },
1161
+ schedule3: { type: "interval", interval: "1s" },
1162
+ }),
1163
+ instances: { max_concurrent: 3 },
1164
+ } as ResolvedAgent;
1165
+
1166
+ const startPromise = scheduler.start([agent]);
1167
+ await wait(100);
1168
+
1169
+ // Multiple jobs should be running
1170
+ expect(runningCount).toBeGreaterThanOrEqual(1);
1171
+
1172
+ // Start shutdown
1173
+ const stopPromise = scheduler.stop();
1174
+
1175
+ await wait(10);
1176
+ expect(scheduler.getStatus()).toBe("stopping");
1177
+
1178
+ // Release all jobs
1179
+ resolveAll!();
1180
+
1181
+ await stopPromise;
1182
+ await startPromise;
1183
+
1184
+ expect(scheduler.getStatus()).toBe("stopped");
1185
+ expect(runningCount).toBe(0);
1186
+ });
1187
+
1188
+ it("returns immediately when there are no running jobs", async () => {
1189
+ const scheduler = new Scheduler({
1190
+ stateDir: tempDir,
1191
+ checkInterval: 100,
1192
+ logger: mockLogger,
1193
+ });
1194
+
1195
+ // Start with no agents (no jobs will be triggered)
1196
+ const startPromise = scheduler.start([]);
1197
+ await wait(50);
1198
+
1199
+ const stopStart = Date.now();
1200
+ await scheduler.stop();
1201
+ const stopDuration = Date.now() - stopStart;
1202
+
1203
+ // Should complete quickly (not wait for timeout)
1204
+ expect(stopDuration).toBeLessThan(100);
1205
+
1206
+ await startPromise;
1207
+ });
1208
+
1209
+ it("updates fleet state on shutdown", async () => {
1210
+ const scheduler = new Scheduler({
1211
+ stateDir: tempDir,
1212
+ checkInterval: 50,
1213
+ logger: mockLogger,
1214
+ onTrigger: async () => {
1215
+ await wait(10);
1216
+ },
1217
+ });
1218
+
1219
+ const agent = createTestAgent("test-agent", {
1220
+ hourly: { type: "interval", interval: "1s" },
1221
+ });
1222
+
1223
+ const startPromise = scheduler.start([agent]);
1224
+ await wait(150);
1225
+
1226
+ // Trigger should have completed and updated state
1227
+ const stateFile = join(tempDir, "state.yaml");
1228
+ let fleetState = await readFleetState(stateFile);
1229
+ const scheduleState = fleetState.agents["test-agent"]?.schedules?.hourly;
1230
+
1231
+ expect(scheduleState?.status).toBe("idle");
1232
+ expect(scheduleState?.last_run_at).not.toBeNull();
1233
+
1234
+ await scheduler.stop();
1235
+ await startPromise;
1236
+
1237
+ // State should still be valid after shutdown
1238
+ fleetState = await readFleetState(stateFile);
1239
+ expect(fleetState.agents["test-agent"]?.schedules?.hourly?.status).toBe("idle");
1240
+ });
1241
+
1242
+ it("getTotalRunningJobCount returns correct count", async () => {
1243
+ let resolveJob: () => void;
1244
+ const jobPromise = new Promise<void>((resolve) => {
1245
+ resolveJob = resolve;
1246
+ });
1247
+
1248
+ const scheduler = new Scheduler({
1249
+ stateDir: tempDir,
1250
+ checkInterval: 30,
1251
+ logger: mockLogger,
1252
+ onTrigger: async () => {
1253
+ await jobPromise;
1254
+ },
1255
+ });
1256
+
1257
+ // Initially should be 0
1258
+ expect(scheduler.getTotalRunningJobCount()).toBe(0);
1259
+
1260
+ const agent = createTestAgent("test-agent", {
1261
+ hourly: { type: "interval", interval: "1s" },
1262
+ });
1263
+
1264
+ const startPromise = scheduler.start([agent]);
1265
+ await wait(100);
1266
+
1267
+ // Should have 1 running job
1268
+ expect(scheduler.getTotalRunningJobCount()).toBe(1);
1269
+
1270
+ resolveJob!();
1271
+ await wait(50);
1272
+
1273
+ // Should be back to 0
1274
+ expect(scheduler.getTotalRunningJobCount()).toBe(0);
1275
+
1276
+ await scheduler.stop();
1277
+ await startPromise;
1278
+ });
1279
+ });
1280
+ });