@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,1708 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { mkdir, rm, realpath, readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { JobExecutor, executeJob, type SDKQueryFunction } from "../job-executor.js";
6
+ import type { SDKMessage, RunnerOptionsWithCallbacks } from "../types.js";
7
+ import type { ResolvedAgent } from "../../config/index.js";
8
+ import {
9
+ getJob,
10
+ readJobOutputAll,
11
+ initStateDirectory,
12
+ getSessionInfo,
13
+ } from "../../state/index.js";
14
+
15
+ // =============================================================================
16
+ // Test Helpers
17
+ // =============================================================================
18
+
19
+ async function createTempDir(): Promise<string> {
20
+ const baseDir = join(
21
+ tmpdir(),
22
+ `herdctl-executor-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
23
+ );
24
+ await mkdir(baseDir, { recursive: true });
25
+ return await realpath(baseDir);
26
+ }
27
+
28
+ function createTestAgent(overrides: Partial<ResolvedAgent> = {}): ResolvedAgent {
29
+ return {
30
+ name: "test-agent",
31
+ configPath: "/path/to/agent.yaml",
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function createMockLogger() {
37
+ return {
38
+ warnings: [] as string[],
39
+ errors: [] as string[],
40
+ infos: [] as string[],
41
+ warn: (msg: string) => {
42
+ // Suppress warnings during tests
43
+ },
44
+ error: (msg: string) => {
45
+ // Suppress errors during tests
46
+ },
47
+ info: (msg: string) => {
48
+ // Suppress info during tests
49
+ },
50
+ };
51
+ }
52
+
53
+ // Helper to create a mock SDK query function
54
+ function createMockSDKQuery(messages: SDKMessage[]): SDKQueryFunction {
55
+ return async function* mockQuery() {
56
+ for (const message of messages) {
57
+ yield message;
58
+ }
59
+ };
60
+ }
61
+
62
+ // Helper to create a mock SDK query that yields messages with delays
63
+ function createDelayedSDKQuery(
64
+ messages: SDKMessage[],
65
+ delayMs: number = 10
66
+ ): SDKQueryFunction {
67
+ return async function* mockQuery() {
68
+ for (const message of messages) {
69
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
70
+ yield message;
71
+ }
72
+ };
73
+ }
74
+
75
+ // Helper to create a mock SDK query that throws an error
76
+ function createErrorSDKQuery(error: Error): SDKQueryFunction {
77
+ return async function* mockQuery() {
78
+ throw error;
79
+ };
80
+ }
81
+
82
+ // =============================================================================
83
+ // JobExecutor tests
84
+ // =============================================================================
85
+
86
+ describe("JobExecutor", () => {
87
+ let tempDir: string;
88
+ let stateDir: string;
89
+
90
+ beforeEach(async () => {
91
+ tempDir = await createTempDir();
92
+ stateDir = join(tempDir, ".herdctl");
93
+ await initStateDirectory({ path: stateDir });
94
+ });
95
+
96
+ afterEach(async () => {
97
+ await rm(tempDir, { recursive: true, force: true });
98
+ });
99
+
100
+ describe("job lifecycle", () => {
101
+ it("creates job record before execution", async () => {
102
+ const messages: SDKMessage[] = [
103
+ { type: "system", content: "Initialized" },
104
+ { type: "assistant", content: "Done" },
105
+ ];
106
+
107
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
108
+ logger: createMockLogger(),
109
+ });
110
+
111
+ const result = await executor.execute({
112
+ agent: createTestAgent(),
113
+ prompt: "Test prompt",
114
+ stateDir,
115
+ });
116
+
117
+ expect(result.jobId).toMatch(/^job-\d{4}-\d{2}-\d{2}-[a-z0-9]+$/);
118
+
119
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
120
+ expect(job).not.toBeNull();
121
+ expect(job?.agent).toBe("test-agent");
122
+ });
123
+
124
+ it("updates job status to running", async () => {
125
+ let jobIdDuringExecution: string | undefined;
126
+
127
+ const sdkQuery: SDKQueryFunction = async function* () {
128
+ // During execution, we can check the job status
129
+ yield { type: "system", content: "Running" };
130
+ };
131
+
132
+ const executor = new JobExecutor(sdkQuery, {
133
+ logger: createMockLogger(),
134
+ });
135
+
136
+ const result = await executor.execute({
137
+ agent: createTestAgent(),
138
+ prompt: "Test prompt",
139
+ stateDir,
140
+ });
141
+
142
+ // Job should be completed now, but was running during execution
143
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
144
+ expect(job?.status).toBe("completed");
145
+ });
146
+
147
+ it("updates job with final status on success", async () => {
148
+ const messages: SDKMessage[] = [
149
+ { type: "system", content: "Start" },
150
+ { type: "assistant", content: "Task completed!" },
151
+ ];
152
+
153
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
154
+ logger: createMockLogger(),
155
+ });
156
+
157
+ const result = await executor.execute({
158
+ agent: createTestAgent(),
159
+ prompt: "Test prompt",
160
+ stateDir,
161
+ });
162
+
163
+ expect(result.success).toBe(true);
164
+
165
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
166
+ expect(job?.status).toBe("completed");
167
+ expect(job?.exit_reason).toBe("success");
168
+ expect(job?.finished_at).toBeDefined();
169
+ expect(job?.duration_seconds).toBeGreaterThanOrEqual(0);
170
+ });
171
+
172
+ it("updates job with failed status on error", async () => {
173
+ const messages: SDKMessage[] = [
174
+ { type: "system", content: "Start" },
175
+ { type: "error", message: "Something went wrong" },
176
+ ];
177
+
178
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
179
+ logger: createMockLogger(),
180
+ });
181
+
182
+ const result = await executor.execute({
183
+ agent: createTestAgent(),
184
+ prompt: "Test prompt",
185
+ stateDir,
186
+ });
187
+
188
+ expect(result.success).toBe(false);
189
+ expect(result.error?.message).toContain("Something went wrong");
190
+
191
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
192
+ expect(job?.status).toBe("failed");
193
+ expect(job?.exit_reason).toBe("error");
194
+ });
195
+
196
+ it("handles SDK query throwing an error", async () => {
197
+ const executor = new JobExecutor(
198
+ createErrorSDKQuery(new Error("SDK error")),
199
+ { logger: createMockLogger() }
200
+ );
201
+
202
+ const result = await executor.execute({
203
+ agent: createTestAgent(),
204
+ prompt: "Test prompt",
205
+ stateDir,
206
+ });
207
+
208
+ expect(result.success).toBe(false);
209
+ expect(result.error?.message).toContain("SDK error");
210
+
211
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
212
+ expect(job?.status).toBe("failed");
213
+ });
214
+ });
215
+
216
+ describe("streaming output", () => {
217
+ it("writes all messages to job output", async () => {
218
+ const messages: SDKMessage[] = [
219
+ { type: "system", content: "Init" },
220
+ { type: "assistant", content: "Hello" },
221
+ { type: "tool_use", tool_name: "bash", input: { command: "ls" } },
222
+ { type: "tool_result", result: "file1\nfile2", success: true },
223
+ { type: "assistant", content: "Done" },
224
+ ];
225
+
226
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
227
+ logger: createMockLogger(),
228
+ });
229
+
230
+ const result = await executor.execute({
231
+ agent: createTestAgent(),
232
+ prompt: "Test prompt",
233
+ stateDir,
234
+ });
235
+
236
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
237
+ expect(output).toHaveLength(5);
238
+ expect(output[0].type).toBe("system");
239
+ expect(output[1].type).toBe("assistant");
240
+ expect(output[2].type).toBe("tool_use");
241
+ expect(output[3].type).toBe("tool_result");
242
+ expect(output[4].type).toBe("assistant");
243
+ });
244
+
245
+ it("writes output immediately without buffering", async () => {
246
+ let outputCountDuringExecution = 0;
247
+ const jobsDir = join(stateDir, "jobs");
248
+
249
+ const sdkQuery: SDKQueryFunction = async function* () {
250
+ yield { type: "system", content: "First" };
251
+ yield { type: "assistant", content: "Second" };
252
+ yield { type: "assistant", content: "Third" };
253
+ };
254
+
255
+ // We can't easily verify real-time writing in a unit test,
256
+ // but we can verify all messages are written
257
+ const executor = new JobExecutor(sdkQuery, {
258
+ logger: createMockLogger(),
259
+ });
260
+
261
+ const result = await executor.execute({
262
+ agent: createTestAgent(),
263
+ prompt: "Test prompt",
264
+ stateDir,
265
+ });
266
+
267
+ const output = await readJobOutputAll(jobsDir, result.jobId);
268
+ expect(output).toHaveLength(3);
269
+ });
270
+
271
+ it("preserves message content and metadata", async () => {
272
+ const messages: SDKMessage[] = [
273
+ { type: "system", content: "Session init", subtype: "session_start" },
274
+ {
275
+ type: "assistant",
276
+ content: "Response",
277
+ usage: { input_tokens: 100, output_tokens: 50 },
278
+ },
279
+ {
280
+ type: "tool_use",
281
+ tool_name: "read_file",
282
+ tool_use_id: "tool-123",
283
+ input: { path: "/etc/hosts" },
284
+ },
285
+ {
286
+ type: "tool_result",
287
+ tool_use_id: "tool-123",
288
+ result: "localhost",
289
+ success: true,
290
+ },
291
+ ];
292
+
293
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
294
+ logger: createMockLogger(),
295
+ });
296
+
297
+ const result = await executor.execute({
298
+ agent: createTestAgent(),
299
+ prompt: "Test prompt",
300
+ stateDir,
301
+ });
302
+
303
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
304
+
305
+ // Check system message
306
+ expect(output[0].type).toBe("system");
307
+ if (output[0].type === "system") {
308
+ expect(output[0].content).toBe("Session init");
309
+ expect(output[0].subtype).toBe("session_start");
310
+ }
311
+
312
+ // Check assistant message
313
+ expect(output[1].type).toBe("assistant");
314
+ if (output[1].type === "assistant") {
315
+ expect(output[1].content).toBe("Response");
316
+ expect(output[1].usage?.input_tokens).toBe(100);
317
+ expect(output[1].usage?.output_tokens).toBe(50);
318
+ }
319
+
320
+ // Check tool_use message
321
+ expect(output[2].type).toBe("tool_use");
322
+ if (output[2].type === "tool_use") {
323
+ expect(output[2].tool_name).toBe("read_file");
324
+ expect(output[2].tool_use_id).toBe("tool-123");
325
+ expect(output[2].input).toEqual({ path: "/etc/hosts" });
326
+ }
327
+
328
+ // Check tool_result message
329
+ expect(output[3].type).toBe("tool_result");
330
+ if (output[3].type === "tool_result") {
331
+ expect(output[3].tool_use_id).toBe("tool-123");
332
+ expect(output[3].result).toBe("localhost");
333
+ expect(output[3].success).toBe(true);
334
+ }
335
+ });
336
+
337
+ it("writes error message to output when SDK throws", async () => {
338
+ const executor = new JobExecutor(
339
+ createErrorSDKQuery(new Error("Connection failed")),
340
+ { logger: createMockLogger() }
341
+ );
342
+
343
+ const result = await executor.execute({
344
+ agent: createTestAgent(),
345
+ prompt: "Test prompt",
346
+ stateDir,
347
+ });
348
+
349
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
350
+ expect(output.some((m) => m.type === "error")).toBe(true);
351
+
352
+ const errorMsg = output.find((m) => m.type === "error");
353
+ if (errorMsg?.type === "error") {
354
+ expect(errorMsg.message).toContain("Connection failed");
355
+ }
356
+ });
357
+ });
358
+
359
+ describe("message type handling", () => {
360
+ it("handles all message types correctly", async () => {
361
+ const messages: SDKMessage[] = [
362
+ { type: "system", content: "System message", subtype: "init" },
363
+ { type: "assistant", content: "Assistant message", partial: false },
364
+ { type: "tool_use", tool_name: "bash", input: { cmd: "test" } },
365
+ { type: "tool_result", result: "output", success: true },
366
+ { type: "error", message: "Error message", code: "ERR_TEST" },
367
+ ];
368
+
369
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
370
+ logger: createMockLogger(),
371
+ });
372
+
373
+ const result = await executor.execute({
374
+ agent: createTestAgent(),
375
+ prompt: "Test prompt",
376
+ stateDir,
377
+ });
378
+
379
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
380
+
381
+ expect(output[0].type).toBe("system");
382
+ expect(output[1].type).toBe("assistant");
383
+ expect(output[2].type).toBe("tool_use");
384
+ expect(output[3].type).toBe("tool_result");
385
+ expect(output[4].type).toBe("error");
386
+ });
387
+
388
+ it("handles partial assistant messages", async () => {
389
+ const messages: SDKMessage[] = [
390
+ { type: "assistant", content: "Part 1...", partial: true },
391
+ { type: "assistant", content: "Part 1... Part 2...", partial: true },
392
+ { type: "assistant", content: "Part 1... Part 2... Done!", partial: false },
393
+ ];
394
+
395
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
396
+ logger: createMockLogger(),
397
+ });
398
+
399
+ const result = await executor.execute({
400
+ agent: createTestAgent(),
401
+ prompt: "Test prompt",
402
+ stateDir,
403
+ });
404
+
405
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
406
+
407
+ expect(output).toHaveLength(3);
408
+ if (output[0].type === "assistant") {
409
+ expect(output[0].partial).toBe(true);
410
+ }
411
+ if (output[2].type === "assistant") {
412
+ expect(output[2].partial).toBe(false);
413
+ }
414
+ });
415
+
416
+ it("handles tool_result with error", async () => {
417
+ const messages: SDKMessage[] = [
418
+ { type: "tool_use", tool_name: "read_file", input: { path: "/nope" } },
419
+ { type: "tool_result", success: false, error: "File not found" },
420
+ ];
421
+
422
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
423
+ logger: createMockLogger(),
424
+ });
425
+
426
+ const result = await executor.execute({
427
+ agent: createTestAgent(),
428
+ prompt: "Test prompt",
429
+ stateDir,
430
+ });
431
+
432
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
433
+
434
+ if (output[1].type === "tool_result") {
435
+ expect(output[1].success).toBe(false);
436
+ expect(output[1].error).toBe("File not found");
437
+ }
438
+ });
439
+ });
440
+
441
+ describe("session handling", () => {
442
+ it("extracts session ID from system message with init subtype", async () => {
443
+ const messages: SDKMessage[] = [
444
+ { type: "system", content: "Init", subtype: "init", session_id: "session-abc123" },
445
+ { type: "assistant", content: "Done" },
446
+ ];
447
+
448
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
449
+ logger: createMockLogger(),
450
+ });
451
+
452
+ const result = await executor.execute({
453
+ agent: createTestAgent(),
454
+ prompt: "Test prompt",
455
+ stateDir,
456
+ });
457
+
458
+ expect(result.sessionId).toBe("session-abc123");
459
+
460
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
461
+ expect(job?.session_id).toBe("session-abc123");
462
+ });
463
+
464
+ it("does not extract session ID from non-init system messages", async () => {
465
+ const messages: SDKMessage[] = [
466
+ { type: "system", content: "Progress", subtype: "progress", session_id: "should-ignore" },
467
+ { type: "assistant", content: "Done" },
468
+ ];
469
+
470
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
471
+ logger: createMockLogger(),
472
+ });
473
+
474
+ const result = await executor.execute({
475
+ agent: createTestAgent(),
476
+ prompt: "Test prompt",
477
+ stateDir,
478
+ });
479
+
480
+ expect(result.sessionId).toBeUndefined();
481
+
482
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
483
+ expect(job?.session_id).toBeUndefined();
484
+ });
485
+
486
+ it("persists session info via updateSessionInfo", async () => {
487
+ const messages: SDKMessage[] = [
488
+ { type: "system", content: "Init", subtype: "init", session_id: "session-persist-123" },
489
+ { type: "assistant", content: "Done" },
490
+ ];
491
+
492
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
493
+ logger: createMockLogger(),
494
+ });
495
+
496
+ await executor.execute({
497
+ agent: createTestAgent({ name: "persist-agent" }),
498
+ prompt: "Test prompt",
499
+ stateDir,
500
+ });
501
+
502
+ // Verify session info was persisted
503
+ const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "persist-agent");
504
+ expect(sessionInfo).not.toBeNull();
505
+ expect(sessionInfo?.session_id).toBe("session-persist-123");
506
+ expect(sessionInfo?.agent_name).toBe("persist-agent");
507
+ expect(sessionInfo?.job_count).toBe(1);
508
+ expect(sessionInfo?.mode).toBe("autonomous");
509
+ });
510
+
511
+ it("increments job_count on subsequent runs with session", async () => {
512
+ const messages: SDKMessage[] = [
513
+ { type: "system", content: "Init", subtype: "init", session_id: "session-multi-123" },
514
+ { type: "assistant", content: "Done" },
515
+ ];
516
+
517
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
518
+ logger: createMockLogger(),
519
+ });
520
+
521
+ // Run twice
522
+ await executor.execute({
523
+ agent: createTestAgent({ name: "multi-agent" }),
524
+ prompt: "First run",
525
+ stateDir,
526
+ });
527
+
528
+ await executor.execute({
529
+ agent: createTestAgent({ name: "multi-agent" }),
530
+ prompt: "Second run",
531
+ stateDir,
532
+ });
533
+
534
+ // Verify job_count was incremented
535
+ const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "multi-agent");
536
+ expect(sessionInfo?.job_count).toBe(2);
537
+ });
538
+
539
+ it("stores timestamps in session info", async () => {
540
+ const messages: SDKMessage[] = [
541
+ { type: "system", content: "Init", subtype: "init", session_id: "session-ts-123" },
542
+ { type: "assistant", content: "Done" },
543
+ ];
544
+
545
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
546
+ logger: createMockLogger(),
547
+ });
548
+
549
+ await executor.execute({
550
+ agent: createTestAgent({ name: "ts-agent" }),
551
+ prompt: "Test prompt",
552
+ stateDir,
553
+ });
554
+
555
+ const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "ts-agent");
556
+ expect(sessionInfo?.created_at).toBeDefined();
557
+ expect(sessionInfo?.last_used_at).toBeDefined();
558
+
559
+ // Verify they are valid ISO timestamps
560
+ expect(() => new Date(sessionInfo!.created_at)).not.toThrow();
561
+ expect(() => new Date(sessionInfo!.last_used_at)).not.toThrow();
562
+ });
563
+
564
+ it("returns session ID in RunnerResult for caller use", async () => {
565
+ const messages: SDKMessage[] = [
566
+ { type: "system", content: "Init", subtype: "init", session_id: "session-result-123" },
567
+ { type: "assistant", content: "Done" },
568
+ ];
569
+
570
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
571
+ logger: createMockLogger(),
572
+ });
573
+
574
+ const result = await executor.execute({
575
+ agent: createTestAgent(),
576
+ prompt: "Test prompt",
577
+ stateDir,
578
+ });
579
+
580
+ // Session ID should be available in the result for caller use
581
+ expect(result.sessionId).toBe("session-result-123");
582
+ });
583
+
584
+ it("passes resume option to SDK", async () => {
585
+ let receivedOptions: Record<string, unknown> | undefined;
586
+
587
+ const sdkQuery: SDKQueryFunction = async function* (params) {
588
+ receivedOptions = params.options;
589
+ yield { type: "assistant", content: "Resumed" };
590
+ };
591
+
592
+ const executor = new JobExecutor(sdkQuery, {
593
+ logger: createMockLogger(),
594
+ });
595
+
596
+ await executor.execute({
597
+ agent: createTestAgent(),
598
+ prompt: "Test prompt",
599
+ stateDir,
600
+ resume: "session-to-resume",
601
+ });
602
+
603
+ expect(receivedOptions?.resume).toBe("session-to-resume");
604
+ });
605
+
606
+ it("passes fork option to SDK", async () => {
607
+ let receivedOptions: Record<string, unknown> | undefined;
608
+
609
+ const sdkQuery: SDKQueryFunction = async function* (params) {
610
+ receivedOptions = params.options;
611
+ yield { type: "assistant", content: "Forked" };
612
+ };
613
+
614
+ const executor = new JobExecutor(sdkQuery, {
615
+ logger: createMockLogger(),
616
+ });
617
+
618
+ await executor.execute({
619
+ agent: createTestAgent(),
620
+ prompt: "Test prompt",
621
+ stateDir,
622
+ fork: "session-to-fork",
623
+ });
624
+
625
+ expect(receivedOptions?.forkSession).toBe(true);
626
+ });
627
+
628
+ it("creates job with trigger_type 'fork' and forked_from when forking", async () => {
629
+ const messages: SDKMessage[] = [
630
+ { type: "system", content: "Init", subtype: "init", session_id: "forked-session-123" },
631
+ { type: "assistant", content: "Forked session started" },
632
+ ];
633
+
634
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
635
+ logger: createMockLogger(),
636
+ });
637
+
638
+ const result = await executor.execute({
639
+ agent: createTestAgent({ name: "fork-agent" }),
640
+ prompt: "Continue from where we left off",
641
+ stateDir,
642
+ fork: "original-session-id",
643
+ forkedFrom: "job-2024-01-15-abc123",
644
+ });
645
+
646
+ expect(result.success).toBe(true);
647
+
648
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
649
+ expect(job?.trigger_type).toBe("fork");
650
+ expect(job?.forked_from).toBe("job-2024-01-15-abc123");
651
+ });
652
+
653
+ it("sets trigger_type to fork even if triggerType option provided", async () => {
654
+ const messages: SDKMessage[] = [
655
+ { type: "assistant", content: "Forked" },
656
+ ];
657
+
658
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
659
+ logger: createMockLogger(),
660
+ });
661
+
662
+ const result = await executor.execute({
663
+ agent: createTestAgent(),
664
+ prompt: "Test prompt",
665
+ stateDir,
666
+ fork: "session-to-fork",
667
+ triggerType: "manual", // Should be overridden by fork
668
+ });
669
+
670
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
671
+ expect(job?.trigger_type).toBe("fork");
672
+ });
673
+
674
+ it("does not set forked_from when not forking", async () => {
675
+ const messages: SDKMessage[] = [
676
+ { type: "assistant", content: "Normal run" },
677
+ ];
678
+
679
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
680
+ logger: createMockLogger(),
681
+ });
682
+
683
+ const result = await executor.execute({
684
+ agent: createTestAgent(),
685
+ prompt: "Test prompt",
686
+ stateDir,
687
+ });
688
+
689
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
690
+ expect(job?.trigger_type).toBe("manual");
691
+ expect(job?.forked_from).toBeNull();
692
+ });
693
+
694
+ it("updates session info job_count after resume", async () => {
695
+ const messages: SDKMessage[] = [
696
+ { type: "system", content: "Init", subtype: "init", session_id: "resume-session-123" },
697
+ { type: "assistant", content: "Resumed" },
698
+ ];
699
+
700
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
701
+ logger: createMockLogger(),
702
+ });
703
+
704
+ // First run to establish the session
705
+ await executor.execute({
706
+ agent: createTestAgent({ name: "resume-test-agent" }),
707
+ prompt: "First run",
708
+ stateDir,
709
+ });
710
+
711
+ // Check initial job count
712
+ let sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "resume-test-agent");
713
+ expect(sessionInfo?.job_count).toBe(1);
714
+
715
+ // Resume the session
716
+ await executor.execute({
717
+ agent: createTestAgent({ name: "resume-test-agent" }),
718
+ prompt: "Resume run",
719
+ stateDir,
720
+ resume: "resume-session-123",
721
+ });
722
+
723
+ // Check job count was incremented
724
+ sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "resume-test-agent");
725
+ expect(sessionInfo?.job_count).toBe(2);
726
+ });
727
+
728
+ it("updates session info job_count after fork", async () => {
729
+ const messages: SDKMessage[] = [
730
+ { type: "system", content: "Init", subtype: "init", session_id: "fork-session-123" },
731
+ { type: "assistant", content: "Forked" },
732
+ ];
733
+
734
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
735
+ logger: createMockLogger(),
736
+ });
737
+
738
+ // First run to establish a session
739
+ await executor.execute({
740
+ agent: createTestAgent({ name: "fork-test-agent" }),
741
+ prompt: "First run",
742
+ stateDir,
743
+ });
744
+
745
+ // Check initial job count
746
+ let sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "fork-test-agent");
747
+ expect(sessionInfo?.job_count).toBe(1);
748
+
749
+ // Fork the session
750
+ await executor.execute({
751
+ agent: createTestAgent({ name: "fork-test-agent" }),
752
+ prompt: "Fork run",
753
+ stateDir,
754
+ fork: "original-session-id",
755
+ forkedFrom: "job-2024-01-15-parent",
756
+ });
757
+
758
+ // Check job count was incremented
759
+ sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "fork-test-agent");
760
+ expect(sessionInfo?.job_count).toBe(2);
761
+ });
762
+
763
+ it("does not persist session info when no session ID is present", async () => {
764
+ const messages: SDKMessage[] = [
765
+ { type: "system", content: "No session" },
766
+ { type: "assistant", content: "Done" },
767
+ ];
768
+
769
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
770
+ logger: createMockLogger(),
771
+ });
772
+
773
+ await executor.execute({
774
+ agent: createTestAgent({ name: "no-session-agent" }),
775
+ prompt: "Test prompt",
776
+ stateDir,
777
+ });
778
+
779
+ // Verify no session info was created
780
+ const sessionInfo = await getSessionInfo(join(stateDir, "sessions"), "no-session-agent");
781
+ expect(sessionInfo).toBeNull();
782
+ });
783
+ });
784
+
785
+ describe("summary extraction", () => {
786
+ it("extracts summary from explicit summary field", async () => {
787
+ const messages: SDKMessage[] = [
788
+ { type: "assistant", content: "Working..." },
789
+ { type: "assistant", content: "Done!", summary: "Task completed" },
790
+ ];
791
+
792
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
793
+ logger: createMockLogger(),
794
+ });
795
+
796
+ const result = await executor.execute({
797
+ agent: createTestAgent(),
798
+ prompt: "Test prompt",
799
+ stateDir,
800
+ });
801
+
802
+ expect(result.summary).toBe("Task completed");
803
+
804
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
805
+ expect(job?.summary).toBe("Task completed");
806
+ });
807
+
808
+ it("extracts summary from short final assistant message", async () => {
809
+ const messages: SDKMessage[] = [
810
+ { type: "assistant", content: "Working..." },
811
+ { type: "assistant", content: "All tasks finished successfully." },
812
+ ];
813
+
814
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
815
+ logger: createMockLogger(),
816
+ });
817
+
818
+ const result = await executor.execute({
819
+ agent: createTestAgent(),
820
+ prompt: "Test prompt",
821
+ stateDir,
822
+ });
823
+
824
+ expect(result.summary).toBe("All tasks finished successfully.");
825
+ });
826
+
827
+ it("returns undefined summary when no assistant messages exist", async () => {
828
+ const messages: SDKMessage[] = [
829
+ { type: "system", content: "Initialized" },
830
+ { type: "tool_use", tool_name: "bash", input: { command: "ls" } },
831
+ { type: "tool_result", result: "file1\nfile2", success: true },
832
+ ];
833
+
834
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
835
+ logger: createMockLogger(),
836
+ });
837
+
838
+ const result = await executor.execute({
839
+ agent: createTestAgent(),
840
+ prompt: "Test prompt",
841
+ stateDir,
842
+ });
843
+
844
+ expect(result.summary).toBeUndefined();
845
+
846
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
847
+ expect(job?.summary).toBeUndefined();
848
+ });
849
+
850
+ it("returns undefined summary when all assistant messages are partial", async () => {
851
+ const messages: SDKMessage[] = [
852
+ { type: "assistant", content: "Partial 1...", partial: true },
853
+ { type: "assistant", content: "Partial 2...", partial: true },
854
+ ];
855
+
856
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
857
+ logger: createMockLogger(),
858
+ });
859
+
860
+ const result = await executor.execute({
861
+ agent: createTestAgent(),
862
+ prompt: "Test prompt",
863
+ stateDir,
864
+ });
865
+
866
+ expect(result.summary).toBeUndefined();
867
+ });
868
+
869
+ it("returns undefined summary when all assistant messages are too long", async () => {
870
+ const longContent = "x".repeat(501);
871
+ const messages: SDKMessage[] = [
872
+ { type: "assistant", content: longContent },
873
+ { type: "assistant", content: longContent },
874
+ ];
875
+
876
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
877
+ logger: createMockLogger(),
878
+ });
879
+
880
+ const result = await executor.execute({
881
+ agent: createTestAgent(),
882
+ prompt: "Test prompt",
883
+ stateDir,
884
+ });
885
+
886
+ expect(result.summary).toBeUndefined();
887
+ });
888
+
889
+ it("truncates long explicit summary to 500 chars", async () => {
890
+ const longSummary = "x".repeat(600);
891
+ const messages: SDKMessage[] = [
892
+ { type: "assistant", content: "Done!", summary: longSummary },
893
+ ];
894
+
895
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
896
+ logger: createMockLogger(),
897
+ });
898
+
899
+ const result = await executor.execute({
900
+ agent: createTestAgent(),
901
+ prompt: "Test prompt",
902
+ stateDir,
903
+ });
904
+
905
+ expect(result.summary).toBeDefined();
906
+ expect(result.summary!.length).toBe(500);
907
+ expect(result.summary!.endsWith("...")).toBe(true);
908
+
909
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
910
+ expect(job?.summary?.length).toBe(500);
911
+ });
912
+
913
+ it("uses latest summary when multiple messages have summaries", async () => {
914
+ const messages: SDKMessage[] = [
915
+ { type: "assistant", content: "First", summary: "First summary" },
916
+ { type: "assistant", content: "Second", summary: "Second summary" },
917
+ { type: "assistant", content: "Third", summary: "Final summary" },
918
+ ];
919
+
920
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
921
+ logger: createMockLogger(),
922
+ });
923
+
924
+ const result = await executor.execute({
925
+ agent: createTestAgent(),
926
+ prompt: "Test prompt",
927
+ stateDir,
928
+ });
929
+
930
+ expect(result.summary).toBe("Final summary");
931
+ });
932
+
933
+ it("handles empty message stream with undefined summary", async () => {
934
+ const messages: SDKMessage[] = [];
935
+
936
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
937
+ logger: createMockLogger(),
938
+ });
939
+
940
+ const result = await executor.execute({
941
+ agent: createTestAgent(),
942
+ prompt: "Test prompt",
943
+ stateDir,
944
+ });
945
+
946
+ expect(result.summary).toBeUndefined();
947
+ });
948
+ });
949
+
950
+ describe("callbacks", () => {
951
+ it("calls onMessage for each SDK message", async () => {
952
+ const messages: SDKMessage[] = [
953
+ { type: "system", content: "Init" },
954
+ { type: "assistant", content: "Hello" },
955
+ { type: "assistant", content: "World" },
956
+ ];
957
+
958
+ const receivedMessages: SDKMessage[] = [];
959
+ const onMessage = vi.fn((msg: SDKMessage) => {
960
+ receivedMessages.push(msg);
961
+ });
962
+
963
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
964
+ logger: createMockLogger(),
965
+ });
966
+
967
+ await executor.execute({
968
+ agent: createTestAgent(),
969
+ prompt: "Test prompt",
970
+ stateDir,
971
+ onMessage,
972
+ });
973
+
974
+ expect(onMessage).toHaveBeenCalledTimes(3);
975
+ expect(receivedMessages).toHaveLength(3);
976
+ expect(receivedMessages[0].type).toBe("system");
977
+ expect(receivedMessages[1].type).toBe("assistant");
978
+ expect(receivedMessages[2].type).toBe("assistant");
979
+ });
980
+
981
+ it("continues execution if onMessage throws", async () => {
982
+ const messages: SDKMessage[] = [
983
+ { type: "system", content: "Init" },
984
+ { type: "assistant", content: "Continue" },
985
+ ];
986
+
987
+ const onMessage = vi.fn(() => {
988
+ throw new Error("Callback error");
989
+ });
990
+
991
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
992
+ logger: createMockLogger(),
993
+ });
994
+
995
+ const result = await executor.execute({
996
+ agent: createTestAgent(),
997
+ prompt: "Test prompt",
998
+ stateDir,
999
+ onMessage,
1000
+ });
1001
+
1002
+ // Should still succeed despite callback error
1003
+ expect(result.success).toBe(true);
1004
+ expect(onMessage).toHaveBeenCalledTimes(2);
1005
+ });
1006
+
1007
+ it("supports async onMessage callback", async () => {
1008
+ const messages: SDKMessage[] = [
1009
+ { type: "assistant", content: "Hello" },
1010
+ ];
1011
+
1012
+ const onMessage = vi.fn(async () => {
1013
+ await new Promise((resolve) => setTimeout(resolve, 10));
1014
+ });
1015
+
1016
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1017
+ logger: createMockLogger(),
1018
+ });
1019
+
1020
+ await executor.execute({
1021
+ agent: createTestAgent(),
1022
+ prompt: "Test prompt",
1023
+ stateDir,
1024
+ onMessage,
1025
+ });
1026
+
1027
+ expect(onMessage).toHaveBeenCalledTimes(1);
1028
+ });
1029
+ });
1030
+
1031
+ describe("trigger types", () => {
1032
+ it("sets trigger type to manual by default", async () => {
1033
+ const messages: SDKMessage[] = [{ type: "assistant", content: "Done" }];
1034
+
1035
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1036
+ logger: createMockLogger(),
1037
+ });
1038
+
1039
+ const result = await executor.execute({
1040
+ agent: createTestAgent(),
1041
+ prompt: "Test prompt",
1042
+ stateDir,
1043
+ });
1044
+
1045
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1046
+ expect(job?.trigger_type).toBe("manual");
1047
+ });
1048
+
1049
+ it("sets trigger type from options", async () => {
1050
+ const messages: SDKMessage[] = [{ type: "assistant", content: "Done" }];
1051
+
1052
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1053
+ logger: createMockLogger(),
1054
+ });
1055
+
1056
+ const result = await executor.execute({
1057
+ agent: createTestAgent(),
1058
+ prompt: "Test prompt",
1059
+ stateDir,
1060
+ triggerType: "schedule",
1061
+ schedule: "daily-cleanup",
1062
+ });
1063
+
1064
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1065
+ expect(job?.trigger_type).toBe("schedule");
1066
+ expect(job?.schedule).toBe("daily-cleanup");
1067
+ });
1068
+ });
1069
+
1070
+ describe("duration tracking", () => {
1071
+ it("calculates duration in seconds", async () => {
1072
+ const messages: SDKMessage[] = [
1073
+ { type: "system", content: "Start" },
1074
+ { type: "assistant", content: "End" },
1075
+ ];
1076
+
1077
+ const executor = new JobExecutor(
1078
+ createDelayedSDKQuery(messages, 50),
1079
+ { logger: createMockLogger() }
1080
+ );
1081
+
1082
+ const result = await executor.execute({
1083
+ agent: createTestAgent(),
1084
+ prompt: "Test prompt",
1085
+ stateDir,
1086
+ });
1087
+
1088
+ expect(result.durationSeconds).toBeGreaterThanOrEqual(0);
1089
+
1090
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1091
+ expect(job?.duration_seconds).toBeGreaterThanOrEqual(0);
1092
+ });
1093
+ });
1094
+ });
1095
+
1096
+ // =============================================================================
1097
+ // executeJob convenience function tests
1098
+ // =============================================================================
1099
+
1100
+ describe("executeJob", () => {
1101
+ let tempDir: string;
1102
+ let stateDir: string;
1103
+
1104
+ beforeEach(async () => {
1105
+ tempDir = await createTempDir();
1106
+ stateDir = join(tempDir, ".herdctl");
1107
+ await initStateDirectory({ path: stateDir });
1108
+ });
1109
+
1110
+ afterEach(async () => {
1111
+ await rm(tempDir, { recursive: true, force: true });
1112
+ });
1113
+
1114
+ it("executes job using convenience function", async () => {
1115
+ const messages: SDKMessage[] = [
1116
+ { type: "system", content: "Init" },
1117
+ { type: "assistant", content: "Done" },
1118
+ ];
1119
+
1120
+ const result = await executeJob(
1121
+ createMockSDKQuery(messages),
1122
+ {
1123
+ agent: createTestAgent({ name: "convenience-agent" }),
1124
+ prompt: "Test prompt",
1125
+ stateDir,
1126
+ },
1127
+ { logger: createMockLogger() }
1128
+ );
1129
+
1130
+ expect(result.success).toBe(true);
1131
+ expect(result.jobId).toBeDefined();
1132
+
1133
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1134
+ expect(job?.agent).toBe("convenience-agent");
1135
+ });
1136
+
1137
+ it("passes executor options", async () => {
1138
+ const messages: SDKMessage[] = [{ type: "assistant", content: "Done" }];
1139
+ const logger = createMockLogger();
1140
+
1141
+ await executeJob(
1142
+ createMockSDKQuery(messages),
1143
+ {
1144
+ agent: createTestAgent(),
1145
+ prompt: "Test prompt",
1146
+ stateDir,
1147
+ },
1148
+ { logger }
1149
+ );
1150
+
1151
+ // Logger should have been used (even though messages are suppressed)
1152
+ });
1153
+ });
1154
+
1155
+ // =============================================================================
1156
+ // Edge cases
1157
+ // =============================================================================
1158
+
1159
+ describe("edge cases", () => {
1160
+ let tempDir: string;
1161
+ let stateDir: string;
1162
+
1163
+ beforeEach(async () => {
1164
+ tempDir = await createTempDir();
1165
+ stateDir = join(tempDir, ".herdctl");
1166
+ await initStateDirectory({ path: stateDir });
1167
+ });
1168
+
1169
+ afterEach(async () => {
1170
+ await rm(tempDir, { recursive: true, force: true });
1171
+ });
1172
+
1173
+ it("handles empty message stream", async () => {
1174
+ const executor = new JobExecutor(createMockSDKQuery([]), {
1175
+ logger: createMockLogger(),
1176
+ });
1177
+
1178
+ const result = await executor.execute({
1179
+ agent: createTestAgent(),
1180
+ prompt: "Test prompt",
1181
+ stateDir,
1182
+ });
1183
+
1184
+ expect(result.success).toBe(true);
1185
+
1186
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1187
+ expect(output).toHaveLength(0);
1188
+ });
1189
+
1190
+ it("handles very long content in messages", async () => {
1191
+ const longContent = "x".repeat(100000);
1192
+ const messages: SDKMessage[] = [
1193
+ { type: "assistant", content: longContent },
1194
+ ];
1195
+
1196
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1197
+ logger: createMockLogger(),
1198
+ });
1199
+
1200
+ const result = await executor.execute({
1201
+ agent: createTestAgent(),
1202
+ prompt: "Test prompt",
1203
+ stateDir,
1204
+ });
1205
+
1206
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1207
+ if (output[0].type === "assistant") {
1208
+ expect(output[0].content).toHaveLength(100000);
1209
+ }
1210
+ });
1211
+
1212
+ it("handles unicode content", async () => {
1213
+ const messages: SDKMessage[] = [
1214
+ { type: "assistant", content: "Hello 世界! 🌍 Γεια σου κόσμε" },
1215
+ ];
1216
+
1217
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1218
+ logger: createMockLogger(),
1219
+ });
1220
+
1221
+ const result = await executor.execute({
1222
+ agent: createTestAgent(),
1223
+ prompt: "Test prompt",
1224
+ stateDir,
1225
+ });
1226
+
1227
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1228
+ if (output[0].type === "assistant") {
1229
+ expect(output[0].content).toBe("Hello 世界! 🌍 Γεια σου κόσμε");
1230
+ }
1231
+ });
1232
+
1233
+ it("handles special characters in content", async () => {
1234
+ const messages: SDKMessage[] = [
1235
+ {
1236
+ type: "assistant",
1237
+ content: 'Content with "quotes", \\backslashes\\, and\nnewlines',
1238
+ },
1239
+ ];
1240
+
1241
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1242
+ logger: createMockLogger(),
1243
+ });
1244
+
1245
+ const result = await executor.execute({
1246
+ agent: createTestAgent(),
1247
+ prompt: "Test prompt",
1248
+ stateDir,
1249
+ });
1250
+
1251
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1252
+ if (output[0].type === "assistant") {
1253
+ expect(output[0].content).toBe(
1254
+ 'Content with "quotes", \\backslashes\\, and\nnewlines'
1255
+ );
1256
+ }
1257
+ });
1258
+
1259
+ it("handles rapid message stream", async () => {
1260
+ const messages: SDKMessage[] = Array(100)
1261
+ .fill(null)
1262
+ .map((_, i) => ({
1263
+ type: "assistant" as const,
1264
+ content: `Message ${i}`,
1265
+ }));
1266
+
1267
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1268
+ logger: createMockLogger(),
1269
+ });
1270
+
1271
+ const result = await executor.execute({
1272
+ agent: createTestAgent(),
1273
+ prompt: "Test prompt",
1274
+ stateDir,
1275
+ });
1276
+
1277
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1278
+ expect(output).toHaveLength(100);
1279
+ });
1280
+ });
1281
+
1282
+ // =============================================================================
1283
+ // Enhanced error handling tests (US-7)
1284
+ // =============================================================================
1285
+
1286
+ describe("error handling (US-7)", () => {
1287
+ let tempDir: string;
1288
+ let stateDir: string;
1289
+
1290
+ beforeEach(async () => {
1291
+ tempDir = await createTempDir();
1292
+ stateDir = join(tempDir, ".herdctl");
1293
+ await initStateDirectory({ path: stateDir });
1294
+ });
1295
+
1296
+ afterEach(async () => {
1297
+ await rm(tempDir, { recursive: true, force: true });
1298
+ });
1299
+
1300
+ describe("SDK initialization errors", () => {
1301
+ it("catches SDK initialization errors (e.g., missing API key)", async () => {
1302
+ // Simulates SDK throwing immediately when query is created
1303
+ const sdkQuery: SDKQueryFunction = () => {
1304
+ throw new Error("ANTHROPIC_API_KEY environment variable is not set");
1305
+ };
1306
+
1307
+ const executor = new JobExecutor(sdkQuery, {
1308
+ logger: createMockLogger(),
1309
+ });
1310
+
1311
+ const result = await executor.execute({
1312
+ agent: createTestAgent({ name: "api-key-agent" }),
1313
+ prompt: "Test prompt",
1314
+ stateDir,
1315
+ });
1316
+
1317
+ expect(result.success).toBe(false);
1318
+ expect(result.error?.message).toContain("ANTHROPIC_API_KEY");
1319
+ expect(result.errorDetails?.type).toBe("initialization");
1320
+ });
1321
+
1322
+ it("provides context (job ID, agent name) in initialization error", async () => {
1323
+ const sdkQuery: SDKQueryFunction = () => {
1324
+ throw new Error("SDK init failed");
1325
+ };
1326
+
1327
+ const executor = new JobExecutor(sdkQuery, {
1328
+ logger: createMockLogger(),
1329
+ });
1330
+
1331
+ const result = await executor.execute({
1332
+ agent: createTestAgent({ name: "context-agent" }),
1333
+ prompt: "Test prompt",
1334
+ stateDir,
1335
+ });
1336
+
1337
+ expect(result.error?.message).toContain("context-agent");
1338
+ expect(result.error?.message).toContain(result.jobId);
1339
+ });
1340
+ });
1341
+
1342
+ describe("SDK streaming errors", () => {
1343
+ it("catches SDK streaming errors during execution", async () => {
1344
+ const sdkQuery: SDKQueryFunction = async function* () {
1345
+ yield { type: "system", content: "Init" };
1346
+ yield { type: "assistant", content: "Working..." };
1347
+ throw new Error("Connection reset by peer");
1348
+ };
1349
+
1350
+ const executor = new JobExecutor(sdkQuery, {
1351
+ logger: createMockLogger(),
1352
+ });
1353
+
1354
+ const result = await executor.execute({
1355
+ agent: createTestAgent({ name: "streaming-agent" }),
1356
+ prompt: "Test prompt",
1357
+ stateDir,
1358
+ });
1359
+
1360
+ expect(result.success).toBe(false);
1361
+ expect(result.error?.message).toContain("Connection reset");
1362
+ expect(result.errorDetails?.type).toBe("streaming");
1363
+ });
1364
+
1365
+ it("tracks messages received before streaming error", async () => {
1366
+ const sdkQuery: SDKQueryFunction = async function* () {
1367
+ yield { type: "system", content: "Init" };
1368
+ yield { type: "assistant", content: "Message 1" };
1369
+ yield { type: "assistant", content: "Message 2" };
1370
+ throw new Error("Stream interrupted");
1371
+ };
1372
+
1373
+ const executor = new JobExecutor(sdkQuery, {
1374
+ logger: createMockLogger(),
1375
+ });
1376
+
1377
+ const result = await executor.execute({
1378
+ agent: createTestAgent(),
1379
+ prompt: "Test prompt",
1380
+ stateDir,
1381
+ });
1382
+
1383
+ expect(result.success).toBe(false);
1384
+ expect(result.errorDetails?.messagesReceived).toBe(3);
1385
+ });
1386
+
1387
+ it("identifies recoverable errors (rate limit)", async () => {
1388
+ const sdkQuery: SDKQueryFunction = async function* () {
1389
+ yield { type: "system", content: "Init" };
1390
+ throw new Error("Rate limit exceeded, please retry");
1391
+ };
1392
+
1393
+ const executor = new JobExecutor(sdkQuery, {
1394
+ logger: createMockLogger(),
1395
+ });
1396
+
1397
+ const result = await executor.execute({
1398
+ agent: createTestAgent(),
1399
+ prompt: "Test prompt",
1400
+ stateDir,
1401
+ });
1402
+
1403
+ expect(result.errorDetails?.recoverable).toBe(true);
1404
+ });
1405
+
1406
+ it("identifies non-recoverable errors", async () => {
1407
+ const sdkQuery: SDKQueryFunction = async function* () {
1408
+ yield { type: "system", content: "Init" };
1409
+ throw new Error("Invalid request format");
1410
+ };
1411
+
1412
+ const executor = new JobExecutor(sdkQuery, {
1413
+ logger: createMockLogger(),
1414
+ });
1415
+
1416
+ const result = await executor.execute({
1417
+ agent: createTestAgent(),
1418
+ prompt: "Test prompt",
1419
+ stateDir,
1420
+ });
1421
+
1422
+ expect(result.errorDetails?.recoverable).toBe(false);
1423
+ });
1424
+ });
1425
+
1426
+ describe("error logging to job output", () => {
1427
+ it("logs error messages to job output as error type messages", async () => {
1428
+ const executor = new JobExecutor(
1429
+ createErrorSDKQuery(new Error("Test error for logging")),
1430
+ { logger: createMockLogger() }
1431
+ );
1432
+
1433
+ const result = await executor.execute({
1434
+ agent: createTestAgent(),
1435
+ prompt: "Test prompt",
1436
+ stateDir,
1437
+ });
1438
+
1439
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1440
+ const errorMessages = output.filter((m) => m.type === "error");
1441
+
1442
+ expect(errorMessages.length).toBeGreaterThanOrEqual(1);
1443
+ expect(errorMessages[0].type).toBe("error");
1444
+ if (errorMessages[0].type === "error") {
1445
+ expect(errorMessages[0].message).toContain("Test error for logging");
1446
+ }
1447
+ });
1448
+
1449
+ it("includes error code in job output when available", async () => {
1450
+ const errorWithCode = new Error("Network error") as NodeJS.ErrnoException;
1451
+ errorWithCode.code = "ECONNRESET";
1452
+
1453
+ const executor = new JobExecutor(createErrorSDKQuery(errorWithCode), {
1454
+ logger: createMockLogger(),
1455
+ });
1456
+
1457
+ const result = await executor.execute({
1458
+ agent: createTestAgent(),
1459
+ prompt: "Test prompt",
1460
+ stateDir,
1461
+ });
1462
+
1463
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1464
+ const errorMsg = output.find((m) => m.type === "error");
1465
+
1466
+ if (errorMsg?.type === "error") {
1467
+ expect(errorMsg.code).toBe("ECONNRESET");
1468
+ }
1469
+ });
1470
+
1471
+ it("includes stack trace in job output", async () => {
1472
+ const error = new Error("Stack trace test");
1473
+
1474
+ const executor = new JobExecutor(createErrorSDKQuery(error), {
1475
+ logger: createMockLogger(),
1476
+ });
1477
+
1478
+ const result = await executor.execute({
1479
+ agent: createTestAgent(),
1480
+ prompt: "Test prompt",
1481
+ stateDir,
1482
+ });
1483
+
1484
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1485
+ const errorMsg = output.find((m) => m.type === "error");
1486
+
1487
+ if (errorMsg?.type === "error") {
1488
+ expect(errorMsg.stack).toBeDefined();
1489
+ expect(errorMsg.stack).toContain("Stack trace test");
1490
+ }
1491
+ });
1492
+ });
1493
+
1494
+ describe("job status updates", () => {
1495
+ it("updates job status to failed with error exit_reason", async () => {
1496
+ const executor = new JobExecutor(
1497
+ createErrorSDKQuery(new Error("Failure")),
1498
+ { logger: createMockLogger() }
1499
+ );
1500
+
1501
+ const result = await executor.execute({
1502
+ agent: createTestAgent(),
1503
+ prompt: "Test prompt",
1504
+ stateDir,
1505
+ });
1506
+
1507
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1508
+ expect(job?.status).toBe("failed");
1509
+ expect(job?.exit_reason).toBe("error");
1510
+ });
1511
+
1512
+ it("sets exit_reason to timeout for timeout errors", async () => {
1513
+ const timeoutError = new Error("Request timed out");
1514
+
1515
+ const executor = new JobExecutor(createErrorSDKQuery(timeoutError), {
1516
+ logger: createMockLogger(),
1517
+ });
1518
+
1519
+ const result = await executor.execute({
1520
+ agent: createTestAgent(),
1521
+ prompt: "Test prompt",
1522
+ stateDir,
1523
+ });
1524
+
1525
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1526
+ expect(job?.exit_reason).toBe("timeout");
1527
+ });
1528
+
1529
+ it("sets exit_reason to cancelled for abort errors", async () => {
1530
+ const abortError = new Error("Operation aborted by user");
1531
+
1532
+ const executor = new JobExecutor(createErrorSDKQuery(abortError), {
1533
+ logger: createMockLogger(),
1534
+ });
1535
+
1536
+ const result = await executor.execute({
1537
+ agent: createTestAgent(),
1538
+ prompt: "Test prompt",
1539
+ stateDir,
1540
+ });
1541
+
1542
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1543
+ expect(job?.exit_reason).toBe("cancelled");
1544
+ });
1545
+
1546
+ it("sets exit_reason to max_turns for turn limit errors", async () => {
1547
+ const maxTurnsError = new Error("Maximum turns exceeded");
1548
+
1549
+ const executor = new JobExecutor(createErrorSDKQuery(maxTurnsError), {
1550
+ logger: createMockLogger(),
1551
+ });
1552
+
1553
+ const result = await executor.execute({
1554
+ agent: createTestAgent(),
1555
+ prompt: "Test prompt",
1556
+ stateDir,
1557
+ });
1558
+
1559
+ const job = await getJob(join(stateDir, "jobs"), result.jobId);
1560
+ expect(job?.exit_reason).toBe("max_turns");
1561
+ });
1562
+ });
1563
+
1564
+ describe("error details in RunnerResult", () => {
1565
+ it("provides descriptive error message with context", async () => {
1566
+ const executor = new JobExecutor(
1567
+ createErrorSDKQuery(new Error("API connection failed")),
1568
+ { logger: createMockLogger() }
1569
+ );
1570
+
1571
+ const result = await executor.execute({
1572
+ agent: createTestAgent({ name: "descriptive-agent" }),
1573
+ prompt: "Test prompt",
1574
+ stateDir,
1575
+ });
1576
+
1577
+ expect(result.error).toBeDefined();
1578
+ expect(result.error?.message).toContain("API connection failed");
1579
+ expect(result.error?.message).toContain("descriptive-agent");
1580
+ expect(result.error?.message).toContain(result.jobId);
1581
+ });
1582
+
1583
+ it("returns error details in RunnerResult", async () => {
1584
+ const errorWithCode = new Error("Network timeout") as NodeJS.ErrnoException;
1585
+ errorWithCode.code = "ETIMEDOUT";
1586
+
1587
+ const executor = new JobExecutor(createErrorSDKQuery(errorWithCode), {
1588
+ logger: createMockLogger(),
1589
+ });
1590
+
1591
+ const result = await executor.execute({
1592
+ agent: createTestAgent(),
1593
+ prompt: "Test prompt",
1594
+ stateDir,
1595
+ });
1596
+
1597
+ expect(result.errorDetails).toBeDefined();
1598
+ expect(result.errorDetails?.message).toContain("Network timeout");
1599
+ expect(result.errorDetails?.code).toBe("ETIMEDOUT");
1600
+ expect(result.errorDetails?.stack).toBeDefined();
1601
+ });
1602
+ });
1603
+
1604
+ describe("malformed SDK responses", () => {
1605
+ it("does not crash on malformed SDK messages", async () => {
1606
+ const sdkQuery: SDKQueryFunction = async function* () {
1607
+ yield { type: "system", content: "Init" };
1608
+ // Yield a malformed message (null)
1609
+ yield null as unknown as SDKMessage;
1610
+ yield { type: "assistant", content: "Continuing after malformed" };
1611
+ };
1612
+
1613
+ const executor = new JobExecutor(sdkQuery, {
1614
+ logger: createMockLogger(),
1615
+ });
1616
+
1617
+ const result = await executor.execute({
1618
+ agent: createTestAgent(),
1619
+ prompt: "Test prompt",
1620
+ stateDir,
1621
+ });
1622
+
1623
+ // Should complete without crashing
1624
+ expect(result.success).toBe(true);
1625
+
1626
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1627
+ // Should have processed all 3 messages (including malformed one logged as system)
1628
+ expect(output.length).toBe(3);
1629
+ // The malformed message should be logged as a system warning
1630
+ const malformedMsg = output.find(
1631
+ (m) => m.type === "system" && m.subtype === "malformed_message"
1632
+ );
1633
+ expect(malformedMsg).toBeDefined();
1634
+ });
1635
+
1636
+ it("handles messages with missing type field", async () => {
1637
+ const sdkQuery: SDKQueryFunction = async function* () {
1638
+ yield { type: "system", content: "Init" };
1639
+ yield { content: "Missing type" } as unknown as SDKMessage;
1640
+ yield { type: "assistant", content: "Done" };
1641
+ };
1642
+
1643
+ const executor = new JobExecutor(sdkQuery, {
1644
+ logger: createMockLogger(),
1645
+ });
1646
+
1647
+ const result = await executor.execute({
1648
+ agent: createTestAgent(),
1649
+ prompt: "Test prompt",
1650
+ stateDir,
1651
+ });
1652
+
1653
+ expect(result.success).toBe(true);
1654
+
1655
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1656
+ // Should contain a system message about unknown type
1657
+ const unknownTypeMsg = output.find(
1658
+ (m) => m.type === "system" && m.subtype === "unknown_type"
1659
+ );
1660
+ expect(unknownTypeMsg).toBeDefined();
1661
+ });
1662
+
1663
+ it("handles messages with unexpected type values", async () => {
1664
+ const sdkQuery: SDKQueryFunction = async function* () {
1665
+ yield { type: "system", content: "Init" };
1666
+ yield { type: "unexpected_type", content: "Unknown" } as unknown as SDKMessage;
1667
+ yield { type: "assistant", content: "Done" };
1668
+ };
1669
+
1670
+ const executor = new JobExecutor(sdkQuery, {
1671
+ logger: createMockLogger(),
1672
+ });
1673
+
1674
+ const result = await executor.execute({
1675
+ agent: createTestAgent(),
1676
+ prompt: "Test prompt",
1677
+ stateDir,
1678
+ });
1679
+
1680
+ expect(result.success).toBe(true);
1681
+
1682
+ const output = await readJobOutputAll(join(stateDir, "jobs"), result.jobId);
1683
+ // Should handle gracefully
1684
+ expect(output.length).toBeGreaterThanOrEqual(2);
1685
+ });
1686
+
1687
+ it("handles SDK error message type gracefully", async () => {
1688
+ const messages: SDKMessage[] = [
1689
+ { type: "system", content: "Start" },
1690
+ { type: "error", message: "SDK reported error", code: "SDK_ERR" },
1691
+ ];
1692
+
1693
+ const executor = new JobExecutor(createMockSDKQuery(messages), {
1694
+ logger: createMockLogger(),
1695
+ });
1696
+
1697
+ const result = await executor.execute({
1698
+ agent: createTestAgent(),
1699
+ prompt: "Test prompt",
1700
+ stateDir,
1701
+ });
1702
+
1703
+ expect(result.success).toBe(false);
1704
+ expect(result.error?.message).toContain("SDK reported error");
1705
+ expect(result.errorDetails?.code).toBe("SDK_ERR");
1706
+ });
1707
+ });
1708
+ });