@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,1334 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { GitHubWorkSourceAdapter, GitHubAPIError, GitHubAuthError, createGitHubAdapter, extractRateLimitInfo, isRateLimitResponse, calculateBackoffDelay, } from "../adapters/github.js";
3
+ // =============================================================================
4
+ // Test Fixtures
5
+ // =============================================================================
6
+ /**
7
+ * Create a mock GitHub issue
8
+ */
9
+ function createMockIssue(overrides = {}) {
10
+ return {
11
+ number: 1,
12
+ title: "Test Issue",
13
+ body: "Test issue body",
14
+ html_url: "https://github.com/owner/repo/issues/1",
15
+ state: "open",
16
+ labels: [{ name: "ready" }],
17
+ created_at: "2024-01-15T10:00:00Z",
18
+ updated_at: "2024-01-15T12:00:00Z",
19
+ assignee: null,
20
+ assignees: [],
21
+ milestone: null,
22
+ user: { login: "testuser" },
23
+ ...overrides,
24
+ };
25
+ }
26
+ /**
27
+ * Create a default adapter config
28
+ */
29
+ function createConfig(overrides = {}) {
30
+ return {
31
+ type: "github",
32
+ owner: "testowner",
33
+ repo: "testrepo",
34
+ token: "test-token",
35
+ ...overrides,
36
+ };
37
+ }
38
+ /**
39
+ * Mock fetch response helper
40
+ */
41
+ function mockFetchResponse(data, options = {}) {
42
+ const { status = 200, headers = {} } = options;
43
+ return Promise.resolve({
44
+ ok: status >= 200 && status < 300,
45
+ status,
46
+ statusText: status === 200 ? "OK" : "Error",
47
+ json: () => Promise.resolve(data),
48
+ headers: {
49
+ get: (name) => headers[name] ?? null,
50
+ },
51
+ });
52
+ }
53
+ function getMockCall(mockFetch, index) {
54
+ const call = mockFetch.mock.calls[index];
55
+ if (!call) {
56
+ throw new Error(`Mock call at index ${index} not found`);
57
+ }
58
+ const [url, init] = call;
59
+ return {
60
+ url,
61
+ method: init?.method ?? "GET",
62
+ body: typeof init?.body === "string" ? init.body : undefined,
63
+ headers: init?.headers ?? {},
64
+ };
65
+ }
66
+ // =============================================================================
67
+ // Test Setup
68
+ // =============================================================================
69
+ describe("GitHubWorkSourceAdapter", () => {
70
+ let originalFetch;
71
+ let mockFetch;
72
+ beforeEach(() => {
73
+ originalFetch = global.fetch;
74
+ mockFetch = vi.fn();
75
+ global.fetch = mockFetch;
76
+ });
77
+ afterEach(() => {
78
+ global.fetch = originalFetch;
79
+ vi.restoreAllMocks();
80
+ });
81
+ // ===========================================================================
82
+ // Constructor Tests
83
+ // ===========================================================================
84
+ describe("constructor", () => {
85
+ it("uses default labels when not configured", () => {
86
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
87
+ expect(adapter.type).toBe("github");
88
+ });
89
+ it("uses custom labels when configured", () => {
90
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
91
+ labels: {
92
+ ready: "custom-ready",
93
+ in_progress: "custom-wip",
94
+ },
95
+ }));
96
+ expect(adapter.type).toBe("github");
97
+ });
98
+ it("uses default exclude_labels when not configured", () => {
99
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
100
+ // Default exclude_labels are ["blocked", "wip"]
101
+ expect(adapter.type).toBe("github");
102
+ });
103
+ it("uses custom exclude_labels when configured", () => {
104
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
105
+ exclude_labels: ["on-hold", "needs-review"],
106
+ }));
107
+ expect(adapter.type).toBe("github");
108
+ });
109
+ });
110
+ // ===========================================================================
111
+ // fetchAvailableWork Tests
112
+ // ===========================================================================
113
+ describe("fetchAvailableWork", () => {
114
+ it("fetches issues with the ready label", async () => {
115
+ const mockIssues = [createMockIssue({ number: 1 }), createMockIssue({ number: 2 })];
116
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
117
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
118
+ const result = await adapter.fetchAvailableWork();
119
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("/repos/testowner/testrepo/issues"), expect.objectContaining({
120
+ method: "GET",
121
+ headers: expect.objectContaining({
122
+ Authorization: "Bearer test-token",
123
+ }),
124
+ }));
125
+ expect(result.items).toHaveLength(2);
126
+ });
127
+ it("filters by ready label in query params", async () => {
128
+ mockFetch.mockReturnValue(mockFetchResponse([]));
129
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
130
+ await adapter.fetchAvailableWork();
131
+ const callUrl = mockFetch.mock.calls[0][0];
132
+ expect(callUrl).toContain("labels=ready");
133
+ });
134
+ it("uses custom ready label", async () => {
135
+ mockFetch.mockReturnValue(mockFetchResponse([]));
136
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
137
+ labels: { ready: "agent-ready" },
138
+ }));
139
+ await adapter.fetchAvailableWork();
140
+ const callUrl = mockFetch.mock.calls[0][0];
141
+ expect(callUrl).toContain("labels=agent-ready");
142
+ });
143
+ it("sorts by creation date ascending (oldest first)", async () => {
144
+ mockFetch.mockReturnValue(mockFetchResponse([]));
145
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
146
+ await adapter.fetchAvailableWork();
147
+ const callUrl = mockFetch.mock.calls[0][0];
148
+ expect(callUrl).toContain("sort=created");
149
+ expect(callUrl).toContain("direction=asc");
150
+ });
151
+ it("excludes issues with exclude_labels", async () => {
152
+ const mockIssues = [
153
+ createMockIssue({ number: 1, labels: [{ name: "ready" }] }),
154
+ createMockIssue({ number: 2, labels: [{ name: "ready" }, { name: "blocked" }] }),
155
+ createMockIssue({ number: 3, labels: [{ name: "ready" }, { name: "wip" }] }),
156
+ ];
157
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
158
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
159
+ const result = await adapter.fetchAvailableWork();
160
+ // Only issue 1 should be returned (2 has blocked, 3 has wip)
161
+ expect(result.items).toHaveLength(1);
162
+ expect(result.items[0].externalId).toBe("1");
163
+ });
164
+ it("excludes issues with custom exclude_labels", async () => {
165
+ const mockIssues = [
166
+ createMockIssue({ number: 1, labels: [{ name: "ready" }] }),
167
+ createMockIssue({ number: 2, labels: [{ name: "ready" }, { name: "on-hold" }] }),
168
+ ];
169
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
170
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
171
+ exclude_labels: ["on-hold"],
172
+ }));
173
+ const result = await adapter.fetchAvailableWork();
174
+ expect(result.items).toHaveLength(1);
175
+ expect(result.items[0].externalId).toBe("1");
176
+ });
177
+ it("excludes issues with in_progress label by default", async () => {
178
+ const mockIssues = [
179
+ createMockIssue({ number: 1, labels: [{ name: "ready" }] }),
180
+ createMockIssue({
181
+ number: 2,
182
+ labels: [{ name: "ready" }, { name: "agent-working" }],
183
+ }),
184
+ ];
185
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
186
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
187
+ const result = await adapter.fetchAvailableWork();
188
+ expect(result.items).toHaveLength(1);
189
+ expect(result.items[0].externalId).toBe("1");
190
+ });
191
+ it("includes claimed issues when includeClaimed is true", async () => {
192
+ const mockIssues = [
193
+ createMockIssue({ number: 1, labels: [{ name: "ready" }] }),
194
+ createMockIssue({
195
+ number: 2,
196
+ labels: [{ name: "ready" }, { name: "agent-working" }],
197
+ }),
198
+ ];
199
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
200
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
201
+ const result = await adapter.fetchAvailableWork({ includeClaimed: true });
202
+ expect(result.items).toHaveLength(2);
203
+ });
204
+ it("applies additional label filters", async () => {
205
+ const mockIssues = [
206
+ createMockIssue({ number: 1, labels: [{ name: "ready" }, { name: "bug" }] }),
207
+ createMockIssue({ number: 2, labels: [{ name: "ready" }, { name: "feature" }] }),
208
+ ];
209
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
210
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
211
+ const result = await adapter.fetchAvailableWork({ labels: ["bug"] });
212
+ expect(result.items).toHaveLength(1);
213
+ expect(result.items[0].externalId).toBe("1");
214
+ });
215
+ it("supports pagination with limit", async () => {
216
+ mockFetch.mockReturnValue(mockFetchResponse([]));
217
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
218
+ await adapter.fetchAvailableWork({ limit: 10 });
219
+ const callUrl = mockFetch.mock.calls[0][0];
220
+ expect(callUrl).toContain("per_page=10");
221
+ });
222
+ it("caps limit at 100 (GitHub API max)", async () => {
223
+ mockFetch.mockReturnValue(mockFetchResponse([]));
224
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
225
+ await adapter.fetchAvailableWork({ limit: 200 });
226
+ const callUrl = mockFetch.mock.calls[0][0];
227
+ expect(callUrl).toContain("per_page=100");
228
+ });
229
+ it("supports pagination with cursor", async () => {
230
+ mockFetch.mockReturnValue(mockFetchResponse([]));
231
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
232
+ await adapter.fetchAvailableWork({ cursor: "2" });
233
+ const callUrl = mockFetch.mock.calls[0][0];
234
+ expect(callUrl).toContain("page=2");
235
+ });
236
+ it("extracts nextCursor from Link header", async () => {
237
+ mockFetch.mockReturnValue(mockFetchResponse([createMockIssue()], {
238
+ headers: {
239
+ Link: '<https://api.github.com/repos/owner/repo/issues?page=2>; rel="next", <https://api.github.com/repos/owner/repo/issues?page=5>; rel="last"',
240
+ },
241
+ }));
242
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
243
+ const result = await adapter.fetchAvailableWork();
244
+ expect(result.nextCursor).toBe("2");
245
+ });
246
+ it("returns undefined nextCursor when no more pages", async () => {
247
+ mockFetch.mockReturnValue(mockFetchResponse([createMockIssue()]));
248
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
249
+ const result = await adapter.fetchAvailableWork();
250
+ expect(result.nextCursor).toBeUndefined();
251
+ });
252
+ it("filters by priority when specified", async () => {
253
+ const mockIssues = [
254
+ createMockIssue({ number: 1, labels: [{ name: "ready" }, { name: "critical" }] }),
255
+ createMockIssue({ number: 2, labels: [{ name: "ready" }] }),
256
+ createMockIssue({ number: 3, labels: [{ name: "ready" }, { name: "low" }] }),
257
+ ];
258
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues));
259
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
260
+ const result = await adapter.fetchAvailableWork({ priority: ["critical", "high"] });
261
+ expect(result.items).toHaveLength(1);
262
+ expect(result.items[0].externalId).toBe("1");
263
+ });
264
+ it("maps issue to WorkItem correctly", async () => {
265
+ const mockIssue = createMockIssue({
266
+ number: 42,
267
+ title: "Fix bug in login",
268
+ body: "The login form is broken",
269
+ html_url: "https://github.com/owner/repo/issues/42",
270
+ labels: [{ name: "ready" }, { name: "bug" }, { name: "high" }],
271
+ assignee: { login: "dev1" },
272
+ assignees: [{ login: "dev1" }, { login: "dev2" }],
273
+ milestone: { title: "v1.0", number: 1 },
274
+ user: { login: "reporter" },
275
+ created_at: "2024-01-10T09:00:00Z",
276
+ updated_at: "2024-01-12T14:30:00Z",
277
+ });
278
+ mockFetch.mockReturnValue(mockFetchResponse([mockIssue]));
279
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
280
+ const result = await adapter.fetchAvailableWork();
281
+ expect(result.items).toHaveLength(1);
282
+ const item = result.items[0];
283
+ expect(item.id).toBe("github-42");
284
+ expect(item.source).toBe("github");
285
+ expect(item.externalId).toBe("42");
286
+ expect(item.title).toBe("Fix bug in login");
287
+ expect(item.description).toBe("The login form is broken");
288
+ expect(item.priority).toBe("high");
289
+ expect(item.labels).toEqual(["ready", "bug", "high"]);
290
+ expect(item.url).toBe("https://github.com/owner/repo/issues/42");
291
+ expect(item.metadata).toEqual({
292
+ state: "open",
293
+ assignee: "dev1",
294
+ assignees: ["dev1", "dev2"],
295
+ milestone: { title: "v1.0", number: 1 },
296
+ author: "reporter",
297
+ });
298
+ expect(item.createdAt).toEqual(new Date("2024-01-10T09:00:00Z"));
299
+ expect(item.updatedAt).toEqual(new Date("2024-01-12T14:30:00Z"));
300
+ });
301
+ it("handles null body in issue", async () => {
302
+ const mockIssue = createMockIssue({ body: null });
303
+ mockFetch.mockReturnValue(mockFetchResponse([mockIssue]));
304
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
305
+ const result = await adapter.fetchAvailableWork();
306
+ expect(result.items[0].description).toBe("");
307
+ });
308
+ it("throws GitHubAPIError when missing owner/repo config", async () => {
309
+ const adapter = new GitHubWorkSourceAdapter({ type: "github" });
310
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
311
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow("GitHub adapter requires 'owner' and 'repo' configuration");
312
+ });
313
+ it("throws GitHubAPIError on API error", async () => {
314
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
315
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
316
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
317
+ });
318
+ it("handles network errors gracefully", async () => {
319
+ mockFetch.mockRejectedValue(new Error("Network error"));
320
+ // Disable retries to test immediate error handling
321
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ retry: { maxRetries: 0 } }));
322
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
323
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow("Network error");
324
+ });
325
+ });
326
+ // ===========================================================================
327
+ // Priority Inference Tests
328
+ // ===========================================================================
329
+ describe("priority inference", () => {
330
+ it.each([
331
+ [["critical"], "critical"],
332
+ [["p0"], "critical"],
333
+ [["urgent"], "critical"],
334
+ [["high"], "high"],
335
+ [["p1"], "high"],
336
+ [["important"], "high"],
337
+ [["low"], "low"],
338
+ [["p3"], "low"],
339
+ [["enhancement"], "medium"],
340
+ [[], "medium"],
341
+ ])("infers priority %s as %s", async (labels, expectedPriority) => {
342
+ const mockIssue = createMockIssue({
343
+ labels: [{ name: "ready" }, ...labels.map((name) => ({ name }))],
344
+ });
345
+ mockFetch.mockReturnValue(mockFetchResponse([mockIssue]));
346
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
347
+ const result = await adapter.fetchAvailableWork();
348
+ expect(result.items[0].priority).toBe(expectedPriority);
349
+ });
350
+ it("handles case-insensitive priority labels", async () => {
351
+ const mockIssue = createMockIssue({
352
+ labels: [{ name: "ready" }, { name: "CRITICAL" }],
353
+ });
354
+ mockFetch.mockReturnValue(mockFetchResponse([mockIssue]));
355
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
356
+ const result = await adapter.fetchAvailableWork();
357
+ expect(result.items[0].priority).toBe("critical");
358
+ });
359
+ });
360
+ // ===========================================================================
361
+ // claimWork Tests
362
+ // ===========================================================================
363
+ describe("claimWork", () => {
364
+ it("adds in_progress label and removes ready label", async () => {
365
+ const mockIssue = createMockIssue({
366
+ number: 5,
367
+ labels: [{ name: "ready" }],
368
+ });
369
+ const updatedIssue = createMockIssue({
370
+ number: 5,
371
+ labels: [{ name: "agent-working" }],
372
+ });
373
+ mockFetch
374
+ .mockReturnValueOnce(mockFetchResponse(mockIssue)) // GET issue
375
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 200 })) // POST labels
376
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })) // DELETE ready label
377
+ .mockReturnValueOnce(mockFetchResponse(updatedIssue)); // GET updated issue
378
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
379
+ const result = await adapter.claimWork("github-5");
380
+ expect(result.success).toBe(true);
381
+ expect(result.workItem).toBeDefined();
382
+ expect(result.workItem?.id).toBe("github-5");
383
+ // Verify the API calls
384
+ expect(mockFetch).toHaveBeenCalledTimes(4);
385
+ // Check POST to add label
386
+ const addLabelCall = getMockCall(mockFetch, 1);
387
+ expect(addLabelCall.url).toContain("/issues/5/labels");
388
+ expect(addLabelCall.method).toBe("POST");
389
+ expect(JSON.parse(addLabelCall.body)).toEqual({
390
+ labels: ["agent-working"],
391
+ });
392
+ // Check DELETE to remove ready label
393
+ const removeLabelCall = getMockCall(mockFetch, 2);
394
+ expect(removeLabelCall.url).toContain("/issues/5/labels/ready");
395
+ expect(removeLabelCall.method).toBe("DELETE");
396
+ });
397
+ it("returns already_claimed when issue has in_progress label", async () => {
398
+ const mockIssue = createMockIssue({
399
+ number: 5,
400
+ labels: [{ name: "ready" }, { name: "agent-working" }],
401
+ });
402
+ mockFetch.mockReturnValueOnce(mockFetchResponse(mockIssue));
403
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
404
+ const result = await adapter.claimWork("github-5");
405
+ expect(result.success).toBe(false);
406
+ expect(result.reason).toBe("already_claimed");
407
+ expect(result.message).toContain("already claimed");
408
+ });
409
+ it("returns invalid_state when issue is closed", async () => {
410
+ const mockIssue = createMockIssue({
411
+ number: 5,
412
+ state: "closed",
413
+ });
414
+ mockFetch.mockReturnValueOnce(mockFetchResponse(mockIssue));
415
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
416
+ const result = await adapter.claimWork("github-5");
417
+ expect(result.success).toBe(false);
418
+ expect(result.reason).toBe("invalid_state");
419
+ expect(result.message).toContain("closed");
420
+ });
421
+ it("returns not_found when issue does not exist", async () => {
422
+ mockFetch.mockReturnValueOnce(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
423
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
424
+ const result = await adapter.claimWork("github-999");
425
+ expect(result.success).toBe(false);
426
+ expect(result.reason).toBe("not_found");
427
+ });
428
+ it("returns permission_denied on 403 error", async () => {
429
+ mockFetch.mockReturnValueOnce(mockFetchResponse({ message: "Forbidden" }, { status: 403 }));
430
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
431
+ const result = await adapter.claimWork("github-5");
432
+ expect(result.success).toBe(false);
433
+ expect(result.reason).toBe("permission_denied");
434
+ });
435
+ it("returns source_error on other API errors", async () => {
436
+ mockFetch.mockReturnValueOnce(mockFetchResponse({ message: "Server Error" }, { status: 500 }));
437
+ // Disable retries to test immediate error handling
438
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ retry: { maxRetries: 0 } }));
439
+ const result = await adapter.claimWork("github-5");
440
+ expect(result.success).toBe(false);
441
+ expect(result.reason).toBe("source_error");
442
+ });
443
+ it("throws on invalid work item ID format", async () => {
444
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
445
+ await expect(adapter.claimWork("invalid-id")).rejects.toThrow(GitHubAPIError);
446
+ await expect(adapter.claimWork("invalid-id")).rejects.toThrow('Invalid work item ID format: "invalid-id"');
447
+ });
448
+ it("uses custom in_progress label", async () => {
449
+ const mockIssue = createMockIssue({ number: 5 });
450
+ const updatedIssue = createMockIssue({ number: 5 });
451
+ mockFetch
452
+ .mockReturnValueOnce(mockFetchResponse(mockIssue))
453
+ .mockReturnValueOnce(mockFetchResponse(undefined))
454
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }))
455
+ .mockReturnValueOnce(mockFetchResponse(updatedIssue));
456
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
457
+ labels: { in_progress: "custom-wip" },
458
+ }));
459
+ await adapter.claimWork("github-5");
460
+ const addLabelCall = getMockCall(mockFetch, 1);
461
+ expect(JSON.parse(addLabelCall.body)).toEqual({
462
+ labels: ["custom-wip"],
463
+ });
464
+ });
465
+ });
466
+ // ===========================================================================
467
+ // completeWork Tests
468
+ // ===========================================================================
469
+ describe("completeWork", () => {
470
+ it("posts comment and closes issue on success outcome", async () => {
471
+ mockFetch
472
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 })) // POST comment
473
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })) // DELETE label
474
+ .mockReturnValueOnce(mockFetchResponse({})); // PATCH issue
475
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
476
+ await adapter.completeWork("github-5", {
477
+ outcome: "success",
478
+ summary: "Fixed the bug",
479
+ });
480
+ // Verify comment was posted
481
+ const commentCall = getMockCall(mockFetch, 0);
482
+ expect(commentCall.url).toContain("/issues/5/comments");
483
+ expect(commentCall.method).toBe("POST");
484
+ const commentBody = JSON.parse(commentCall.body).body;
485
+ expect(commentBody).toContain("✅");
486
+ expect(commentBody).toContain("success");
487
+ expect(commentBody).toContain("Fixed the bug");
488
+ // Verify issue was closed
489
+ const closeCall = getMockCall(mockFetch, 2);
490
+ expect(closeCall.url).toContain("/issues/5");
491
+ expect(closeCall.method).toBe("PATCH");
492
+ expect(JSON.parse(closeCall.body)).toEqual({
493
+ state: "closed",
494
+ state_reason: "completed",
495
+ });
496
+ });
497
+ it("posts comment but does not close on failure outcome", async () => {
498
+ mockFetch
499
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 })) // POST comment
500
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })); // DELETE label
501
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
502
+ await adapter.completeWork("github-5", {
503
+ outcome: "failure",
504
+ summary: "Could not fix the bug",
505
+ error: "Compilation error",
506
+ });
507
+ // Should only have 2 calls (comment + delete label), no close
508
+ expect(mockFetch).toHaveBeenCalledTimes(2);
509
+ const failureCommentCall = getMockCall(mockFetch, 0);
510
+ const failureCommentBody = JSON.parse(failureCommentCall.body).body;
511
+ expect(failureCommentBody).toContain("❌");
512
+ expect(failureCommentBody).toContain("failure");
513
+ expect(failureCommentBody).toContain("Compilation error");
514
+ });
515
+ it("posts comment but does not close on partial outcome", async () => {
516
+ mockFetch
517
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 }))
518
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }));
519
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
520
+ await adapter.completeWork("github-5", {
521
+ outcome: "partial",
522
+ summary: "Partially completed",
523
+ });
524
+ expect(mockFetch).toHaveBeenCalledTimes(2);
525
+ const partialCommentCall = getMockCall(mockFetch, 0);
526
+ const partialCommentBody = JSON.parse(partialCommentCall.body).body;
527
+ expect(partialCommentBody).toContain("⚠️");
528
+ expect(partialCommentBody).toContain("partial");
529
+ });
530
+ it("includes details in comment when provided", async () => {
531
+ mockFetch
532
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 }))
533
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }))
534
+ .mockReturnValueOnce(mockFetchResponse({}));
535
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
536
+ await adapter.completeWork("github-5", {
537
+ outcome: "success",
538
+ summary: "Fixed the bug",
539
+ details: "Changed the validation logic in login.ts",
540
+ });
541
+ const detailsCommentCall = getMockCall(mockFetch, 0);
542
+ const detailsCommentBody = JSON.parse(detailsCommentCall.body).body;
543
+ expect(detailsCommentBody).toContain("### Details");
544
+ expect(detailsCommentBody).toContain("Changed the validation logic");
545
+ });
546
+ it("includes artifacts in comment when provided", async () => {
547
+ mockFetch
548
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 }))
549
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }))
550
+ .mockReturnValueOnce(mockFetchResponse({}));
551
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
552
+ await adapter.completeWork("github-5", {
553
+ outcome: "success",
554
+ summary: "Created files",
555
+ artifacts: ["src/new-file.ts", "tests/new-file.test.ts"],
556
+ });
557
+ const artifactsCommentCall = getMockCall(mockFetch, 0);
558
+ const artifactsCommentBody = JSON.parse(artifactsCommentCall.body).body;
559
+ expect(artifactsCommentBody).toContain("### Artifacts");
560
+ expect(artifactsCommentBody).toContain("src/new-file.ts");
561
+ expect(artifactsCommentBody).toContain("tests/new-file.test.ts");
562
+ });
563
+ it("removes in_progress label", async () => {
564
+ mockFetch
565
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 }))
566
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }))
567
+ .mockReturnValueOnce(mockFetchResponse({}));
568
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
569
+ await adapter.completeWork("github-5", {
570
+ outcome: "success",
571
+ summary: "Done",
572
+ });
573
+ const deleteCall = getMockCall(mockFetch, 1);
574
+ expect(deleteCall.url).toContain("/labels/agent-working");
575
+ expect(deleteCall.method).toBe("DELETE");
576
+ });
577
+ });
578
+ // ===========================================================================
579
+ // releaseWork Tests
580
+ // ===========================================================================
581
+ describe("releaseWork", () => {
582
+ it("removes in_progress label and adds ready label", async () => {
583
+ mockFetch
584
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })) // DELETE in_progress
585
+ .mockReturnValueOnce(mockFetchResponse([{ name: "ready" }])); // POST ready
586
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
587
+ const result = await adapter.releaseWork("github-5");
588
+ expect(result.success).toBe(true);
589
+ // Verify DELETE call
590
+ const deleteCall = getMockCall(mockFetch, 0);
591
+ expect(deleteCall.url).toContain("/labels/agent-working");
592
+ expect(deleteCall.method).toBe("DELETE");
593
+ // Verify POST call
594
+ const postCall = getMockCall(mockFetch, 1);
595
+ expect(postCall.url).toContain("/issues/5/labels");
596
+ expect(postCall.method).toBe("POST");
597
+ expect(JSON.parse(postCall.body)).toEqual({ labels: ["ready"] });
598
+ });
599
+ it("adds comment when addComment is true", async () => {
600
+ mockFetch
601
+ .mockReturnValueOnce(mockFetchResponse({ id: 1 })) // POST comment
602
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })) // DELETE label
603
+ .mockReturnValueOnce(mockFetchResponse([{ name: "ready" }])); // POST ready
604
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
605
+ const result = await adapter.releaseWork("github-5", {
606
+ addComment: true,
607
+ reason: "Agent timed out",
608
+ });
609
+ expect(result.success).toBe(true);
610
+ const releaseCommentCall = getMockCall(mockFetch, 0);
611
+ expect(releaseCommentCall.url).toContain("/issues/5/comments");
612
+ const releaseCommentBody = JSON.parse(releaseCommentCall.body).body;
613
+ expect(releaseCommentBody).toContain("Work Released");
614
+ expect(releaseCommentBody).toContain("Agent timed out");
615
+ });
616
+ it("does not add comment when addComment is false", async () => {
617
+ mockFetch
618
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }))
619
+ .mockReturnValueOnce(mockFetchResponse([{ name: "ready" }]));
620
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
621
+ await adapter.releaseWork("github-5", {
622
+ addComment: false,
623
+ reason: "Agent timed out",
624
+ });
625
+ // Should only have 2 calls (delete label + add label)
626
+ expect(mockFetch).toHaveBeenCalledTimes(2);
627
+ const noCommentCall = getMockCall(mockFetch, 0);
628
+ expect(noCommentCall.url).toContain("/labels/");
629
+ });
630
+ it("returns failure on API error", async () => {
631
+ mockFetch
632
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 }))
633
+ .mockRejectedValueOnce(new Error("Network error"));
634
+ // Disable retries to test immediate error handling
635
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ retry: { maxRetries: 0 } }));
636
+ const result = await adapter.releaseWork("github-5");
637
+ expect(result.success).toBe(false);
638
+ expect(result.message).toContain("Network error");
639
+ });
640
+ it("respects cleanup_on_failure: true (default)", async () => {
641
+ mockFetch
642
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })) // DELETE in_progress
643
+ .mockReturnValueOnce(mockFetchResponse([{ name: "ready" }])); // POST ready
644
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
645
+ const result = await adapter.releaseWork("github-5");
646
+ expect(result.success).toBe(true);
647
+ // Should have 2 calls: DELETE in_progress + POST ready
648
+ expect(mockFetch).toHaveBeenCalledTimes(2);
649
+ const postCall = getMockCall(mockFetch, 1);
650
+ expect(postCall.url).toContain("/issues/5/labels");
651
+ expect(postCall.method).toBe("POST");
652
+ expect(JSON.parse(postCall.body)).toEqual({ labels: ["ready"] });
653
+ });
654
+ it("respects cleanup_on_failure: false (skips re-adding ready label)", async () => {
655
+ mockFetch.mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })); // DELETE in_progress
656
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ cleanup_on_failure: false }));
657
+ const result = await adapter.releaseWork("github-5");
658
+ expect(result.success).toBe(true);
659
+ // Should only have 1 call: DELETE in_progress (no POST ready)
660
+ expect(mockFetch).toHaveBeenCalledTimes(1);
661
+ const deleteCall = getMockCall(mockFetch, 0);
662
+ expect(deleteCall.url).toContain("/labels/agent-working");
663
+ expect(deleteCall.method).toBe("DELETE");
664
+ });
665
+ it("respects cleanup_on_failure: true when explicitly set", async () => {
666
+ mockFetch
667
+ .mockReturnValueOnce(mockFetchResponse(undefined, { status: 204 })) // DELETE in_progress
668
+ .mockReturnValueOnce(mockFetchResponse([{ name: "ready" }])); // POST ready
669
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ cleanup_on_failure: true }));
670
+ const result = await adapter.releaseWork("github-5");
671
+ expect(result.success).toBe(true);
672
+ // Should have 2 calls: DELETE in_progress + POST ready
673
+ expect(mockFetch).toHaveBeenCalledTimes(2);
674
+ });
675
+ });
676
+ // ===========================================================================
677
+ // getWork Tests
678
+ // ===========================================================================
679
+ describe("getWork", () => {
680
+ it("fetches and returns work item", async () => {
681
+ const mockIssue = createMockIssue({
682
+ number: 10,
683
+ title: "Test Issue",
684
+ });
685
+ mockFetch.mockReturnValueOnce(mockFetchResponse(mockIssue));
686
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
687
+ const result = await adapter.getWork("github-10");
688
+ expect(result).toBeDefined();
689
+ expect(result?.id).toBe("github-10");
690
+ expect(result?.title).toBe("Test Issue");
691
+ expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining("/repos/testowner/testrepo/issues/10"), expect.any(Object));
692
+ });
693
+ it("returns undefined when issue not found", async () => {
694
+ mockFetch.mockReturnValueOnce(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
695
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
696
+ const result = await adapter.getWork("github-999");
697
+ expect(result).toBeUndefined();
698
+ });
699
+ it("throws on other API errors", async () => {
700
+ mockFetch.mockReturnValueOnce(mockFetchResponse({ message: "Server Error" }, { status: 500 }));
701
+ // Disable retries to test immediate error handling
702
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ retry: { maxRetries: 0 } }));
703
+ await expect(adapter.getWork("github-5")).rejects.toThrow(GitHubAPIError);
704
+ });
705
+ });
706
+ // ===========================================================================
707
+ // GitHubAPIError Tests
708
+ // ===========================================================================
709
+ describe("GitHubAPIError", () => {
710
+ it("has correct name and properties", () => {
711
+ const error = new GitHubAPIError("Test error", {
712
+ statusCode: 404,
713
+ endpoint: "/repos/test",
714
+ });
715
+ expect(error.name).toBe("GitHubAPIError");
716
+ expect(error.message).toBe("Test error");
717
+ expect(error.statusCode).toBe(404);
718
+ expect(error.endpoint).toBe("/repos/test");
719
+ });
720
+ it("preserves cause", () => {
721
+ const cause = new Error("Original error");
722
+ const error = new GitHubAPIError("Wrapped error", { cause });
723
+ expect(error.cause).toBe(cause);
724
+ });
725
+ });
726
+ // ===========================================================================
727
+ // createGitHubAdapter Factory Tests
728
+ // ===========================================================================
729
+ describe("createGitHubAdapter", () => {
730
+ it("creates a GitHubWorkSourceAdapter instance", () => {
731
+ const adapter = createGitHubAdapter(createConfig());
732
+ expect(adapter).toBeInstanceOf(GitHubWorkSourceAdapter);
733
+ expect(adapter.type).toBe("github");
734
+ });
735
+ it("passes config to adapter", () => {
736
+ const adapter = createGitHubAdapter(createConfig({
737
+ owner: "myorg",
738
+ repo: "myrepo",
739
+ }));
740
+ expect(adapter.type).toBe("github");
741
+ });
742
+ });
743
+ // ===========================================================================
744
+ // Token Handling Tests
745
+ // ===========================================================================
746
+ describe("token handling", () => {
747
+ it("uses token from config", async () => {
748
+ mockFetch.mockReturnValue(mockFetchResponse([]));
749
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ token: "config-token" }));
750
+ await adapter.fetchAvailableWork();
751
+ const tokenCall = getMockCall(mockFetch, 0);
752
+ expect(tokenCall.headers.Authorization).toBe("Bearer config-token");
753
+ });
754
+ it("uses GITHUB_TOKEN env var when no config token", async () => {
755
+ const originalEnv = process.env.GITHUB_TOKEN;
756
+ process.env.GITHUB_TOKEN = "env-token";
757
+ try {
758
+ mockFetch.mockReturnValue(mockFetchResponse([]));
759
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ token: undefined }));
760
+ await adapter.fetchAvailableWork();
761
+ const envTokenCall = getMockCall(mockFetch, 0);
762
+ expect(envTokenCall.headers.Authorization).toBe("Bearer env-token");
763
+ }
764
+ finally {
765
+ if (originalEnv !== undefined) {
766
+ process.env.GITHUB_TOKEN = originalEnv;
767
+ }
768
+ else {
769
+ delete process.env.GITHUB_TOKEN;
770
+ }
771
+ }
772
+ });
773
+ it("makes unauthenticated request when no token available", async () => {
774
+ const originalEnv = process.env.GITHUB_TOKEN;
775
+ delete process.env.GITHUB_TOKEN;
776
+ try {
777
+ mockFetch.mockReturnValue(mockFetchResponse([]));
778
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ token: undefined }));
779
+ await adapter.fetchAvailableWork();
780
+ const noTokenCall = getMockCall(mockFetch, 0);
781
+ expect(noTokenCall.headers.Authorization).toBeUndefined();
782
+ }
783
+ finally {
784
+ if (originalEnv !== undefined) {
785
+ process.env.GITHUB_TOKEN = originalEnv;
786
+ }
787
+ }
788
+ });
789
+ });
790
+ // ===========================================================================
791
+ // Custom API Base URL Tests
792
+ // ===========================================================================
793
+ describe("custom API base URL", () => {
794
+ it("uses default api.github.com when not configured", async () => {
795
+ mockFetch.mockReturnValue(mockFetchResponse([]));
796
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
797
+ await adapter.fetchAvailableWork();
798
+ const defaultUrlCall = getMockCall(mockFetch, 0);
799
+ expect(defaultUrlCall.url.startsWith("https://api.github.com")).toBe(true);
800
+ });
801
+ it("uses custom API base URL for GitHub Enterprise", async () => {
802
+ mockFetch.mockReturnValue(mockFetchResponse([]));
803
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
804
+ apiBaseUrl: "https://github.mycompany.com/api/v3",
805
+ }));
806
+ await adapter.fetchAvailableWork();
807
+ const customUrlCall = getMockCall(mockFetch, 0);
808
+ expect(customUrlCall.url.startsWith("https://github.mycompany.com/api/v3")).toBe(true);
809
+ });
810
+ });
811
+ // ===========================================================================
812
+ // Rate Limit Handling Tests
813
+ // ===========================================================================
814
+ describe("rate limit handling", () => {
815
+ it("extracts rate limit info from response headers", async () => {
816
+ const mockIssues = [createMockIssue()];
817
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues, {
818
+ headers: {
819
+ "X-RateLimit-Limit": "5000",
820
+ "X-RateLimit-Remaining": "4999",
821
+ "X-RateLimit-Reset": "1700000000",
822
+ "X-RateLimit-Resource": "core",
823
+ },
824
+ }));
825
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
826
+ await adapter.fetchAvailableWork();
827
+ expect(adapter.lastRateLimitInfo).toEqual({
828
+ limit: 5000,
829
+ remaining: 4999,
830
+ reset: 1700000000,
831
+ resource: "core",
832
+ });
833
+ });
834
+ it("triggers rate limit warning when remaining is below threshold", async () => {
835
+ const warningCallback = vi.fn();
836
+ const mockIssues = [createMockIssue()];
837
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues, {
838
+ headers: {
839
+ "X-RateLimit-Limit": "5000",
840
+ "X-RateLimit-Remaining": "50",
841
+ "X-RateLimit-Reset": "1700000000",
842
+ "X-RateLimit-Resource": "core",
843
+ },
844
+ }));
845
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
846
+ rateLimitWarning: {
847
+ warningThreshold: 100,
848
+ onWarning: warningCallback,
849
+ },
850
+ }));
851
+ await adapter.fetchAvailableWork();
852
+ expect(warningCallback).toHaveBeenCalledWith({
853
+ limit: 5000,
854
+ remaining: 50,
855
+ reset: 1700000000,
856
+ resource: "core",
857
+ });
858
+ });
859
+ it("does not trigger warning when remaining is above threshold", async () => {
860
+ const warningCallback = vi.fn();
861
+ const mockIssues = [createMockIssue()];
862
+ mockFetch.mockReturnValue(mockFetchResponse(mockIssues, {
863
+ headers: {
864
+ "X-RateLimit-Limit": "5000",
865
+ "X-RateLimit-Remaining": "150",
866
+ "X-RateLimit-Reset": "1700000000",
867
+ "X-RateLimit-Resource": "core",
868
+ },
869
+ }));
870
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
871
+ rateLimitWarning: {
872
+ warningThreshold: 100,
873
+ onWarning: warningCallback,
874
+ },
875
+ }));
876
+ await adapter.fetchAvailableWork();
877
+ expect(warningCallback).not.toHaveBeenCalled();
878
+ });
879
+ it("detects rate limit error from 403 with remaining=0", async () => {
880
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "API rate limit exceeded" }, {
881
+ status: 403,
882
+ headers: {
883
+ "X-RateLimit-Limit": "5000",
884
+ "X-RateLimit-Remaining": "0",
885
+ "X-RateLimit-Reset": "1700000000",
886
+ "X-RateLimit-Resource": "core",
887
+ },
888
+ }));
889
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
890
+ retry: { maxRetries: 0 }, // Disable retries for this test
891
+ }));
892
+ try {
893
+ await adapter.fetchAvailableWork();
894
+ expect.fail("Should have thrown");
895
+ }
896
+ catch (error) {
897
+ expect(error).toBeInstanceOf(GitHubAPIError);
898
+ const apiError = error;
899
+ expect(apiError.isRateLimitError).toBe(true);
900
+ expect(apiError.statusCode).toBe(403);
901
+ expect(apiError.rateLimitInfo).toEqual({
902
+ limit: 5000,
903
+ remaining: 0,
904
+ reset: 1700000000,
905
+ resource: "core",
906
+ });
907
+ }
908
+ });
909
+ it("detects rate limit error from 429 status", async () => {
910
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Too Many Requests" }, { status: 429 }));
911
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
912
+ retry: { maxRetries: 0 },
913
+ }));
914
+ try {
915
+ await adapter.fetchAvailableWork();
916
+ expect.fail("Should have thrown");
917
+ }
918
+ catch (error) {
919
+ expect(error).toBeInstanceOf(GitHubAPIError);
920
+ const apiError = error;
921
+ expect(apiError.isRateLimitError).toBe(true);
922
+ expect(apiError.statusCode).toBe(429);
923
+ }
924
+ });
925
+ });
926
+ // ===========================================================================
927
+ // Retry Logic Tests
928
+ // ===========================================================================
929
+ describe("retry logic", () => {
930
+ it("retries on rate limit error with exponential backoff", async () => {
931
+ const mockIssues = [createMockIssue()];
932
+ // First call fails with rate limit, second succeeds
933
+ mockFetch
934
+ .mockReturnValueOnce(mockFetchResponse({ message: "Rate limit exceeded" }, {
935
+ status: 403,
936
+ headers: {
937
+ "X-RateLimit-Limit": "5000",
938
+ "X-RateLimit-Remaining": "0",
939
+ "X-RateLimit-Reset": String(Math.floor(Date.now() / 1000) + 1),
940
+ },
941
+ }))
942
+ .mockReturnValueOnce(mockFetchResponse(mockIssues));
943
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
944
+ retry: {
945
+ maxRetries: 1,
946
+ baseDelayMs: 10, // Short delay for tests
947
+ maxDelayMs: 100,
948
+ },
949
+ }));
950
+ const result = await adapter.fetchAvailableWork();
951
+ expect(mockFetch).toHaveBeenCalledTimes(2);
952
+ expect(result.items).toHaveLength(1);
953
+ });
954
+ it("retries on network errors", async () => {
955
+ const mockIssues = [createMockIssue()];
956
+ // First call fails with network error, second succeeds
957
+ mockFetch
958
+ .mockRejectedValueOnce(new Error("Network connection failed"))
959
+ .mockReturnValueOnce(mockFetchResponse(mockIssues));
960
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
961
+ retry: {
962
+ maxRetries: 1,
963
+ baseDelayMs: 10,
964
+ },
965
+ }));
966
+ const result = await adapter.fetchAvailableWork();
967
+ expect(mockFetch).toHaveBeenCalledTimes(2);
968
+ expect(result.items).toHaveLength(1);
969
+ });
970
+ it("retries on 5xx server errors", async () => {
971
+ const mockIssues = [createMockIssue()];
972
+ mockFetch
973
+ .mockReturnValueOnce(mockFetchResponse({ message: "Internal Server Error" }, { status: 500 }))
974
+ .mockReturnValueOnce(mockFetchResponse(mockIssues));
975
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
976
+ retry: {
977
+ maxRetries: 1,
978
+ baseDelayMs: 10,
979
+ },
980
+ }));
981
+ const result = await adapter.fetchAvailableWork();
982
+ expect(mockFetch).toHaveBeenCalledTimes(2);
983
+ expect(result.items).toHaveLength(1);
984
+ });
985
+ it("does not retry on 404 errors", async () => {
986
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
987
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
988
+ retry: { maxRetries: 3, baseDelayMs: 10 },
989
+ }));
990
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
991
+ expect(mockFetch).toHaveBeenCalledTimes(1);
992
+ });
993
+ it("does not retry on 401 unauthorized errors", async () => {
994
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Bad credentials" }, { status: 401 }));
995
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
996
+ retry: { maxRetries: 3, baseDelayMs: 10 },
997
+ }));
998
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
999
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1000
+ });
1001
+ it("gives up after max retries", async () => {
1002
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Server Error" }, { status: 500 }));
1003
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
1004
+ retry: {
1005
+ maxRetries: 2,
1006
+ baseDelayMs: 10,
1007
+ },
1008
+ }));
1009
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
1010
+ // Initial attempt + 2 retries = 3 total calls
1011
+ expect(mockFetch).toHaveBeenCalledTimes(3);
1012
+ });
1013
+ it("respects custom retry configuration", async () => {
1014
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Server Error" }, { status: 500 }));
1015
+ const adapter = new GitHubWorkSourceAdapter(createConfig({
1016
+ retry: {
1017
+ maxRetries: 5,
1018
+ baseDelayMs: 5,
1019
+ },
1020
+ }));
1021
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
1022
+ expect(mockFetch).toHaveBeenCalledTimes(6); // 1 + 5 retries
1023
+ });
1024
+ });
1025
+ // ===========================================================================
1026
+ // 404 Error Handling Tests
1027
+ // ===========================================================================
1028
+ describe("404 error handling", () => {
1029
+ it("handles 404 gracefully in getWork (returns undefined)", async () => {
1030
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
1031
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1032
+ const result = await adapter.getWork("github-999");
1033
+ expect(result).toBeUndefined();
1034
+ });
1035
+ it("handles 404 gracefully in claimWork (returns not_found)", async () => {
1036
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
1037
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1038
+ const result = await adapter.claimWork("github-999");
1039
+ expect(result.success).toBe(false);
1040
+ expect(result.reason).toBe("not_found");
1041
+ });
1042
+ it("throws 404 in fetchAvailableWork (indicates config error)", async () => {
1043
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Not Found" }, { status: 404 }));
1044
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1045
+ await expect(adapter.fetchAvailableWork()).rejects.toThrow(GitHubAPIError);
1046
+ });
1047
+ });
1048
+ // ===========================================================================
1049
+ // PAT Validation Tests
1050
+ // ===========================================================================
1051
+ describe("validateToken", () => {
1052
+ it("validates token with required scopes", async () => {
1053
+ mockFetch.mockReturnValue(mockFetchResponse({ login: "testuser" }, {
1054
+ headers: {
1055
+ "X-OAuth-Scopes": "repo, user",
1056
+ "X-RateLimit-Limit": "5000",
1057
+ "X-RateLimit-Remaining": "4999",
1058
+ "X-RateLimit-Reset": "1700000000",
1059
+ },
1060
+ }));
1061
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1062
+ const result = await adapter.validateToken();
1063
+ expect(result.valid).toBe(true);
1064
+ expect(result.scopes).toContain("repo");
1065
+ });
1066
+ it("throws GitHubAuthError when required scopes are missing", async () => {
1067
+ mockFetch.mockReturnValue(mockFetchResponse({ login: "testuser" }, {
1068
+ headers: {
1069
+ "X-OAuth-Scopes": "user, read:org",
1070
+ "X-RateLimit-Limit": "5000",
1071
+ "X-RateLimit-Remaining": "4999",
1072
+ "X-RateLimit-Reset": "1700000000",
1073
+ },
1074
+ }));
1075
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1076
+ try {
1077
+ await adapter.validateToken();
1078
+ expect.fail("Should have thrown");
1079
+ }
1080
+ catch (error) {
1081
+ expect(error).toBeInstanceOf(GitHubAuthError);
1082
+ const authError = error;
1083
+ expect(authError.missingScopes).toContain("repo");
1084
+ expect(authError.foundScopes).toContain("user");
1085
+ }
1086
+ });
1087
+ it("throws GitHubAuthError when token is missing", async () => {
1088
+ const originalEnv = process.env.GITHUB_TOKEN;
1089
+ delete process.env.GITHUB_TOKEN;
1090
+ try {
1091
+ const adapter = new GitHubWorkSourceAdapter(createConfig({ token: undefined }));
1092
+ await expect(adapter.validateToken()).rejects.toThrow(GitHubAuthError);
1093
+ }
1094
+ finally {
1095
+ if (originalEnv !== undefined) {
1096
+ process.env.GITHUB_TOKEN = originalEnv;
1097
+ }
1098
+ }
1099
+ });
1100
+ it("throws GitHubAuthError on 401 unauthorized", async () => {
1101
+ mockFetch.mockReturnValue(mockFetchResponse({ message: "Bad credentials" }, {
1102
+ status: 401,
1103
+ headers: {
1104
+ "X-OAuth-Scopes": "",
1105
+ },
1106
+ }));
1107
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1108
+ await expect(adapter.validateToken()).rejects.toThrow(GitHubAuthError);
1109
+ });
1110
+ it("updates rate limit info during validation", async () => {
1111
+ mockFetch.mockReturnValue(mockFetchResponse({ login: "testuser" }, {
1112
+ headers: {
1113
+ "X-OAuth-Scopes": "repo",
1114
+ "X-RateLimit-Limit": "5000",
1115
+ "X-RateLimit-Remaining": "4500",
1116
+ "X-RateLimit-Reset": "1700000000",
1117
+ },
1118
+ }));
1119
+ const adapter = new GitHubWorkSourceAdapter(createConfig());
1120
+ await adapter.validateToken();
1121
+ expect(adapter.lastRateLimitInfo?.remaining).toBe(4500);
1122
+ });
1123
+ });
1124
+ // ===========================================================================
1125
+ // GitHubAPIError Enhanced Tests
1126
+ // ===========================================================================
1127
+ describe("GitHubAPIError enhanced features", () => {
1128
+ it("isRetryable returns true for rate limit errors", () => {
1129
+ const error = new GitHubAPIError("Rate limited", {
1130
+ statusCode: 403,
1131
+ isRateLimitError: true,
1132
+ });
1133
+ expect(error.isRetryable()).toBe(true);
1134
+ });
1135
+ it("isRetryable returns true for network errors (no status code)", () => {
1136
+ const error = new GitHubAPIError("Network error");
1137
+ expect(error.isRetryable()).toBe(true);
1138
+ });
1139
+ it("isRetryable returns true for 5xx errors", () => {
1140
+ const error500 = new GitHubAPIError("Server error", { statusCode: 500 });
1141
+ const error502 = new GitHubAPIError("Bad gateway", { statusCode: 502 });
1142
+ const error503 = new GitHubAPIError("Service unavailable", { statusCode: 503 });
1143
+ expect(error500.isRetryable()).toBe(true);
1144
+ expect(error502.isRetryable()).toBe(true);
1145
+ expect(error503.isRetryable()).toBe(true);
1146
+ });
1147
+ it("isRetryable returns false for 4xx errors (except rate limit)", () => {
1148
+ const error401 = new GitHubAPIError("Unauthorized", { statusCode: 401 });
1149
+ const error403 = new GitHubAPIError("Forbidden", { statusCode: 403, isRateLimitError: false });
1150
+ const error404 = new GitHubAPIError("Not found", { statusCode: 404 });
1151
+ expect(error401.isRetryable()).toBe(false);
1152
+ expect(error403.isRetryable()).toBe(false);
1153
+ expect(error404.isRetryable()).toBe(false);
1154
+ });
1155
+ it("isNotFound returns true for 404 errors", () => {
1156
+ const error = new GitHubAPIError("Not found", { statusCode: 404 });
1157
+ expect(error.isNotFound()).toBe(true);
1158
+ });
1159
+ it("isPermissionDenied returns true for 403 without rate limit", () => {
1160
+ const error = new GitHubAPIError("Forbidden", { statusCode: 403, isRateLimitError: false });
1161
+ expect(error.isPermissionDenied()).toBe(true);
1162
+ });
1163
+ it("isPermissionDenied returns false for rate limit 403", () => {
1164
+ const error = new GitHubAPIError("Rate limited", { statusCode: 403, isRateLimitError: true });
1165
+ expect(error.isPermissionDenied()).toBe(false);
1166
+ });
1167
+ it("getTimeUntilReset returns time in ms", () => {
1168
+ const futureReset = Math.floor(Date.now() / 1000) + 60; // 60 seconds from now
1169
+ const error = new GitHubAPIError("Rate limited", {
1170
+ statusCode: 403,
1171
+ isRateLimitError: true,
1172
+ rateLimitInfo: {
1173
+ limit: 5000,
1174
+ remaining: 0,
1175
+ reset: futureReset,
1176
+ resource: "core",
1177
+ },
1178
+ });
1179
+ const timeUntilReset = error.getTimeUntilReset();
1180
+ expect(timeUntilReset).toBeDefined();
1181
+ expect(timeUntilReset).toBeGreaterThan(50000); // Should be close to 60000
1182
+ expect(timeUntilReset).toBeLessThanOrEqual(60000);
1183
+ });
1184
+ it("rateLimitResetAt is set correctly", () => {
1185
+ const resetTimestamp = 1700000000;
1186
+ const error = new GitHubAPIError("Rate limited", {
1187
+ rateLimitInfo: {
1188
+ limit: 5000,
1189
+ remaining: 0,
1190
+ reset: resetTimestamp,
1191
+ resource: "core",
1192
+ },
1193
+ });
1194
+ expect(error.rateLimitResetAt).toEqual(new Date(resetTimestamp * 1000));
1195
+ });
1196
+ });
1197
+ // ===========================================================================
1198
+ // GitHubAuthError Tests
1199
+ // ===========================================================================
1200
+ describe("GitHubAuthError", () => {
1201
+ it("calculates missing scopes correctly", () => {
1202
+ const error = new GitHubAuthError("Missing scopes", {
1203
+ foundScopes: ["user", "read:org"],
1204
+ requiredScopes: ["repo", "user"],
1205
+ });
1206
+ expect(error.missingScopes).toEqual(["repo"]);
1207
+ expect(error.foundScopes).toEqual(["user", "read:org"]);
1208
+ expect(error.requiredScopes).toEqual(["repo", "user"]);
1209
+ });
1210
+ it("has correct name", () => {
1211
+ const error = new GitHubAuthError("Test", {
1212
+ foundScopes: [],
1213
+ requiredScopes: ["repo"],
1214
+ });
1215
+ expect(error.name).toBe("GitHubAuthError");
1216
+ });
1217
+ });
1218
+ // ===========================================================================
1219
+ // Utility Function Tests
1220
+ // ===========================================================================
1221
+ describe("extractRateLimitInfo", () => {
1222
+ it("extracts all rate limit headers", () => {
1223
+ const headers = new Headers({
1224
+ "X-RateLimit-Limit": "5000",
1225
+ "X-RateLimit-Remaining": "4999",
1226
+ "X-RateLimit-Reset": "1700000000",
1227
+ "X-RateLimit-Resource": "core",
1228
+ });
1229
+ const info = extractRateLimitInfo(headers);
1230
+ expect(info).toEqual({
1231
+ limit: 5000,
1232
+ remaining: 4999,
1233
+ reset: 1700000000,
1234
+ resource: "core",
1235
+ });
1236
+ });
1237
+ it("returns undefined when headers are missing", () => {
1238
+ const headers = new Headers();
1239
+ const info = extractRateLimitInfo(headers);
1240
+ expect(info).toBeUndefined();
1241
+ });
1242
+ it("defaults resource to core when not provided", () => {
1243
+ const headers = new Headers({
1244
+ "X-RateLimit-Limit": "5000",
1245
+ "X-RateLimit-Remaining": "4999",
1246
+ "X-RateLimit-Reset": "1700000000",
1247
+ });
1248
+ const info = extractRateLimitInfo(headers);
1249
+ expect(info?.resource).toBe("core");
1250
+ });
1251
+ });
1252
+ describe("isRateLimitResponse", () => {
1253
+ it("returns true for 403 with remaining=0", () => {
1254
+ const response = {
1255
+ status: 403,
1256
+ headers: {
1257
+ get: (name) => name === "X-RateLimit-Remaining" ? "0" : null,
1258
+ },
1259
+ };
1260
+ expect(isRateLimitResponse(response)).toBe(true);
1261
+ });
1262
+ it("returns true for 429 status", () => {
1263
+ const response = {
1264
+ status: 429,
1265
+ headers: {
1266
+ get: () => null,
1267
+ },
1268
+ };
1269
+ expect(isRateLimitResponse(response)).toBe(true);
1270
+ });
1271
+ it("returns false for 403 with remaining > 0", () => {
1272
+ const response = {
1273
+ status: 403,
1274
+ headers: {
1275
+ get: (name) => name === "X-RateLimit-Remaining" ? "100" : null,
1276
+ },
1277
+ };
1278
+ expect(isRateLimitResponse(response)).toBe(false);
1279
+ });
1280
+ it("returns false for non-403/429 status", () => {
1281
+ const response = {
1282
+ status: 404,
1283
+ headers: {
1284
+ get: () => null,
1285
+ },
1286
+ };
1287
+ expect(isRateLimitResponse(response)).toBe(false);
1288
+ });
1289
+ });
1290
+ describe("calculateBackoffDelay", () => {
1291
+ const options = {
1292
+ maxRetries: 3,
1293
+ baseDelayMs: 1000,
1294
+ maxDelayMs: 30000,
1295
+ jitterFactor: 0,
1296
+ };
1297
+ it("calculates exponential backoff", () => {
1298
+ expect(calculateBackoffDelay(0, options)).toBe(1000);
1299
+ expect(calculateBackoffDelay(1, options)).toBe(2000);
1300
+ expect(calculateBackoffDelay(2, options)).toBe(4000);
1301
+ expect(calculateBackoffDelay(3, options)).toBe(8000);
1302
+ });
1303
+ it("respects max delay", () => {
1304
+ const smallMaxOptions = { ...options, maxDelayMs: 3000 };
1305
+ expect(calculateBackoffDelay(0, smallMaxOptions)).toBe(1000);
1306
+ expect(calculateBackoffDelay(1, smallMaxOptions)).toBe(2000);
1307
+ expect(calculateBackoffDelay(2, smallMaxOptions)).toBe(3000); // Capped
1308
+ expect(calculateBackoffDelay(3, smallMaxOptions)).toBe(3000); // Still capped
1309
+ });
1310
+ it("uses rate limit reset time when provided", () => {
1311
+ const resetMs = 5000;
1312
+ const delay = calculateBackoffDelay(0, options, resetMs);
1313
+ // Should be resetMs + 1000 buffer
1314
+ expect(delay).toBe(6000);
1315
+ });
1316
+ it("caps rate limit reset delay at maxDelayMs", () => {
1317
+ const resetMs = 60000; // 60 seconds
1318
+ const delay = calculateBackoffDelay(0, options, resetMs);
1319
+ expect(delay).toBe(30000); // maxDelayMs
1320
+ });
1321
+ it("adds jitter when jitterFactor > 0", () => {
1322
+ const jitterOptions = { ...options, jitterFactor: 0.1 };
1323
+ // Run multiple times to verify jitter adds variance
1324
+ const delays = Array.from({ length: 10 }, () => calculateBackoffDelay(0, jitterOptions));
1325
+ // Base delay is 1000, with 10% jitter range is 1000-1100
1326
+ expect(Math.min(...delays)).toBeGreaterThanOrEqual(1000);
1327
+ expect(Math.max(...delays)).toBeLessThanOrEqual(1100);
1328
+ // Should have some variance (not all the same)
1329
+ const uniqueDelays = new Set(delays);
1330
+ expect(uniqueDelays.size).toBeGreaterThan(1);
1331
+ });
1332
+ });
1333
+ });
1334
+ //# sourceMappingURL=github.test.js.map