@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,2869 @@
1
+ /**
2
+ * Additional tests for FleetManager to improve code coverage
3
+ *
4
+ * Targets specific uncovered code paths:
5
+ * - Error handling in startSchedulerAsync
6
+ * - Error handling in handleScheduleTrigger
7
+ * - Default logger usage
8
+ * - Log streaming methods edge cases
9
+ * - ConcurrencyLimitError paths
10
+ * - Configuration error paths
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
14
+ import { mkdtemp, rm, mkdir, writeFile } from "fs/promises";
15
+ import { tmpdir } from "os";
16
+ import { join } from "path";
17
+ import { FleetManager } from "../fleet-manager.js";
18
+ import {
19
+ ConcurrencyLimitError,
20
+ JobCancelError,
21
+ FleetManagerConfigError,
22
+ FleetManagerStateDirError,
23
+ AgentNotFoundError,
24
+ ScheduleNotFoundError,
25
+ } from "../errors.js";
26
+ import type { FleetManagerLogger } from "../types.js";
27
+
28
+ describe("FleetManager Coverage Tests", () => {
29
+ let tempDir: string;
30
+ let configDir: string;
31
+ let stateDir: string;
32
+
33
+ beforeEach(async () => {
34
+ tempDir = await mkdtemp(join(tmpdir(), "fleet-coverage-test-"));
35
+ configDir = join(tempDir, "config");
36
+ stateDir = join(tempDir, ".herdctl");
37
+ await mkdir(configDir, { recursive: true });
38
+ });
39
+
40
+ afterEach(async () => {
41
+ await rm(tempDir, { recursive: true, force: true });
42
+ });
43
+
44
+ async function createConfig(config: object) {
45
+ const configPath = join(configDir, "herdctl.yaml");
46
+ const yaml = await import("yaml");
47
+ await writeFile(configPath, yaml.stringify(config));
48
+ return configPath;
49
+ }
50
+
51
+ async function createAgentConfig(name: string, config: object) {
52
+ const agentDir = join(configDir, "agents");
53
+ await mkdir(agentDir, { recursive: true });
54
+ const agentPath = join(agentDir, `${name}.yaml`);
55
+ const yaml = await import("yaml");
56
+ await writeFile(agentPath, yaml.stringify(config));
57
+ return agentPath;
58
+ }
59
+
60
+ function createSilentLogger(): FleetManagerLogger {
61
+ return {
62
+ debug: vi.fn(),
63
+ info: vi.fn(),
64
+ warn: vi.fn(),
65
+ error: vi.fn(),
66
+ };
67
+ }
68
+
69
+ // ===========================================================================
70
+ // Default Logger Tests
71
+ // ===========================================================================
72
+ describe("Default logger", () => {
73
+ it("uses default console logger when none provided", async () => {
74
+ // Mock console methods
75
+ const originalDebug = console.debug;
76
+ const originalInfo = console.info;
77
+ const originalWarn = console.warn;
78
+ const originalError = console.error;
79
+
80
+ const debugSpy = vi.fn();
81
+ const infoSpy = vi.fn();
82
+ const warnSpy = vi.fn();
83
+ const errorSpy = vi.fn();
84
+
85
+ console.debug = debugSpy;
86
+ console.info = infoSpy;
87
+ console.warn = warnSpy;
88
+ console.error = errorSpy;
89
+
90
+ try {
91
+ await createAgentConfig("test-agent", {
92
+ name: "test-agent",
93
+ });
94
+
95
+ const configPath = await createConfig({
96
+ version: 1,
97
+ agents: [{ path: "./agents/test-agent.yaml" }],
98
+ });
99
+
100
+ // Create manager without logger - uses default
101
+ const manager = new FleetManager({
102
+ configPath,
103
+ stateDir,
104
+ });
105
+
106
+ await manager.initialize();
107
+
108
+ // Default logger should have logged to console.info
109
+ expect(infoSpy).toHaveBeenCalled();
110
+ expect(infoSpy.mock.calls.some((call) =>
111
+ call[0].includes("[fleet-manager]")
112
+ )).toBe(true);
113
+ } finally {
114
+ // Restore console methods
115
+ console.debug = originalDebug;
116
+ console.info = originalInfo;
117
+ console.warn = originalWarn;
118
+ console.error = originalError;
119
+ }
120
+ });
121
+
122
+ it("default logger debug method works", async () => {
123
+ const originalDebug = console.debug;
124
+ const debugSpy = vi.fn();
125
+ console.debug = debugSpy;
126
+
127
+ try {
128
+ await createAgentConfig("debug-agent", {
129
+ name: "debug-agent",
130
+ });
131
+
132
+ const configPath = await createConfig({
133
+ version: 1,
134
+ agents: [{ path: "./agents/debug-agent.yaml" }],
135
+ });
136
+
137
+ const manager = new FleetManager({
138
+ configPath,
139
+ stateDir,
140
+ });
141
+
142
+ await manager.initialize();
143
+
144
+ // Debug should have been called with loading config message
145
+ expect(debugSpy).toHaveBeenCalled();
146
+ } finally {
147
+ console.debug = originalDebug;
148
+ }
149
+ });
150
+ });
151
+
152
+ // ===========================================================================
153
+ // ConcurrencyLimitError Tests
154
+ // ===========================================================================
155
+ describe("ConcurrencyLimitError in trigger", () => {
156
+ it("ConcurrencyLimitError has correct properties", async () => {
157
+ // Test the error class directly
158
+ const error = new ConcurrencyLimitError("limited-agent", 1, 1);
159
+ expect(error.name).toBe("ConcurrencyLimitError");
160
+ expect(error.agentName).toBe("limited-agent");
161
+ expect(error.currentJobs).toBe(1);
162
+ expect(error.limit).toBe(1);
163
+ expect(error.isAtLimit()).toBe(true);
164
+ expect(error.message).toContain("limited-agent");
165
+ expect(error.message).toContain("concurrency limit");
166
+ });
167
+ });
168
+
169
+ // ===========================================================================
170
+ // Configuration Error Handling
171
+ // ===========================================================================
172
+ describe("Configuration error handling", () => {
173
+ it("wraps ConfigNotFoundError in FleetManagerConfigError", async () => {
174
+ const manager = new FleetManager({
175
+ configPath: "/nonexistent/path/config.yaml",
176
+ stateDir,
177
+ logger: createSilentLogger(),
178
+ });
179
+
180
+ await expect(manager.initialize()).rejects.toThrow(FleetManagerConfigError);
181
+ });
182
+
183
+ it("wraps ConfigError in FleetManagerConfigError", async () => {
184
+ const configPath = join(configDir, "herdctl.yaml");
185
+ // Invalid YAML
186
+ await writeFile(configPath, "invalid: yaml: content: [:");
187
+
188
+ const manager = new FleetManager({
189
+ configPath,
190
+ stateDir,
191
+ logger: createSilentLogger(),
192
+ });
193
+
194
+ await expect(manager.initialize()).rejects.toThrow(FleetManagerConfigError);
195
+ });
196
+
197
+ it("wraps unknown errors in FleetManagerConfigError", async () => {
198
+ // Create a config that will cause an unexpected error
199
+ const configPath = join(configDir, "herdctl.yaml");
200
+ await writeFile(configPath, "version: 1\nagents: 'not-an-array'");
201
+
202
+ const manager = new FleetManager({
203
+ configPath,
204
+ stateDir,
205
+ logger: createSilentLogger(),
206
+ });
207
+
208
+ await expect(manager.initialize()).rejects.toThrow(FleetManagerConfigError);
209
+ });
210
+ });
211
+
212
+ // ===========================================================================
213
+ // Log Streaming Tests
214
+ // ===========================================================================
215
+ describe("Log streaming edge cases", () => {
216
+ it("streamLogs returns async iterable", async () => {
217
+ await createAgentConfig("stream-agent", {
218
+ name: "stream-agent",
219
+ });
220
+
221
+ const configPath = await createConfig({
222
+ version: 1,
223
+ agents: [{ path: "./agents/stream-agent.yaml" }],
224
+ });
225
+
226
+ const manager = new FleetManager({
227
+ configPath,
228
+ stateDir,
229
+ logger: createSilentLogger(),
230
+ });
231
+
232
+ await manager.initialize();
233
+
234
+ const stream = manager.streamLogs({ includeHistory: false });
235
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
236
+ });
237
+
238
+ it("streamLogs with level filter", async () => {
239
+ await createAgentConfig("level-filter-agent", {
240
+ name: "level-filter-agent",
241
+ });
242
+
243
+ const configPath = await createConfig({
244
+ version: 1,
245
+ agents: [{ path: "./agents/level-filter-agent.yaml" }],
246
+ });
247
+
248
+ const manager = new FleetManager({
249
+ configPath,
250
+ stateDir,
251
+ logger: createSilentLogger(),
252
+ });
253
+
254
+ await manager.initialize();
255
+
256
+ // Test different log levels
257
+ const errorStream = manager.streamLogs({ level: "error", includeHistory: false });
258
+ expect(errorStream[Symbol.asyncIterator]).toBeDefined();
259
+
260
+ const warnStream = manager.streamLogs({ level: "warn", includeHistory: false });
261
+ expect(warnStream[Symbol.asyncIterator]).toBeDefined();
262
+
263
+ const debugStream = manager.streamLogs({ level: "debug", includeHistory: false });
264
+ expect(debugStream[Symbol.asyncIterator]).toBeDefined();
265
+ });
266
+
267
+ it("streamLogs with agent filter", async () => {
268
+ await createAgentConfig("filter-agent", {
269
+ name: "filter-agent",
270
+ });
271
+
272
+ const configPath = await createConfig({
273
+ version: 1,
274
+ agents: [{ path: "./agents/filter-agent.yaml" }],
275
+ });
276
+
277
+ const manager = new FleetManager({
278
+ configPath,
279
+ stateDir,
280
+ logger: createSilentLogger(),
281
+ });
282
+
283
+ await manager.initialize();
284
+
285
+ const stream = manager.streamLogs({
286
+ agentName: "filter-agent",
287
+ includeHistory: false,
288
+ });
289
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
290
+ });
291
+
292
+ it("streamLogs with job filter", async () => {
293
+ await createAgentConfig("job-filter-agent", {
294
+ name: "job-filter-agent",
295
+ });
296
+
297
+ const configPath = await createConfig({
298
+ version: 1,
299
+ agents: [{ path: "./agents/job-filter-agent.yaml" }],
300
+ });
301
+
302
+ const manager = new FleetManager({
303
+ configPath,
304
+ stateDir,
305
+ logger: createSilentLogger(),
306
+ });
307
+
308
+ await manager.initialize();
309
+
310
+ const stream = manager.streamLogs({
311
+ jobId: "job-2024-01-15-abc123",
312
+ includeHistory: false,
313
+ });
314
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
315
+ });
316
+
317
+ it("streamLogs with history limit", async () => {
318
+ await createAgentConfig("history-limit-agent", {
319
+ name: "history-limit-agent",
320
+ });
321
+
322
+ const configPath = await createConfig({
323
+ version: 1,
324
+ agents: [{ path: "./agents/history-limit-agent.yaml" }],
325
+ });
326
+
327
+ const manager = new FleetManager({
328
+ configPath,
329
+ stateDir,
330
+ logger: createSilentLogger(),
331
+ });
332
+
333
+ await manager.initialize();
334
+
335
+ // Create some jobs first
336
+ await manager.trigger("history-limit-agent");
337
+ await manager.trigger("history-limit-agent");
338
+
339
+ const stream = manager.streamLogs({
340
+ includeHistory: true,
341
+ historyLimit: 5,
342
+ });
343
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
344
+ });
345
+
346
+ it("streamJobOutput returns async iterable", async () => {
347
+ await createAgentConfig("job-output-agent", {
348
+ name: "job-output-agent",
349
+ });
350
+
351
+ const configPath = await createConfig({
352
+ version: 1,
353
+ agents: [{ path: "./agents/job-output-agent.yaml" }],
354
+ });
355
+
356
+ const manager = new FleetManager({
357
+ configPath,
358
+ stateDir,
359
+ logger: createSilentLogger(),
360
+ });
361
+
362
+ await manager.initialize();
363
+
364
+ // Trigger a job
365
+ const result = await manager.trigger("job-output-agent");
366
+
367
+ const stream = manager.streamJobOutput(result.jobId);
368
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
369
+ });
370
+
371
+ it("streamAgentLogs returns async iterable", async () => {
372
+ await createAgentConfig("agent-logs-agent", {
373
+ name: "agent-logs-agent",
374
+ });
375
+
376
+ const configPath = await createConfig({
377
+ version: 1,
378
+ agents: [{ path: "./agents/agent-logs-agent.yaml" }],
379
+ });
380
+
381
+ const manager = new FleetManager({
382
+ configPath,
383
+ stateDir,
384
+ logger: createSilentLogger(),
385
+ });
386
+
387
+ await manager.initialize();
388
+
389
+ const stream = manager.streamAgentLogs("agent-logs-agent");
390
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
391
+ });
392
+ });
393
+
394
+ // ===========================================================================
395
+ // handleScheduleTrigger Error Handling
396
+ // ===========================================================================
397
+ describe("Schedule trigger error handling", () => {
398
+ it("emits schedule:trigger and schedule:complete events", async () => {
399
+ await createAgentConfig("trigger-event-agent", {
400
+ name: "trigger-event-agent",
401
+ schedules: {
402
+ test: {
403
+ type: "interval",
404
+ interval: "100ms",
405
+ prompt: "Test prompt",
406
+ },
407
+ },
408
+ });
409
+
410
+ const configPath = await createConfig({
411
+ version: 1,
412
+ agents: [{ path: "./agents/trigger-event-agent.yaml" }],
413
+ });
414
+
415
+ const manager = new FleetManager({
416
+ configPath,
417
+ stateDir,
418
+ checkInterval: 50,
419
+ logger: createSilentLogger(),
420
+ });
421
+
422
+ const triggerHandler = vi.fn();
423
+ const triggeredHandler = vi.fn();
424
+ const completeHandler = vi.fn();
425
+
426
+ manager.on("schedule:trigger", triggerHandler);
427
+ manager.on("schedule:triggered", triggeredHandler);
428
+ manager.on("schedule:complete", completeHandler);
429
+
430
+ await manager.initialize();
431
+ await manager.start();
432
+
433
+ // Wait for schedule to trigger
434
+ await new Promise((resolve) => setTimeout(resolve, 200));
435
+
436
+ await manager.stop();
437
+
438
+ // Both legacy and new events should be emitted
439
+ expect(triggerHandler).toHaveBeenCalled();
440
+ expect(triggeredHandler).toHaveBeenCalled();
441
+ expect(completeHandler).toHaveBeenCalled();
442
+ });
443
+ });
444
+
445
+ // ===========================================================================
446
+ // Scheduler Error Handling (startSchedulerAsync)
447
+ // ===========================================================================
448
+ describe("startSchedulerAsync error handling", () => {
449
+ it("handles scheduler errors and sets error state", async () => {
450
+ await createAgentConfig("error-agent", {
451
+ name: "error-agent",
452
+ schedules: {
453
+ test: {
454
+ type: "interval",
455
+ interval: "100ms",
456
+ },
457
+ },
458
+ });
459
+
460
+ const configPath = await createConfig({
461
+ version: 1,
462
+ agents: [{ path: "./agents/error-agent.yaml" }],
463
+ });
464
+
465
+ const logger = createSilentLogger();
466
+ const manager = new FleetManager({
467
+ configPath,
468
+ stateDir,
469
+ checkInterval: 50,
470
+ logger,
471
+ });
472
+
473
+ const errorHandler = vi.fn();
474
+ manager.on("error", errorHandler);
475
+
476
+ await manager.initialize();
477
+ await manager.start();
478
+
479
+ // Let it run briefly
480
+ await new Promise((resolve) => setTimeout(resolve, 100));
481
+
482
+ await manager.stop();
483
+
484
+ // The manager should still be in stopped state
485
+ expect(manager.state.status).toBe("stopped");
486
+ });
487
+ });
488
+
489
+ // ===========================================================================
490
+ // Config Change Detection Edge Cases
491
+ // ===========================================================================
492
+ describe("Config change detection edge cases", () => {
493
+ it("detects workspace changes between string and object forms", async () => {
494
+ await createAgentConfig("workspace-agent", {
495
+ name: "workspace-agent",
496
+ workspace: "/simple/path",
497
+ });
498
+
499
+ const configPath = await createConfig({
500
+ version: 1,
501
+ agents: [{ path: "./agents/workspace-agent.yaml" }],
502
+ });
503
+
504
+ const manager = new FleetManager({
505
+ configPath,
506
+ stateDir,
507
+ logger: createSilentLogger(),
508
+ });
509
+
510
+ await manager.initialize();
511
+
512
+ // Modify to object workspace
513
+ await createAgentConfig("workspace-agent", {
514
+ name: "workspace-agent",
515
+ workspace: {
516
+ root: "/object/path",
517
+ },
518
+ });
519
+
520
+ const result = await manager.reload();
521
+
522
+ expect(result.changes).toContainEqual(
523
+ expect.objectContaining({
524
+ type: "modified",
525
+ category: "agent",
526
+ name: "workspace-agent",
527
+ })
528
+ );
529
+ });
530
+
531
+ it("detects max_turns changes", async () => {
532
+ await createAgentConfig("turns-agent", {
533
+ name: "turns-agent",
534
+ max_turns: 10,
535
+ });
536
+
537
+ const configPath = await createConfig({
538
+ version: 1,
539
+ agents: [{ path: "./agents/turns-agent.yaml" }],
540
+ });
541
+
542
+ const manager = new FleetManager({
543
+ configPath,
544
+ stateDir,
545
+ logger: createSilentLogger(),
546
+ });
547
+
548
+ await manager.initialize();
549
+
550
+ // Modify max_turns
551
+ await createAgentConfig("turns-agent", {
552
+ name: "turns-agent",
553
+ max_turns: 20,
554
+ });
555
+
556
+ const result = await manager.reload();
557
+
558
+ expect(result.changes).toContainEqual(
559
+ expect.objectContaining({
560
+ type: "modified",
561
+ category: "agent",
562
+ name: "turns-agent",
563
+ details: expect.stringContaining("max_turns"),
564
+ })
565
+ );
566
+ });
567
+
568
+ it("detects system_prompt changes", async () => {
569
+ await createAgentConfig("prompt-agent", {
570
+ name: "prompt-agent",
571
+ system_prompt: "Original system prompt",
572
+ });
573
+
574
+ const configPath = await createConfig({
575
+ version: 1,
576
+ agents: [{ path: "./agents/prompt-agent.yaml" }],
577
+ });
578
+
579
+ const manager = new FleetManager({
580
+ configPath,
581
+ stateDir,
582
+ logger: createSilentLogger(),
583
+ });
584
+
585
+ await manager.initialize();
586
+
587
+ // Modify system_prompt
588
+ await createAgentConfig("prompt-agent", {
589
+ name: "prompt-agent",
590
+ system_prompt: "Updated system prompt",
591
+ });
592
+
593
+ const result = await manager.reload();
594
+
595
+ expect(result.changes).toContainEqual(
596
+ expect.objectContaining({
597
+ type: "modified",
598
+ category: "agent",
599
+ name: "prompt-agent",
600
+ details: expect.stringContaining("system_prompt"),
601
+ })
602
+ );
603
+ });
604
+
605
+ it("detects max_concurrent changes", async () => {
606
+ await createAgentConfig("concurrent-agent", {
607
+ name: "concurrent-agent",
608
+ instances: { max_concurrent: 2 },
609
+ });
610
+
611
+ const configPath = await createConfig({
612
+ version: 1,
613
+ agents: [{ path: "./agents/concurrent-agent.yaml" }],
614
+ });
615
+
616
+ const manager = new FleetManager({
617
+ configPath,
618
+ stateDir,
619
+ logger: createSilentLogger(),
620
+ });
621
+
622
+ await manager.initialize();
623
+
624
+ // Modify max_concurrent
625
+ await createAgentConfig("concurrent-agent", {
626
+ name: "concurrent-agent",
627
+ instances: { max_concurrent: 5 },
628
+ });
629
+
630
+ const result = await manager.reload();
631
+
632
+ expect(result.changes).toContainEqual(
633
+ expect.objectContaining({
634
+ type: "modified",
635
+ category: "agent",
636
+ name: "concurrent-agent",
637
+ details: expect.stringContaining("max_concurrent"),
638
+ })
639
+ );
640
+ });
641
+
642
+ it("detects schedule type changes", async () => {
643
+ await createAgentConfig("type-change-agent", {
644
+ name: "type-change-agent",
645
+ schedules: {
646
+ check: {
647
+ type: "interval",
648
+ interval: "1h",
649
+ },
650
+ },
651
+ });
652
+
653
+ const configPath = await createConfig({
654
+ version: 1,
655
+ agents: [{ path: "./agents/type-change-agent.yaml" }],
656
+ });
657
+
658
+ const manager = new FleetManager({
659
+ configPath,
660
+ stateDir,
661
+ logger: createSilentLogger(),
662
+ });
663
+
664
+ await manager.initialize();
665
+
666
+ // Change to cron type
667
+ await createAgentConfig("type-change-agent", {
668
+ name: "type-change-agent",
669
+ schedules: {
670
+ check: {
671
+ type: "cron",
672
+ expression: "0 * * * *",
673
+ },
674
+ },
675
+ });
676
+
677
+ const result = await manager.reload();
678
+
679
+ expect(result.changes).toContainEqual(
680
+ expect.objectContaining({
681
+ type: "modified",
682
+ category: "schedule",
683
+ name: "type-change-agent/check",
684
+ })
685
+ );
686
+ });
687
+
688
+ it("detects schedule expression changes", async () => {
689
+ await createAgentConfig("expr-agent", {
690
+ name: "expr-agent",
691
+ schedules: {
692
+ check: {
693
+ type: "cron",
694
+ expression: "0 * * * *",
695
+ },
696
+ },
697
+ });
698
+
699
+ const configPath = await createConfig({
700
+ version: 1,
701
+ agents: [{ path: "./agents/expr-agent.yaml" }],
702
+ });
703
+
704
+ const manager = new FleetManager({
705
+ configPath,
706
+ stateDir,
707
+ logger: createSilentLogger(),
708
+ });
709
+
710
+ await manager.initialize();
711
+
712
+ // Change expression
713
+ await createAgentConfig("expr-agent", {
714
+ name: "expr-agent",
715
+ schedules: {
716
+ check: {
717
+ type: "cron",
718
+ expression: "30 * * * *",
719
+ },
720
+ },
721
+ });
722
+
723
+ const result = await manager.reload();
724
+
725
+ expect(result.changes).toContainEqual(
726
+ expect.objectContaining({
727
+ type: "modified",
728
+ category: "schedule",
729
+ name: "expr-agent/check",
730
+ details: expect.stringContaining("expression"),
731
+ })
732
+ );
733
+ });
734
+
735
+ it("handles added agent with schedules", async () => {
736
+ await createAgentConfig("original-agent", {
737
+ name: "original-agent",
738
+ });
739
+
740
+ const configPath = await createConfig({
741
+ version: 1,
742
+ agents: [{ path: "./agents/original-agent.yaml" }],
743
+ });
744
+
745
+ const manager = new FleetManager({
746
+ configPath,
747
+ stateDir,
748
+ logger: createSilentLogger(),
749
+ });
750
+
751
+ await manager.initialize();
752
+
753
+ // Add new agent with schedules
754
+ await createAgentConfig("new-agent-with-schedules", {
755
+ name: "new-agent-with-schedules",
756
+ schedules: {
757
+ hourly: {
758
+ type: "interval",
759
+ interval: "1h",
760
+ },
761
+ daily: {
762
+ type: "interval",
763
+ interval: "24h",
764
+ },
765
+ },
766
+ });
767
+
768
+ await createConfig({
769
+ version: 1,
770
+ agents: [
771
+ { path: "./agents/original-agent.yaml" },
772
+ { path: "./agents/new-agent-with-schedules.yaml" },
773
+ ],
774
+ });
775
+
776
+ const result = await manager.reload();
777
+
778
+ // Should have agent added
779
+ expect(result.changes).toContainEqual(
780
+ expect.objectContaining({
781
+ type: "added",
782
+ category: "agent",
783
+ name: "new-agent-with-schedules",
784
+ })
785
+ );
786
+
787
+ // Should have both schedules added
788
+ expect(result.changes).toContainEqual(
789
+ expect.objectContaining({
790
+ type: "added",
791
+ category: "schedule",
792
+ name: "new-agent-with-schedules/hourly",
793
+ })
794
+ );
795
+
796
+ expect(result.changes).toContainEqual(
797
+ expect.objectContaining({
798
+ type: "added",
799
+ category: "schedule",
800
+ name: "new-agent-with-schedules/daily",
801
+ })
802
+ );
803
+ });
804
+
805
+ it("handles removed agent with schedules", async () => {
806
+ await createAgentConfig("keep-agent", {
807
+ name: "keep-agent",
808
+ });
809
+
810
+ await createAgentConfig("remove-agent", {
811
+ name: "remove-agent",
812
+ schedules: {
813
+ hourly: {
814
+ type: "interval",
815
+ interval: "1h",
816
+ },
817
+ },
818
+ });
819
+
820
+ const configPath = await createConfig({
821
+ version: 1,
822
+ agents: [
823
+ { path: "./agents/keep-agent.yaml" },
824
+ { path: "./agents/remove-agent.yaml" },
825
+ ],
826
+ });
827
+
828
+ const manager = new FleetManager({
829
+ configPath,
830
+ stateDir,
831
+ logger: createSilentLogger(),
832
+ });
833
+
834
+ await manager.initialize();
835
+
836
+ // Remove the agent
837
+ await createConfig({
838
+ version: 1,
839
+ agents: [{ path: "./agents/keep-agent.yaml" }],
840
+ });
841
+
842
+ const result = await manager.reload();
843
+
844
+ // Should have agent removed
845
+ expect(result.changes).toContainEqual(
846
+ expect.objectContaining({
847
+ type: "removed",
848
+ category: "agent",
849
+ name: "remove-agent",
850
+ })
851
+ );
852
+
853
+ // Should have schedule removed
854
+ expect(result.changes).toContainEqual(
855
+ expect.objectContaining({
856
+ type: "removed",
857
+ category: "schedule",
858
+ name: "remove-agent/hourly",
859
+ })
860
+ );
861
+ });
862
+ });
863
+
864
+ // ===========================================================================
865
+ // Stop Options Tests
866
+ // ===========================================================================
867
+ describe("Stop options", () => {
868
+ it("stop with waitForJobs=false", async () => {
869
+ await createAgentConfig("no-wait-agent", {
870
+ name: "no-wait-agent",
871
+ });
872
+
873
+ const configPath = await createConfig({
874
+ version: 1,
875
+ agents: [{ path: "./agents/no-wait-agent.yaml" }],
876
+ });
877
+
878
+ const manager = new FleetManager({
879
+ configPath,
880
+ stateDir,
881
+ checkInterval: 10000,
882
+ logger: createSilentLogger(),
883
+ });
884
+
885
+ await manager.initialize();
886
+ await manager.start();
887
+ await new Promise((resolve) => setTimeout(resolve, 50));
888
+
889
+ await manager.stop({ waitForJobs: false });
890
+
891
+ expect(manager.state.status).toBe("stopped");
892
+ });
893
+
894
+ it("stop with cancelOnTimeout cancels jobs on timeout", async () => {
895
+ await createAgentConfig("cancel-timeout-agent", {
896
+ name: "cancel-timeout-agent",
897
+ });
898
+
899
+ const configPath = await createConfig({
900
+ version: 1,
901
+ agents: [{ path: "./agents/cancel-timeout-agent.yaml" }],
902
+ });
903
+
904
+ const logger = createSilentLogger();
905
+ const manager = new FleetManager({
906
+ configPath,
907
+ stateDir,
908
+ checkInterval: 10000,
909
+ logger,
910
+ });
911
+
912
+ await manager.initialize();
913
+ await manager.start();
914
+ await new Promise((resolve) => setTimeout(resolve, 50));
915
+
916
+ // Create a job
917
+ await manager.trigger("cancel-timeout-agent");
918
+
919
+ await manager.stop({
920
+ timeout: 100,
921
+ cancelOnTimeout: true,
922
+ cancelTimeout: 50,
923
+ });
924
+
925
+ expect(manager.state.status).toBe("stopped");
926
+ });
927
+ });
928
+
929
+ // ===========================================================================
930
+ // Fleet Status Edge Cases
931
+ // ===========================================================================
932
+ describe("Fleet status edge cases", () => {
933
+ it("computeFleetCounts handles different agent states", async () => {
934
+ await createAgentConfig("count-agent", {
935
+ name: "count-agent",
936
+ schedules: {
937
+ test: {
938
+ type: "interval",
939
+ interval: "1h",
940
+ },
941
+ },
942
+ });
943
+
944
+ const configPath = await createConfig({
945
+ version: 1,
946
+ agents: [{ path: "./agents/count-agent.yaml" }],
947
+ });
948
+
949
+ const manager = new FleetManager({
950
+ configPath,
951
+ stateDir,
952
+ logger: createSilentLogger(),
953
+ });
954
+
955
+ await manager.initialize();
956
+
957
+ const status = await manager.getFleetStatus();
958
+
959
+ expect(status.counts.totalAgents).toBe(1);
960
+ expect(status.counts.idleAgents).toBe(1);
961
+ expect(status.counts.runningAgents).toBe(0);
962
+ expect(status.counts.errorAgents).toBe(0);
963
+ expect(status.counts.totalSchedules).toBe(1);
964
+ });
965
+
966
+ it("getFleetStatus computes uptime correctly when stopped", async () => {
967
+ await createAgentConfig("uptime-agent", {
968
+ name: "uptime-agent",
969
+ });
970
+
971
+ const configPath = await createConfig({
972
+ version: 1,
973
+ agents: [{ path: "./agents/uptime-agent.yaml" }],
974
+ });
975
+
976
+ const manager = new FleetManager({
977
+ configPath,
978
+ stateDir,
979
+ checkInterval: 10000,
980
+ logger: createSilentLogger(),
981
+ });
982
+
983
+ await manager.initialize();
984
+ await manager.start();
985
+
986
+ // Wait a bit to accumulate uptime
987
+ await new Promise((resolve) => setTimeout(resolve, 100));
988
+
989
+ const runningStatus = await manager.getFleetStatus();
990
+ expect(runningStatus.uptimeSeconds).toBeGreaterThanOrEqual(0);
991
+
992
+ await manager.stop();
993
+
994
+ // Uptime should still be calculated after stop
995
+ const stoppedStatus = await manager.getFleetStatus();
996
+ expect(stoppedStatus.uptimeSeconds).toBeGreaterThanOrEqual(0);
997
+ });
998
+ });
999
+
1000
+ // ===========================================================================
1001
+ // cancelJob Additional Tests
1002
+ // ===========================================================================
1003
+ describe("cancelJob additional tests", () => {
1004
+ it("calculates duration correctly for already stopped jobs", async () => {
1005
+ await createAgentConfig("duration-agent", {
1006
+ name: "duration-agent",
1007
+ });
1008
+
1009
+ const configPath = await createConfig({
1010
+ version: 1,
1011
+ agents: [{ path: "./agents/duration-agent.yaml" }],
1012
+ });
1013
+
1014
+ const manager = new FleetManager({
1015
+ configPath,
1016
+ stateDir,
1017
+ logger: createSilentLogger(),
1018
+ });
1019
+
1020
+ await manager.initialize();
1021
+
1022
+ // Trigger and cancel a job
1023
+ const result = await manager.trigger("duration-agent");
1024
+ await manager.cancelJob(result.jobId);
1025
+
1026
+ // Cancel again (already stopped)
1027
+ const secondCancel = await manager.cancelJob(result.jobId);
1028
+
1029
+ expect(secondCancel.success).toBe(true);
1030
+ expect(secondCancel.terminationType).toBe("already_stopped");
1031
+ });
1032
+ });
1033
+
1034
+ // ===========================================================================
1035
+ // forkJob Additional Tests
1036
+ // ===========================================================================
1037
+ describe("forkJob additional tests", () => {
1038
+ it("forks with schedule modification", async () => {
1039
+ await createAgentConfig("fork-schedule-agent", {
1040
+ name: "fork-schedule-agent",
1041
+ schedules: {
1042
+ hourly: { type: "interval", interval: "1h", prompt: "Hourly check" },
1043
+ daily: { type: "interval", interval: "24h", prompt: "Daily check" },
1044
+ },
1045
+ });
1046
+
1047
+ const configPath = await createConfig({
1048
+ version: 1,
1049
+ agents: [{ path: "./agents/fork-schedule-agent.yaml" }],
1050
+ });
1051
+
1052
+ const manager = new FleetManager({
1053
+ configPath,
1054
+ stateDir,
1055
+ logger: createSilentLogger(),
1056
+ });
1057
+
1058
+ await manager.initialize();
1059
+
1060
+ // Trigger with hourly schedule
1061
+ const original = await manager.trigger("fork-schedule-agent", "hourly");
1062
+
1063
+ // Fork with different schedule
1064
+ const forked = await manager.forkJob(original.jobId, {
1065
+ schedule: "daily",
1066
+ });
1067
+
1068
+ expect(forked.forkedFromJobId).toBe(original.jobId);
1069
+ });
1070
+
1071
+ it("forks preserving original prompt when no modification", async () => {
1072
+ await createAgentConfig("fork-preserve-agent", {
1073
+ name: "fork-preserve-agent",
1074
+ });
1075
+
1076
+ const configPath = await createConfig({
1077
+ version: 1,
1078
+ agents: [{ path: "./agents/fork-preserve-agent.yaml" }],
1079
+ });
1080
+
1081
+ const manager = new FleetManager({
1082
+ configPath,
1083
+ stateDir,
1084
+ logger: createSilentLogger(),
1085
+ });
1086
+
1087
+ await manager.initialize();
1088
+
1089
+ const original = await manager.trigger("fork-preserve-agent", undefined, {
1090
+ prompt: "Original prompt",
1091
+ });
1092
+
1093
+ // Fork without modifications
1094
+ const forked = await manager.forkJob(original.jobId);
1095
+
1096
+ expect(forked.prompt).toBe("Original prompt");
1097
+ });
1098
+ });
1099
+
1100
+ // ===========================================================================
1101
+ // getAgents and getConfig tests
1102
+ // ===========================================================================
1103
+ describe("getAgents and getConfig", () => {
1104
+ it("getAgents returns empty array when not initialized", async () => {
1105
+ const configPath = await createConfig({
1106
+ version: 1,
1107
+ agents: [],
1108
+ });
1109
+
1110
+ const manager = new FleetManager({
1111
+ configPath,
1112
+ stateDir,
1113
+ logger: createSilentLogger(),
1114
+ });
1115
+
1116
+ const agents = manager.getAgents();
1117
+ expect(agents).toEqual([]);
1118
+ });
1119
+
1120
+ it("getConfig returns null when not initialized", async () => {
1121
+ const configPath = await createConfig({
1122
+ version: 1,
1123
+ agents: [],
1124
+ });
1125
+
1126
+ const manager = new FleetManager({
1127
+ configPath,
1128
+ stateDir,
1129
+ logger: createSilentLogger(),
1130
+ });
1131
+
1132
+ const config = manager.getConfig();
1133
+ expect(config).toBeNull();
1134
+ });
1135
+
1136
+ it("getAgents returns agents after initialization", async () => {
1137
+ await createAgentConfig("get-agent", {
1138
+ name: "get-agent",
1139
+ });
1140
+
1141
+ const configPath = await createConfig({
1142
+ version: 1,
1143
+ agents: [{ path: "./agents/get-agent.yaml" }],
1144
+ });
1145
+
1146
+ const manager = new FleetManager({
1147
+ configPath,
1148
+ stateDir,
1149
+ logger: createSilentLogger(),
1150
+ });
1151
+
1152
+ await manager.initialize();
1153
+
1154
+ const agents = manager.getAgents();
1155
+ expect(agents).toHaveLength(1);
1156
+ expect(agents[0].name).toBe("get-agent");
1157
+ });
1158
+ });
1159
+
1160
+ // ===========================================================================
1161
+ // Schedule enable/disable edge cases
1162
+ // ===========================================================================
1163
+ describe("Schedule enable/disable edge cases", () => {
1164
+ it("enableSchedule throws AgentNotFoundError for unknown agent", async () => {
1165
+ await createAgentConfig("enable-agent", {
1166
+ name: "enable-agent",
1167
+ schedules: {
1168
+ test: { type: "interval", interval: "1h" },
1169
+ },
1170
+ });
1171
+
1172
+ const configPath = await createConfig({
1173
+ version: 1,
1174
+ agents: [{ path: "./agents/enable-agent.yaml" }],
1175
+ });
1176
+
1177
+ const manager = new FleetManager({
1178
+ configPath,
1179
+ stateDir,
1180
+ logger: createSilentLogger(),
1181
+ });
1182
+
1183
+ await manager.initialize();
1184
+
1185
+ await expect(
1186
+ manager.enableSchedule("unknown-agent", "test")
1187
+ ).rejects.toThrow(AgentNotFoundError);
1188
+ });
1189
+
1190
+ it("enableSchedule throws ScheduleNotFoundError for unknown schedule", async () => {
1191
+ await createAgentConfig("enable-schedule-agent", {
1192
+ name: "enable-schedule-agent",
1193
+ schedules: {
1194
+ known: { type: "interval", interval: "1h" },
1195
+ },
1196
+ });
1197
+
1198
+ const configPath = await createConfig({
1199
+ version: 1,
1200
+ agents: [{ path: "./agents/enable-schedule-agent.yaml" }],
1201
+ });
1202
+
1203
+ const manager = new FleetManager({
1204
+ configPath,
1205
+ stateDir,
1206
+ logger: createSilentLogger(),
1207
+ });
1208
+
1209
+ await manager.initialize();
1210
+
1211
+ await expect(
1212
+ manager.enableSchedule("enable-schedule-agent", "unknown")
1213
+ ).rejects.toThrow(ScheduleNotFoundError);
1214
+ });
1215
+
1216
+ it("disableSchedule throws AgentNotFoundError for unknown agent", async () => {
1217
+ await createAgentConfig("disable-agent", {
1218
+ name: "disable-agent",
1219
+ schedules: {
1220
+ test: { type: "interval", interval: "1h" },
1221
+ },
1222
+ });
1223
+
1224
+ const configPath = await createConfig({
1225
+ version: 1,
1226
+ agents: [{ path: "./agents/disable-agent.yaml" }],
1227
+ });
1228
+
1229
+ const manager = new FleetManager({
1230
+ configPath,
1231
+ stateDir,
1232
+ logger: createSilentLogger(),
1233
+ });
1234
+
1235
+ await manager.initialize();
1236
+
1237
+ await expect(
1238
+ manager.disableSchedule("unknown-agent", "test")
1239
+ ).rejects.toThrow(AgentNotFoundError);
1240
+ });
1241
+
1242
+ it("disableSchedule throws ScheduleNotFoundError for unknown schedule", async () => {
1243
+ await createAgentConfig("disable-schedule-agent", {
1244
+ name: "disable-schedule-agent",
1245
+ schedules: {
1246
+ known: { type: "interval", interval: "1h" },
1247
+ },
1248
+ });
1249
+
1250
+ const configPath = await createConfig({
1251
+ version: 1,
1252
+ agents: [{ path: "./agents/disable-schedule-agent.yaml" }],
1253
+ });
1254
+
1255
+ const manager = new FleetManager({
1256
+ configPath,
1257
+ stateDir,
1258
+ logger: createSilentLogger(),
1259
+ });
1260
+
1261
+ await manager.initialize();
1262
+
1263
+ await expect(
1264
+ manager.disableSchedule("disable-schedule-agent", "unknown")
1265
+ ).rejects.toThrow(ScheduleNotFoundError);
1266
+ });
1267
+
1268
+ it("enableSchedule for agent without schedules throws ScheduleNotFoundError", async () => {
1269
+ await createAgentConfig("no-schedule-enable", {
1270
+ name: "no-schedule-enable",
1271
+ });
1272
+
1273
+ const configPath = await createConfig({
1274
+ version: 1,
1275
+ agents: [{ path: "./agents/no-schedule-enable.yaml" }],
1276
+ });
1277
+
1278
+ const manager = new FleetManager({
1279
+ configPath,
1280
+ stateDir,
1281
+ logger: createSilentLogger(),
1282
+ });
1283
+
1284
+ await manager.initialize();
1285
+
1286
+ await expect(
1287
+ manager.enableSchedule("no-schedule-enable", "any")
1288
+ ).rejects.toThrow(ScheduleNotFoundError);
1289
+ });
1290
+
1291
+ it("disableSchedule for agent without schedules throws ScheduleNotFoundError", async () => {
1292
+ await createAgentConfig("no-schedule-disable", {
1293
+ name: "no-schedule-disable",
1294
+ });
1295
+
1296
+ const configPath = await createConfig({
1297
+ version: 1,
1298
+ agents: [{ path: "./agents/no-schedule-disable.yaml" }],
1299
+ });
1300
+
1301
+ const manager = new FleetManager({
1302
+ configPath,
1303
+ stateDir,
1304
+ logger: createSilentLogger(),
1305
+ });
1306
+
1307
+ await manager.initialize();
1308
+
1309
+ await expect(
1310
+ manager.disableSchedule("no-schedule-disable", "any")
1311
+ ).rejects.toThrow(ScheduleNotFoundError);
1312
+ });
1313
+ });
1314
+
1315
+ // ===========================================================================
1316
+ // persistShutdownState edge cases
1317
+ // ===========================================================================
1318
+ describe("persistShutdownState", () => {
1319
+ it("handles stop when stateDir not initialized", async () => {
1320
+ const configPath = await createConfig({
1321
+ version: 1,
1322
+ agents: [],
1323
+ });
1324
+
1325
+ const manager = new FleetManager({
1326
+ configPath,
1327
+ stateDir,
1328
+ logger: createSilentLogger(),
1329
+ });
1330
+
1331
+ // Stop without initializing - should be a no-op
1332
+ await manager.stop();
1333
+ expect(manager.state.status).toBe("uninitialized");
1334
+ });
1335
+ });
1336
+
1337
+ // ===========================================================================
1338
+ // cancelRunningJobs edge case
1339
+ // ===========================================================================
1340
+ describe("cancelRunningJobs", () => {
1341
+ it("handles case with no running jobs", async () => {
1342
+ await createAgentConfig("no-running-agent", {
1343
+ name: "no-running-agent",
1344
+ });
1345
+
1346
+ const configPath = await createConfig({
1347
+ version: 1,
1348
+ agents: [{ path: "./agents/no-running-agent.yaml" }],
1349
+ });
1350
+
1351
+ const logger = createSilentLogger();
1352
+ const manager = new FleetManager({
1353
+ configPath,
1354
+ stateDir,
1355
+ checkInterval: 10000,
1356
+ logger,
1357
+ });
1358
+
1359
+ await manager.initialize();
1360
+ await manager.start();
1361
+ await new Promise((resolve) => setTimeout(resolve, 50));
1362
+
1363
+ // Stop with cancelOnTimeout - but no jobs are running
1364
+ await manager.stop({
1365
+ timeout: 100,
1366
+ cancelOnTimeout: true,
1367
+ cancelTimeout: 50,
1368
+ });
1369
+
1370
+ // Should have logged "No running jobs to cancel"
1371
+ expect(logger.debug).toHaveBeenCalled();
1372
+ });
1373
+ });
1374
+
1375
+ // ===========================================================================
1376
+ // Additional trigger tests for coverage
1377
+ // ===========================================================================
1378
+ describe("Trigger edge cases", () => {
1379
+ it("trigger with bypassConcurrencyLimit option", async () => {
1380
+ await createAgentConfig("bypass-agent", {
1381
+ name: "bypass-agent",
1382
+ instances: { max_concurrent: 1 },
1383
+ });
1384
+
1385
+ const configPath = await createConfig({
1386
+ version: 1,
1387
+ agents: [{ path: "./agents/bypass-agent.yaml" }],
1388
+ });
1389
+
1390
+ const manager = new FleetManager({
1391
+ configPath,
1392
+ stateDir,
1393
+ logger: createSilentLogger(),
1394
+ });
1395
+
1396
+ await manager.initialize();
1397
+
1398
+ // Trigger with bypass option - should work even if at capacity
1399
+ const result = await manager.trigger("bypass-agent", undefined, {
1400
+ bypassConcurrencyLimit: true,
1401
+ prompt: "Test prompt",
1402
+ });
1403
+
1404
+ expect(result.agentName).toBe("bypass-agent");
1405
+ expect(result.prompt).toBe("Test prompt");
1406
+ });
1407
+ });
1408
+
1409
+ // ===========================================================================
1410
+ // Additional schedule tests for coverage
1411
+ // ===========================================================================
1412
+ describe("Schedule state file edge cases", () => {
1413
+ it("getSchedules returns empty array when no agents have schedules", async () => {
1414
+ await createAgentConfig("no-schedule-agent", {
1415
+ name: "no-schedule-agent",
1416
+ });
1417
+
1418
+ const configPath = await createConfig({
1419
+ version: 1,
1420
+ agents: [{ path: "./agents/no-schedule-agent.yaml" }],
1421
+ });
1422
+
1423
+ const manager = new FleetManager({
1424
+ configPath,
1425
+ stateDir,
1426
+ logger: createSilentLogger(),
1427
+ });
1428
+
1429
+ await manager.initialize();
1430
+
1431
+ const schedules = await manager.getSchedules();
1432
+ expect(schedules).toEqual([]);
1433
+ });
1434
+ });
1435
+
1436
+ // ===========================================================================
1437
+ // Additional config change tests for coverage
1438
+ // ===========================================================================
1439
+ describe("Additional config change detection", () => {
1440
+ it("detects model changes", async () => {
1441
+ await createAgentConfig("model-agent", {
1442
+ name: "model-agent",
1443
+ model: "claude-3-sonnet",
1444
+ });
1445
+
1446
+ const configPath = await createConfig({
1447
+ version: 1,
1448
+ agents: [{ path: "./agents/model-agent.yaml" }],
1449
+ });
1450
+
1451
+ const manager = new FleetManager({
1452
+ configPath,
1453
+ stateDir,
1454
+ logger: createSilentLogger(),
1455
+ });
1456
+
1457
+ await manager.initialize();
1458
+
1459
+ // Modify model
1460
+ await createAgentConfig("model-agent", {
1461
+ name: "model-agent",
1462
+ model: "claude-3-opus",
1463
+ });
1464
+
1465
+ const result = await manager.reload();
1466
+
1467
+ expect(result.changes).toContainEqual(
1468
+ expect.objectContaining({
1469
+ type: "modified",
1470
+ category: "agent",
1471
+ name: "model-agent",
1472
+ details: expect.stringContaining("model"),
1473
+ })
1474
+ );
1475
+ });
1476
+
1477
+ it("detects description changes", async () => {
1478
+ await createAgentConfig("desc-agent", {
1479
+ name: "desc-agent",
1480
+ description: "Original description",
1481
+ });
1482
+
1483
+ const configPath = await createConfig({
1484
+ version: 1,
1485
+ agents: [{ path: "./agents/desc-agent.yaml" }],
1486
+ });
1487
+
1488
+ const manager = new FleetManager({
1489
+ configPath,
1490
+ stateDir,
1491
+ logger: createSilentLogger(),
1492
+ });
1493
+
1494
+ await manager.initialize();
1495
+
1496
+ // Modify description
1497
+ await createAgentConfig("desc-agent", {
1498
+ name: "desc-agent",
1499
+ description: "Updated description",
1500
+ });
1501
+
1502
+ const result = await manager.reload();
1503
+
1504
+ expect(result.changes).toContainEqual(
1505
+ expect.objectContaining({
1506
+ type: "modified",
1507
+ category: "agent",
1508
+ name: "desc-agent",
1509
+ details: expect.stringContaining("description"),
1510
+ })
1511
+ );
1512
+ });
1513
+
1514
+ it("detects schedule prompt changes", async () => {
1515
+ await createAgentConfig("prompt-schedule-agent", {
1516
+ name: "prompt-schedule-agent",
1517
+ schedules: {
1518
+ check: {
1519
+ type: "interval",
1520
+ interval: "1h",
1521
+ prompt: "Original prompt",
1522
+ },
1523
+ },
1524
+ });
1525
+
1526
+ const configPath = await createConfig({
1527
+ version: 1,
1528
+ agents: [{ path: "./agents/prompt-schedule-agent.yaml" }],
1529
+ });
1530
+
1531
+ const manager = new FleetManager({
1532
+ configPath,
1533
+ stateDir,
1534
+ logger: createSilentLogger(),
1535
+ });
1536
+
1537
+ await manager.initialize();
1538
+
1539
+ // Modify schedule prompt
1540
+ await createAgentConfig("prompt-schedule-agent", {
1541
+ name: "prompt-schedule-agent",
1542
+ schedules: {
1543
+ check: {
1544
+ type: "interval",
1545
+ interval: "1h",
1546
+ prompt: "Updated prompt",
1547
+ },
1548
+ },
1549
+ });
1550
+
1551
+ const result = await manager.reload();
1552
+
1553
+ expect(result.changes).toContainEqual(
1554
+ expect.objectContaining({
1555
+ type: "modified",
1556
+ category: "schedule",
1557
+ name: "prompt-schedule-agent/check",
1558
+ details: expect.stringContaining("prompt"),
1559
+ })
1560
+ );
1561
+ });
1562
+
1563
+ it("detects schedule interval changes", async () => {
1564
+ await createAgentConfig("interval-schedule-agent", {
1565
+ name: "interval-schedule-agent",
1566
+ schedules: {
1567
+ check: {
1568
+ type: "interval",
1569
+ interval: "1h",
1570
+ },
1571
+ },
1572
+ });
1573
+
1574
+ const configPath = await createConfig({
1575
+ version: 1,
1576
+ agents: [{ path: "./agents/interval-schedule-agent.yaml" }],
1577
+ });
1578
+
1579
+ const manager = new FleetManager({
1580
+ configPath,
1581
+ stateDir,
1582
+ logger: createSilentLogger(),
1583
+ });
1584
+
1585
+ await manager.initialize();
1586
+
1587
+ // Modify interval
1588
+ await createAgentConfig("interval-schedule-agent", {
1589
+ name: "interval-schedule-agent",
1590
+ schedules: {
1591
+ check: {
1592
+ type: "interval",
1593
+ interval: "2h",
1594
+ },
1595
+ },
1596
+ });
1597
+
1598
+ const result = await manager.reload();
1599
+
1600
+ expect(result.changes).toContainEqual(
1601
+ expect.objectContaining({
1602
+ type: "modified",
1603
+ category: "schedule",
1604
+ name: "interval-schedule-agent/check",
1605
+ details: expect.stringContaining("interval"),
1606
+ })
1607
+ );
1608
+ });
1609
+ });
1610
+
1611
+ // ===========================================================================
1612
+ // Event emission tests
1613
+ // ===========================================================================
1614
+ describe("Event emission tests", () => {
1615
+ it("emits initialized event", async () => {
1616
+ await createAgentConfig("event-init-agent", {
1617
+ name: "event-init-agent",
1618
+ });
1619
+
1620
+ const configPath = await createConfig({
1621
+ version: 1,
1622
+ agents: [{ path: "./agents/event-init-agent.yaml" }],
1623
+ });
1624
+
1625
+ const manager = new FleetManager({
1626
+ configPath,
1627
+ stateDir,
1628
+ logger: createSilentLogger(),
1629
+ });
1630
+
1631
+ const initHandler = vi.fn();
1632
+ manager.on("initialized", initHandler);
1633
+
1634
+ await manager.initialize();
1635
+
1636
+ expect(initHandler).toHaveBeenCalledTimes(1);
1637
+ });
1638
+
1639
+ it("emits error event on initialization failure", async () => {
1640
+ // Create invalid config
1641
+ const configPath = await createConfig({
1642
+ version: 1,
1643
+ agents: [{ path: "./agents/nonexistent.yaml" }],
1644
+ });
1645
+
1646
+ const manager = new FleetManager({
1647
+ configPath,
1648
+ stateDir,
1649
+ logger: createSilentLogger(),
1650
+ });
1651
+
1652
+ const errorHandler = vi.fn();
1653
+ manager.on("error", errorHandler);
1654
+
1655
+ try {
1656
+ await manager.initialize();
1657
+ } catch {
1658
+ // Expected
1659
+ }
1660
+
1661
+ expect(errorHandler).toHaveBeenCalledTimes(1);
1662
+ });
1663
+ });
1664
+
1665
+ // ===========================================================================
1666
+ // streamAgentLogs tests
1667
+ // ===========================================================================
1668
+ describe("streamAgentLogs", () => {
1669
+ it("throws AgentNotFoundError for unknown agent", async () => {
1670
+ await createAgentConfig("known-agent", {
1671
+ name: "known-agent",
1672
+ });
1673
+
1674
+ const configPath = await createConfig({
1675
+ version: 1,
1676
+ agents: [{ path: "./agents/known-agent.yaml" }],
1677
+ });
1678
+
1679
+ const manager = new FleetManager({
1680
+ configPath,
1681
+ stateDir,
1682
+ logger: createSilentLogger(),
1683
+ });
1684
+
1685
+ await manager.initialize();
1686
+
1687
+ const stream = manager.streamAgentLogs("unknown-agent");
1688
+ // Get the iterator and call next to trigger the check
1689
+ const iterator = stream[Symbol.asyncIterator]();
1690
+ await expect(iterator.next()).rejects.toThrow(AgentNotFoundError);
1691
+ });
1692
+
1693
+ it("returns async iterable for valid agent", async () => {
1694
+ await createAgentConfig("stream-log-agent", {
1695
+ name: "stream-log-agent",
1696
+ });
1697
+
1698
+ const configPath = await createConfig({
1699
+ version: 1,
1700
+ agents: [{ path: "./agents/stream-log-agent.yaml" }],
1701
+ });
1702
+
1703
+ const manager = new FleetManager({
1704
+ configPath,
1705
+ stateDir,
1706
+ logger: createSilentLogger(),
1707
+ });
1708
+
1709
+ await manager.initialize();
1710
+
1711
+ const stream = manager.streamAgentLogs("stream-log-agent");
1712
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
1713
+ });
1714
+ });
1715
+
1716
+ // ===========================================================================
1717
+ // getAgentInfo tests
1718
+ // ===========================================================================
1719
+ describe("getAgentInfo", () => {
1720
+ it("returns agent info before initialization", async () => {
1721
+ const configPath = await createConfig({
1722
+ version: 1,
1723
+ agents: [],
1724
+ });
1725
+
1726
+ const manager = new FleetManager({
1727
+ configPath,
1728
+ stateDir,
1729
+ logger: createSilentLogger(),
1730
+ });
1731
+
1732
+ // Before initialization, should return empty array
1733
+ const agents = await manager.getAgentInfo();
1734
+ expect(agents).toEqual([]);
1735
+ });
1736
+
1737
+ it("returns agent info with all fields", async () => {
1738
+ await createAgentConfig("full-agent", {
1739
+ name: "full-agent",
1740
+ description: "Full test agent",
1741
+ model: "claude-3",
1742
+ workspace: "/path/to/workspace",
1743
+ instances: { max_concurrent: 3 },
1744
+ schedules: {
1745
+ hourly: { type: "interval", interval: "1h" },
1746
+ },
1747
+ });
1748
+
1749
+ const configPath = await createConfig({
1750
+ version: 1,
1751
+ agents: [{ path: "./agents/full-agent.yaml" }],
1752
+ });
1753
+
1754
+ const manager = new FleetManager({
1755
+ configPath,
1756
+ stateDir,
1757
+ logger: createSilentLogger(),
1758
+ });
1759
+
1760
+ await manager.initialize();
1761
+
1762
+ const agents = await manager.getAgentInfo();
1763
+ expect(agents).toHaveLength(1);
1764
+
1765
+ const agent = agents[0];
1766
+ expect(agent.name).toBe("full-agent");
1767
+ expect(agent.description).toBe("Full test agent");
1768
+ expect(agent.model).toBe("claude-3");
1769
+ expect(agent.workspace).toBe("/path/to/workspace");
1770
+ expect(agent.maxConcurrent).toBe(3);
1771
+ expect(agent.scheduleCount).toBe(1);
1772
+ expect(agent.schedules).toHaveLength(1);
1773
+ expect(agent.schedules[0].name).toBe("hourly");
1774
+ });
1775
+
1776
+ it("returns agent info with workspace object", async () => {
1777
+ await createAgentConfig("workspace-obj-agent", {
1778
+ name: "workspace-obj-agent",
1779
+ workspace: {
1780
+ root: "/object/workspace/path",
1781
+ },
1782
+ });
1783
+
1784
+ const configPath = await createConfig({
1785
+ version: 1,
1786
+ agents: [{ path: "./agents/workspace-obj-agent.yaml" }],
1787
+ });
1788
+
1789
+ const manager = new FleetManager({
1790
+ configPath,
1791
+ stateDir,
1792
+ logger: createSilentLogger(),
1793
+ });
1794
+
1795
+ await manager.initialize();
1796
+
1797
+ const agents = await manager.getAgentInfo();
1798
+ expect(agents[0].workspace).toBe("/object/workspace/path");
1799
+ });
1800
+ });
1801
+
1802
+ // ===========================================================================
1803
+ // getFleetStatus tests
1804
+ // ===========================================================================
1805
+ describe("getFleetStatus", () => {
1806
+ it("returns scheduler status when not initialized", async () => {
1807
+ const configPath = await createConfig({
1808
+ version: 1,
1809
+ agents: [],
1810
+ });
1811
+
1812
+ const manager = new FleetManager({
1813
+ configPath,
1814
+ stateDir,
1815
+ logger: createSilentLogger(),
1816
+ });
1817
+
1818
+ const status = await manager.getFleetStatus();
1819
+ expect(status.scheduler.status).toBe("stopped");
1820
+ });
1821
+ });
1822
+
1823
+ // ===========================================================================
1824
+ // Multiple workspace format tests
1825
+ // ===========================================================================
1826
+ describe("Workspace handling", () => {
1827
+ it("handles agent with no workspace", async () => {
1828
+ await createAgentConfig("no-workspace-agent", {
1829
+ name: "no-workspace-agent",
1830
+ });
1831
+
1832
+ const configPath = await createConfig({
1833
+ version: 1,
1834
+ agents: [{ path: "./agents/no-workspace-agent.yaml" }],
1835
+ });
1836
+
1837
+ const manager = new FleetManager({
1838
+ configPath,
1839
+ stateDir,
1840
+ logger: createSilentLogger(),
1841
+ });
1842
+
1843
+ await manager.initialize();
1844
+
1845
+ const agents = await manager.getAgentInfo();
1846
+ expect(agents[0].workspace).toBeUndefined();
1847
+ });
1848
+ });
1849
+
1850
+ // ===========================================================================
1851
+ // Stop error handling
1852
+ // ===========================================================================
1853
+ describe("Stop error handling", () => {
1854
+ it("handles stop when status is stopping", async () => {
1855
+ await createAgentConfig("stopping-agent", {
1856
+ name: "stopping-agent",
1857
+ });
1858
+
1859
+ const configPath = await createConfig({
1860
+ version: 1,
1861
+ agents: [{ path: "./agents/stopping-agent.yaml" }],
1862
+ });
1863
+
1864
+ const manager = new FleetManager({
1865
+ configPath,
1866
+ stateDir,
1867
+ checkInterval: 10000,
1868
+ logger: createSilentLogger(),
1869
+ });
1870
+
1871
+ await manager.initialize();
1872
+ await manager.start();
1873
+ await new Promise((resolve) => setTimeout(resolve, 50));
1874
+
1875
+ // Call stop which will complete
1876
+ await manager.stop();
1877
+
1878
+ // Second stop should be no-op since status is 'stopped'
1879
+ await manager.stop();
1880
+ expect(manager.state.status).toBe("stopped");
1881
+ });
1882
+ });
1883
+
1884
+ // ===========================================================================
1885
+ // FleetManagerStateError tests
1886
+ // ===========================================================================
1887
+ describe("FleetManagerStateError", () => {
1888
+ it("has requiredState alias for backwards compatibility", async () => {
1889
+ const { FleetManagerStateError } = await import("../errors.js");
1890
+ const error = new FleetManagerStateError("test", "current", "required");
1891
+ expect(error.requiredState).toBe("required");
1892
+ });
1893
+ });
1894
+
1895
+ // ===========================================================================
1896
+ // Event emission helper methods
1897
+ // ===========================================================================
1898
+ describe("Event emission helpers", () => {
1899
+ it("emitConfigReloaded emits config:reloaded event", async () => {
1900
+ await createAgentConfig("emit-config-agent", {
1901
+ name: "emit-config-agent",
1902
+ });
1903
+
1904
+ const configPath = await createConfig({
1905
+ version: 1,
1906
+ agents: [{ path: "./agents/emit-config-agent.yaml" }],
1907
+ });
1908
+
1909
+ const manager = new FleetManager({
1910
+ configPath,
1911
+ stateDir,
1912
+ logger: createSilentLogger(),
1913
+ });
1914
+
1915
+ await manager.initialize();
1916
+
1917
+ const handler = vi.fn();
1918
+ manager.on("config:reloaded", handler);
1919
+
1920
+ manager.emitConfigReloaded({
1921
+ agentCount: 1,
1922
+ agentNames: ["emit-config-agent"],
1923
+ configPath,
1924
+ changes: [],
1925
+ timestamp: new Date().toISOString(),
1926
+ });
1927
+
1928
+ expect(handler).toHaveBeenCalledTimes(1);
1929
+ });
1930
+
1931
+ it("emitAgentStarted emits agent:started event", async () => {
1932
+ await createAgentConfig("emit-start-agent", {
1933
+ name: "emit-start-agent",
1934
+ });
1935
+
1936
+ const configPath = await createConfig({
1937
+ version: 1,
1938
+ agents: [{ path: "./agents/emit-start-agent.yaml" }],
1939
+ });
1940
+
1941
+ const manager = new FleetManager({
1942
+ configPath,
1943
+ stateDir,
1944
+ logger: createSilentLogger(),
1945
+ });
1946
+
1947
+ await manager.initialize();
1948
+
1949
+ const handler = vi.fn();
1950
+ manager.on("agent:started", handler);
1951
+
1952
+ const agent = manager.getAgents().find(a => a.name === "emit-start-agent")!;
1953
+ manager.emitAgentStarted({
1954
+ agent,
1955
+ timestamp: new Date().toISOString(),
1956
+ });
1957
+
1958
+ expect(handler).toHaveBeenCalledTimes(1);
1959
+ });
1960
+
1961
+ it("emitAgentStopped emits agent:stopped event", async () => {
1962
+ await createAgentConfig("emit-stop-agent", {
1963
+ name: "emit-stop-agent",
1964
+ });
1965
+
1966
+ const configPath = await createConfig({
1967
+ version: 1,
1968
+ agents: [{ path: "./agents/emit-stop-agent.yaml" }],
1969
+ });
1970
+
1971
+ const manager = new FleetManager({
1972
+ configPath,
1973
+ stateDir,
1974
+ logger: createSilentLogger(),
1975
+ });
1976
+
1977
+ await manager.initialize();
1978
+
1979
+ const handler = vi.fn();
1980
+ manager.on("agent:stopped", handler);
1981
+
1982
+ manager.emitAgentStopped({
1983
+ agentName: "emit-stop-agent",
1984
+ reason: "test",
1985
+ timestamp: new Date().toISOString(),
1986
+ });
1987
+
1988
+ expect(handler).toHaveBeenCalledTimes(1);
1989
+ });
1990
+
1991
+ it("emitScheduleSkipped emits schedule:skipped event", async () => {
1992
+ await createAgentConfig("emit-skip-agent", {
1993
+ name: "emit-skip-agent",
1994
+ schedules: {
1995
+ test: { type: "interval", interval: "1h" },
1996
+ },
1997
+ });
1998
+
1999
+ const configPath = await createConfig({
2000
+ version: 1,
2001
+ agents: [{ path: "./agents/emit-skip-agent.yaml" }],
2002
+ });
2003
+
2004
+ const manager = new FleetManager({
2005
+ configPath,
2006
+ stateDir,
2007
+ logger: createSilentLogger(),
2008
+ });
2009
+
2010
+ await manager.initialize();
2011
+
2012
+ const handler = vi.fn();
2013
+ manager.on("schedule:skipped", handler);
2014
+
2015
+ manager.emitScheduleSkipped({
2016
+ agentName: "emit-skip-agent",
2017
+ scheduleName: "test",
2018
+ reason: "max_concurrent",
2019
+ timestamp: new Date().toISOString(),
2020
+ });
2021
+
2022
+ expect(handler).toHaveBeenCalledTimes(1);
2023
+ });
2024
+
2025
+ it("emitJobCreated emits job:created event", async () => {
2026
+ await createAgentConfig("emit-job-create-agent", {
2027
+ name: "emit-job-create-agent",
2028
+ });
2029
+
2030
+ const configPath = await createConfig({
2031
+ version: 1,
2032
+ agents: [{ path: "./agents/emit-job-create-agent.yaml" }],
2033
+ });
2034
+
2035
+ const manager = new FleetManager({
2036
+ configPath,
2037
+ stateDir,
2038
+ logger: createSilentLogger(),
2039
+ });
2040
+
2041
+ await manager.initialize();
2042
+
2043
+ const handler = vi.fn();
2044
+ manager.on("job:created", handler);
2045
+
2046
+ manager.emitJobCreated({
2047
+ job: {
2048
+ id: "test-job-id",
2049
+ agent: "emit-job-create-agent",
2050
+ trigger_type: "manual",
2051
+ status: "pending",
2052
+ started_at: new Date().toISOString(),
2053
+ schedule: null,
2054
+ prompt: null,
2055
+ forked_from: null,
2056
+ },
2057
+ agentName: "emit-job-create-agent",
2058
+ timestamp: new Date().toISOString(),
2059
+ });
2060
+
2061
+ expect(handler).toHaveBeenCalledTimes(1);
2062
+ });
2063
+
2064
+ it("emitJobOutput emits job:output event", async () => {
2065
+ await createAgentConfig("emit-output-agent", {
2066
+ name: "emit-output-agent",
2067
+ });
2068
+
2069
+ const configPath = await createConfig({
2070
+ version: 1,
2071
+ agents: [{ path: "./agents/emit-output-agent.yaml" }],
2072
+ });
2073
+
2074
+ const manager = new FleetManager({
2075
+ configPath,
2076
+ stateDir,
2077
+ logger: createSilentLogger(),
2078
+ });
2079
+
2080
+ await manager.initialize();
2081
+
2082
+ const handler = vi.fn();
2083
+ manager.on("job:output", handler);
2084
+
2085
+ manager.emitJobOutput({
2086
+ jobId: "test-job-id",
2087
+ agentName: "emit-output-agent",
2088
+ output: "test output",
2089
+ outputType: "stdout",
2090
+ timestamp: new Date().toISOString(),
2091
+ });
2092
+
2093
+ expect(handler).toHaveBeenCalledTimes(1);
2094
+ });
2095
+
2096
+ it("emitJobCompleted emits job:completed event", async () => {
2097
+ await createAgentConfig("emit-complete-agent", {
2098
+ name: "emit-complete-agent",
2099
+ });
2100
+
2101
+ const configPath = await createConfig({
2102
+ version: 1,
2103
+ agents: [{ path: "./agents/emit-complete-agent.yaml" }],
2104
+ });
2105
+
2106
+ const manager = new FleetManager({
2107
+ configPath,
2108
+ stateDir,
2109
+ logger: createSilentLogger(),
2110
+ });
2111
+
2112
+ await manager.initialize();
2113
+
2114
+ const handler = vi.fn();
2115
+ manager.on("job:completed", handler);
2116
+
2117
+ manager.emitJobCompleted({
2118
+ job: {
2119
+ id: "test-job-id",
2120
+ agent: "emit-complete-agent",
2121
+ trigger_type: "manual",
2122
+ status: "completed",
2123
+ started_at: new Date().toISOString(),
2124
+ schedule: null,
2125
+ prompt: null,
2126
+ forked_from: null,
2127
+ },
2128
+ agentName: "emit-complete-agent",
2129
+ exitReason: "success",
2130
+ durationSeconds: 10,
2131
+ timestamp: new Date().toISOString(),
2132
+ });
2133
+
2134
+ expect(handler).toHaveBeenCalledTimes(1);
2135
+ });
2136
+
2137
+ it("emitJobFailed emits job:failed event", async () => {
2138
+ await createAgentConfig("emit-fail-agent", {
2139
+ name: "emit-fail-agent",
2140
+ });
2141
+
2142
+ const configPath = await createConfig({
2143
+ version: 1,
2144
+ agents: [{ path: "./agents/emit-fail-agent.yaml" }],
2145
+ });
2146
+
2147
+ const manager = new FleetManager({
2148
+ configPath,
2149
+ stateDir,
2150
+ logger: createSilentLogger(),
2151
+ });
2152
+
2153
+ await manager.initialize();
2154
+
2155
+ const handler = vi.fn();
2156
+ manager.on("job:failed", handler);
2157
+
2158
+ manager.emitJobFailed({
2159
+ job: {
2160
+ id: "test-job-id",
2161
+ agent: "emit-fail-agent",
2162
+ trigger_type: "manual",
2163
+ status: "failed",
2164
+ started_at: new Date().toISOString(),
2165
+ schedule: null,
2166
+ prompt: null,
2167
+ forked_from: null,
2168
+ },
2169
+ agentName: "emit-fail-agent",
2170
+ error: new Error("Test error"),
2171
+ exitReason: "error",
2172
+ timestamp: new Date().toISOString(),
2173
+ });
2174
+
2175
+ expect(handler).toHaveBeenCalledTimes(1);
2176
+ });
2177
+
2178
+ it("emitJobCancelled emits job:cancelled event", async () => {
2179
+ await createAgentConfig("emit-cancel-agent", {
2180
+ name: "emit-cancel-agent",
2181
+ });
2182
+
2183
+ const configPath = await createConfig({
2184
+ version: 1,
2185
+ agents: [{ path: "./agents/emit-cancel-agent.yaml" }],
2186
+ });
2187
+
2188
+ const manager = new FleetManager({
2189
+ configPath,
2190
+ stateDir,
2191
+ logger: createSilentLogger(),
2192
+ });
2193
+
2194
+ await manager.initialize();
2195
+
2196
+ const handler = vi.fn();
2197
+ manager.on("job:cancelled", handler);
2198
+
2199
+ manager.emitJobCancelled({
2200
+ job: {
2201
+ id: "test-job-id",
2202
+ agent: "emit-cancel-agent",
2203
+ trigger_type: "manual",
2204
+ status: "cancelled",
2205
+ started_at: new Date().toISOString(),
2206
+ schedule: null,
2207
+ prompt: null,
2208
+ forked_from: null,
2209
+ },
2210
+ agentName: "emit-cancel-agent",
2211
+ terminationType: "graceful",
2212
+ timestamp: new Date().toISOString(),
2213
+ });
2214
+
2215
+ expect(handler).toHaveBeenCalledTimes(1);
2216
+ });
2217
+
2218
+ it("emitJobForked emits job:forked event", async () => {
2219
+ await createAgentConfig("emit-fork-agent", {
2220
+ name: "emit-fork-agent",
2221
+ });
2222
+
2223
+ const configPath = await createConfig({
2224
+ version: 1,
2225
+ agents: [{ path: "./agents/emit-fork-agent.yaml" }],
2226
+ });
2227
+
2228
+ const manager = new FleetManager({
2229
+ configPath,
2230
+ stateDir,
2231
+ logger: createSilentLogger(),
2232
+ });
2233
+
2234
+ await manager.initialize();
2235
+
2236
+ const handler = vi.fn();
2237
+ manager.on("job:forked", handler);
2238
+
2239
+ const originalJob = {
2240
+ id: "original-job-id",
2241
+ agent: "emit-fork-agent",
2242
+ trigger_type: "manual" as const,
2243
+ status: "completed" as const,
2244
+ started_at: new Date().toISOString(),
2245
+ schedule: null,
2246
+ prompt: null,
2247
+ forked_from: null,
2248
+ };
2249
+
2250
+ manager.emitJobForked({
2251
+ job: {
2252
+ id: "forked-job-id",
2253
+ agent: "emit-fork-agent",
2254
+ trigger_type: "fork",
2255
+ status: "pending",
2256
+ started_at: new Date().toISOString(),
2257
+ schedule: null,
2258
+ prompt: null,
2259
+ forked_from: "original-job-id",
2260
+ },
2261
+ originalJob,
2262
+ agentName: "emit-fork-agent",
2263
+ timestamp: new Date().toISOString(),
2264
+ });
2265
+
2266
+ expect(handler).toHaveBeenCalledTimes(1);
2267
+ });
2268
+ });
2269
+
2270
+ // ===========================================================================
2271
+ // getAgentInfoByName tests
2272
+ // ===========================================================================
2273
+ describe("getAgentInfoByName", () => {
2274
+ it("returns info for existing agent", async () => {
2275
+ await createAgentConfig("info-by-name-agent", {
2276
+ name: "info-by-name-agent",
2277
+ description: "Test agent for getAgentInfoByName",
2278
+ });
2279
+
2280
+ const configPath = await createConfig({
2281
+ version: 1,
2282
+ agents: [{ path: "./agents/info-by-name-agent.yaml" }],
2283
+ });
2284
+
2285
+ const manager = new FleetManager({
2286
+ configPath,
2287
+ stateDir,
2288
+ logger: createSilentLogger(),
2289
+ });
2290
+
2291
+ await manager.initialize();
2292
+
2293
+ const info = await manager.getAgentInfoByName("info-by-name-agent");
2294
+ expect(info.name).toBe("info-by-name-agent");
2295
+ expect(info.description).toBe("Test agent for getAgentInfoByName");
2296
+ });
2297
+
2298
+ it("throws AgentNotFoundError for unknown agent", async () => {
2299
+ await createAgentConfig("known-agent-info", {
2300
+ name: "known-agent-info",
2301
+ });
2302
+
2303
+ const configPath = await createConfig({
2304
+ version: 1,
2305
+ agents: [{ path: "./agents/known-agent-info.yaml" }],
2306
+ });
2307
+
2308
+ const manager = new FleetManager({
2309
+ configPath,
2310
+ stateDir,
2311
+ logger: createSilentLogger(),
2312
+ });
2313
+
2314
+ await manager.initialize();
2315
+
2316
+ await expect(
2317
+ manager.getAgentInfoByName("nonexistent-agent")
2318
+ ).rejects.toThrow(AgentNotFoundError);
2319
+ });
2320
+ });
2321
+
2322
+ // ===========================================================================
2323
+ // forkJob error cases
2324
+ // ===========================================================================
2325
+ describe("forkJob error cases", () => {
2326
+ it("throws JobForkError when job not found", async () => {
2327
+ await createAgentConfig("fork-error-agent", {
2328
+ name: "fork-error-agent",
2329
+ });
2330
+
2331
+ const configPath = await createConfig({
2332
+ version: 1,
2333
+ agents: [{ path: "./agents/fork-error-agent.yaml" }],
2334
+ });
2335
+
2336
+ const manager = new FleetManager({
2337
+ configPath,
2338
+ stateDir,
2339
+ logger: createSilentLogger(),
2340
+ });
2341
+
2342
+ await manager.initialize();
2343
+
2344
+ const { JobForkError } = await import("../errors.js");
2345
+ await expect(
2346
+ manager.forkJob("job-2099-01-01-nonexistent")
2347
+ ).rejects.toThrow(JobForkError);
2348
+ });
2349
+
2350
+ it("throws InvalidStateError when not initialized", async () => {
2351
+ const configPath = await createConfig({
2352
+ version: 1,
2353
+ agents: [],
2354
+ });
2355
+
2356
+ const manager = new FleetManager({
2357
+ configPath,
2358
+ stateDir,
2359
+ logger: createSilentLogger(),
2360
+ });
2361
+
2362
+ const { InvalidStateError } = await import("../errors.js");
2363
+ await expect(
2364
+ manager.forkJob("any-job-id")
2365
+ ).rejects.toThrow(InvalidStateError);
2366
+ });
2367
+ });
2368
+
2369
+ // ===========================================================================
2370
+ // cancelJob error cases
2371
+ // ===========================================================================
2372
+ describe("cancelJob error cases", () => {
2373
+ it("throws JobNotFoundError when job not found", async () => {
2374
+ await createAgentConfig("cancel-error-agent", {
2375
+ name: "cancel-error-agent",
2376
+ });
2377
+
2378
+ const configPath = await createConfig({
2379
+ version: 1,
2380
+ agents: [{ path: "./agents/cancel-error-agent.yaml" }],
2381
+ });
2382
+
2383
+ const manager = new FleetManager({
2384
+ configPath,
2385
+ stateDir,
2386
+ logger: createSilentLogger(),
2387
+ });
2388
+
2389
+ await manager.initialize();
2390
+
2391
+ const { JobNotFoundError } = await import("../errors.js");
2392
+ await expect(
2393
+ manager.cancelJob("job-2099-01-01-nonexistent")
2394
+ ).rejects.toThrow(JobNotFoundError);
2395
+ });
2396
+
2397
+ it("throws InvalidStateError when not initialized", async () => {
2398
+ const configPath = await createConfig({
2399
+ version: 1,
2400
+ agents: [],
2401
+ });
2402
+
2403
+ const manager = new FleetManager({
2404
+ configPath,
2405
+ stateDir,
2406
+ logger: createSilentLogger(),
2407
+ });
2408
+
2409
+ const { InvalidStateError } = await import("../errors.js");
2410
+ await expect(
2411
+ manager.cancelJob("any-job-id")
2412
+ ).rejects.toThrow(InvalidStateError);
2413
+ });
2414
+ });
2415
+
2416
+ // ===========================================================================
2417
+ // streamJobOutput tests
2418
+ // ===========================================================================
2419
+ describe("streamJobOutput error cases", () => {
2420
+ it("throws JobNotFoundError for non-existent job", async () => {
2421
+ await createAgentConfig("stream-error-agent", {
2422
+ name: "stream-error-agent",
2423
+ });
2424
+
2425
+ const configPath = await createConfig({
2426
+ version: 1,
2427
+ agents: [{ path: "./agents/stream-error-agent.yaml" }],
2428
+ });
2429
+
2430
+ const manager = new FleetManager({
2431
+ configPath,
2432
+ stateDir,
2433
+ logger: createSilentLogger(),
2434
+ });
2435
+
2436
+ await manager.initialize();
2437
+
2438
+ const stream = manager.streamJobOutput("job-2099-01-01-nonexistent");
2439
+ const iterator = stream[Symbol.asyncIterator]();
2440
+
2441
+ const { JobNotFoundError } = await import("../errors.js");
2442
+ await expect(iterator.next()).rejects.toThrow(JobNotFoundError);
2443
+ });
2444
+ });
2445
+
2446
+ // ===========================================================================
2447
+ // trigger edge cases
2448
+ // ===========================================================================
2449
+ describe("trigger edge cases", () => {
2450
+ it("triggers with schedule that has prompt", async () => {
2451
+ await createAgentConfig("trigger-schedule-prompt", {
2452
+ name: "trigger-schedule-prompt",
2453
+ schedules: {
2454
+ hourly: {
2455
+ type: "interval",
2456
+ interval: "1h",
2457
+ prompt: "Hourly check prompt",
2458
+ },
2459
+ },
2460
+ });
2461
+
2462
+ const configPath = await createConfig({
2463
+ version: 1,
2464
+ agents: [{ path: "./agents/trigger-schedule-prompt.yaml" }],
2465
+ });
2466
+
2467
+ const manager = new FleetManager({
2468
+ configPath,
2469
+ stateDir,
2470
+ logger: createSilentLogger(),
2471
+ });
2472
+
2473
+ await manager.initialize();
2474
+
2475
+ const result = await manager.trigger("trigger-schedule-prompt", "hourly");
2476
+
2477
+ expect(result.prompt).toBe("Hourly check prompt");
2478
+ expect(result.scheduleName).toBe("hourly");
2479
+ });
2480
+
2481
+ it("throws InvalidStateError when triggering before initialization", async () => {
2482
+ const configPath = await createConfig({
2483
+ version: 1,
2484
+ agents: [],
2485
+ });
2486
+
2487
+ const manager = new FleetManager({
2488
+ configPath,
2489
+ stateDir,
2490
+ logger: createSilentLogger(),
2491
+ });
2492
+
2493
+ const { InvalidStateError } = await import("../errors.js");
2494
+ await expect(manager.trigger("any-agent")).rejects.toThrow(InvalidStateError);
2495
+ });
2496
+
2497
+ it("throws AgentNotFoundError for unknown agent", async () => {
2498
+ await createAgentConfig("known-trigger-agent", {
2499
+ name: "known-trigger-agent",
2500
+ });
2501
+
2502
+ const configPath = await createConfig({
2503
+ version: 1,
2504
+ agents: [{ path: "./agents/known-trigger-agent.yaml" }],
2505
+ });
2506
+
2507
+ const manager = new FleetManager({
2508
+ configPath,
2509
+ stateDir,
2510
+ logger: createSilentLogger(),
2511
+ });
2512
+
2513
+ await manager.initialize();
2514
+
2515
+ await expect(manager.trigger("unknown-agent")).rejects.toThrow(AgentNotFoundError);
2516
+ });
2517
+
2518
+ it("throws ScheduleNotFoundError for unknown schedule", async () => {
2519
+ await createAgentConfig("known-schedule-trigger", {
2520
+ name: "known-schedule-trigger",
2521
+ schedules: {
2522
+ known: { type: "interval", interval: "1h" },
2523
+ },
2524
+ });
2525
+
2526
+ const configPath = await createConfig({
2527
+ version: 1,
2528
+ agents: [{ path: "./agents/known-schedule-trigger.yaml" }],
2529
+ });
2530
+
2531
+ const manager = new FleetManager({
2532
+ configPath,
2533
+ stateDir,
2534
+ logger: createSilentLogger(),
2535
+ });
2536
+
2537
+ await manager.initialize();
2538
+
2539
+ await expect(
2540
+ manager.trigger("known-schedule-trigger", "unknown-schedule")
2541
+ ).rejects.toThrow(ScheduleNotFoundError);
2542
+ });
2543
+ });
2544
+
2545
+ // ===========================================================================
2546
+ // reload edge cases
2547
+ // ===========================================================================
2548
+ describe("reload edge cases", () => {
2549
+ it("throws InvalidStateError when not initialized", async () => {
2550
+ const configPath = await createConfig({
2551
+ version: 1,
2552
+ agents: [],
2553
+ });
2554
+
2555
+ const manager = new FleetManager({
2556
+ configPath,
2557
+ stateDir,
2558
+ logger: createSilentLogger(),
2559
+ });
2560
+
2561
+ const { InvalidStateError } = await import("../errors.js");
2562
+ await expect(manager.reload()).rejects.toThrow(InvalidStateError);
2563
+ });
2564
+
2565
+ it("handles removed schedule from existing agent", async () => {
2566
+ await createAgentConfig("schedule-remove-agent", {
2567
+ name: "schedule-remove-agent",
2568
+ schedules: {
2569
+ keep: { type: "interval", interval: "1h" },
2570
+ remove: { type: "interval", interval: "2h" },
2571
+ },
2572
+ });
2573
+
2574
+ const configPath = await createConfig({
2575
+ version: 1,
2576
+ agents: [{ path: "./agents/schedule-remove-agent.yaml" }],
2577
+ });
2578
+
2579
+ const manager = new FleetManager({
2580
+ configPath,
2581
+ stateDir,
2582
+ logger: createSilentLogger(),
2583
+ });
2584
+
2585
+ await manager.initialize();
2586
+
2587
+ // Remove one schedule
2588
+ await createAgentConfig("schedule-remove-agent", {
2589
+ name: "schedule-remove-agent",
2590
+ schedules: {
2591
+ keep: { type: "interval", interval: "1h" },
2592
+ },
2593
+ });
2594
+
2595
+ const result = await manager.reload();
2596
+
2597
+ expect(result.changes).toContainEqual(
2598
+ expect.objectContaining({
2599
+ type: "removed",
2600
+ category: "schedule",
2601
+ name: "schedule-remove-agent/remove",
2602
+ })
2603
+ );
2604
+ });
2605
+
2606
+ it("handles added schedule to existing agent", async () => {
2607
+ await createAgentConfig("schedule-add-agent", {
2608
+ name: "schedule-add-agent",
2609
+ schedules: {
2610
+ original: { type: "interval", interval: "1h" },
2611
+ },
2612
+ });
2613
+
2614
+ const configPath = await createConfig({
2615
+ version: 1,
2616
+ agents: [{ path: "./agents/schedule-add-agent.yaml" }],
2617
+ });
2618
+
2619
+ const manager = new FleetManager({
2620
+ configPath,
2621
+ stateDir,
2622
+ logger: createSilentLogger(),
2623
+ });
2624
+
2625
+ await manager.initialize();
2626
+
2627
+ // Add a schedule
2628
+ await createAgentConfig("schedule-add-agent", {
2629
+ name: "schedule-add-agent",
2630
+ schedules: {
2631
+ original: { type: "interval", interval: "1h" },
2632
+ newschedule: { type: "interval", interval: "2h" },
2633
+ },
2634
+ });
2635
+
2636
+ const result = await manager.reload();
2637
+
2638
+ expect(result.changes).toContainEqual(
2639
+ expect.objectContaining({
2640
+ type: "added",
2641
+ category: "schedule",
2642
+ name: "schedule-add-agent/newschedule",
2643
+ })
2644
+ );
2645
+ });
2646
+ });
2647
+
2648
+ // ===========================================================================
2649
+ // start error cases
2650
+ // ===========================================================================
2651
+ describe("start error cases", () => {
2652
+ it("throws InvalidStateError when not initialized", async () => {
2653
+ const configPath = await createConfig({
2654
+ version: 1,
2655
+ agents: [],
2656
+ });
2657
+
2658
+ const manager = new FleetManager({
2659
+ configPath,
2660
+ stateDir,
2661
+ logger: createSilentLogger(),
2662
+ });
2663
+
2664
+ const { FleetManagerStateError } = await import("../errors.js");
2665
+ await expect(manager.start()).rejects.toThrow(FleetManagerStateError);
2666
+ });
2667
+ });
2668
+
2669
+ // ===========================================================================
2670
+ // getSchedule tests
2671
+ // ===========================================================================
2672
+ describe("getSchedule", () => {
2673
+ it("returns schedule info for valid agent and schedule", async () => {
2674
+ await createAgentConfig("get-schedule-agent", {
2675
+ name: "get-schedule-agent",
2676
+ schedules: {
2677
+ hourly: { type: "interval", interval: "1h" },
2678
+ },
2679
+ });
2680
+
2681
+ const configPath = await createConfig({
2682
+ version: 1,
2683
+ agents: [{ path: "./agents/get-schedule-agent.yaml" }],
2684
+ });
2685
+
2686
+ const manager = new FleetManager({
2687
+ configPath,
2688
+ stateDir,
2689
+ logger: createSilentLogger(),
2690
+ });
2691
+
2692
+ await manager.initialize();
2693
+
2694
+ const schedule = await manager.getSchedule("get-schedule-agent", "hourly");
2695
+ expect(schedule.name).toBe("hourly");
2696
+ expect(schedule.agentName).toBe("get-schedule-agent");
2697
+ expect(schedule.type).toBe("interval");
2698
+ });
2699
+
2700
+ it("throws AgentNotFoundError for unknown agent", async () => {
2701
+ await createAgentConfig("known-get-schedule", {
2702
+ name: "known-get-schedule",
2703
+ schedules: {
2704
+ test: { type: "interval", interval: "1h" },
2705
+ },
2706
+ });
2707
+
2708
+ const configPath = await createConfig({
2709
+ version: 1,
2710
+ agents: [{ path: "./agents/known-get-schedule.yaml" }],
2711
+ });
2712
+
2713
+ const manager = new FleetManager({
2714
+ configPath,
2715
+ stateDir,
2716
+ logger: createSilentLogger(),
2717
+ });
2718
+
2719
+ await manager.initialize();
2720
+
2721
+ await expect(
2722
+ manager.getSchedule("unknown-agent", "test")
2723
+ ).rejects.toThrow(AgentNotFoundError);
2724
+ });
2725
+
2726
+ it("throws ScheduleNotFoundError for unknown schedule", async () => {
2727
+ await createAgentConfig("known-agent-get-schedule", {
2728
+ name: "known-agent-get-schedule",
2729
+ schedules: {
2730
+ known: { type: "interval", interval: "1h" },
2731
+ },
2732
+ });
2733
+
2734
+ const configPath = await createConfig({
2735
+ version: 1,
2736
+ agents: [{ path: "./agents/known-agent-get-schedule.yaml" }],
2737
+ });
2738
+
2739
+ const manager = new FleetManager({
2740
+ configPath,
2741
+ stateDir,
2742
+ logger: createSilentLogger(),
2743
+ });
2744
+
2745
+ await manager.initialize();
2746
+
2747
+ await expect(
2748
+ manager.getSchedule("known-agent-get-schedule", "unknown")
2749
+ ).rejects.toThrow(ScheduleNotFoundError);
2750
+ });
2751
+
2752
+ it("throws ScheduleNotFoundError when agent has no schedules", async () => {
2753
+ await createAgentConfig("no-schedules-agent", {
2754
+ name: "no-schedules-agent",
2755
+ });
2756
+
2757
+ const configPath = await createConfig({
2758
+ version: 1,
2759
+ agents: [{ path: "./agents/no-schedules-agent.yaml" }],
2760
+ });
2761
+
2762
+ const manager = new FleetManager({
2763
+ configPath,
2764
+ stateDir,
2765
+ logger: createSilentLogger(),
2766
+ });
2767
+
2768
+ await manager.initialize();
2769
+
2770
+ await expect(
2771
+ manager.getSchedule("no-schedules-agent", "any")
2772
+ ).rejects.toThrow(ScheduleNotFoundError);
2773
+ });
2774
+ });
2775
+
2776
+ // ===========================================================================
2777
+ // streamJobOutput for completed job
2778
+ // ===========================================================================
2779
+ describe("streamJobOutput for completed jobs", () => {
2780
+ it("streams output for completed job and stops", async () => {
2781
+ await createAgentConfig("completed-stream-agent", {
2782
+ name: "completed-stream-agent",
2783
+ });
2784
+
2785
+ const configPath = await createConfig({
2786
+ version: 1,
2787
+ agents: [{ path: "./agents/completed-stream-agent.yaml" }],
2788
+ });
2789
+
2790
+ const manager = new FleetManager({
2791
+ configPath,
2792
+ stateDir,
2793
+ logger: createSilentLogger(),
2794
+ });
2795
+
2796
+ await manager.initialize();
2797
+
2798
+ // Create a job and cancel it immediately to make it "completed"
2799
+ const result = await manager.trigger("completed-stream-agent");
2800
+ await manager.cancelJob(result.jobId);
2801
+
2802
+ // Stream should complete quickly for cancelled job
2803
+ const stream = manager.streamJobOutput(result.jobId);
2804
+ const entries: unknown[] = [];
2805
+
2806
+ // Use a short timeout for the test
2807
+ const timeout = setTimeout(() => {}, 100);
2808
+
2809
+ try {
2810
+ for await (const entry of stream) {
2811
+ entries.push(entry);
2812
+ // Break after first entry or timeout
2813
+ break;
2814
+ }
2815
+ } finally {
2816
+ clearTimeout(timeout);
2817
+ }
2818
+
2819
+ // The stream should have yielded at least some entries or completed
2820
+ expect(stream[Symbol.asyncIterator]).toBeDefined();
2821
+ });
2822
+ });
2823
+
2824
+ // ===========================================================================
2825
+ // initialize edge cases
2826
+ // ===========================================================================
2827
+ describe("initialize edge cases", () => {
2828
+ it("emits started event", async () => {
2829
+ await createAgentConfig("emit-started-agent-1", {
2830
+ name: "emit-started-agent-1",
2831
+ });
2832
+ await createAgentConfig("emit-started-agent-2", {
2833
+ name: "emit-started-agent-2",
2834
+ });
2835
+
2836
+ const configPath = await createConfig({
2837
+ version: 1,
2838
+ agents: [
2839
+ { path: "./agents/emit-started-agent-1.yaml" },
2840
+ { path: "./agents/emit-started-agent-2.yaml" },
2841
+ ],
2842
+ });
2843
+
2844
+ const manager = new FleetManager({
2845
+ configPath,
2846
+ stateDir,
2847
+ logger: createSilentLogger(),
2848
+ });
2849
+
2850
+ const startedHandler = vi.fn();
2851
+ manager.on("started", startedHandler);
2852
+
2853
+ await manager.initialize();
2854
+ await manager.start();
2855
+ await new Promise((resolve) => setTimeout(resolve, 50));
2856
+
2857
+ await manager.stop();
2858
+
2859
+ // started event is emitted when fleet starts
2860
+ expect(startedHandler).toHaveBeenCalled();
2861
+ });
2862
+ });
2863
+ });
2864
+
2865
+
2866
+
2867
+
2868
+
2869
+