@cyanautomation/kaseki-agent 1.4.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 (459) hide show
  1. package/.dockerignore +54 -0
  2. package/.eslintignore +11 -0
  3. package/.eslintrc.json +95 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +53 -0
  6. package/.github/ISSUE_TEMPLATE/security.md +51 -0
  7. package/.github/PULL_REQUEST_TEMPLATE/default.md +71 -0
  8. package/.github/dependabot.yml +38 -0
  9. package/.github/skills/dependency-cache-optimization/SKILL.md +526 -0
  10. package/.github/skills/docker-image-management/SKILL.md +532 -0
  11. package/.github/skills/frontend-design/SKILL.md +782 -0
  12. package/.github/skills/prompt-engineering/SKILL.md +360 -0
  13. package/.github/skills/quality-gate-config/SKILL.md +591 -0
  14. package/.github/skills/result-report-analysis/SKILL.md +576 -0
  15. package/.github/skills/test-automation/SKILL.md +593 -0
  16. package/.github/skills/workflow-diagnosis/SKILL.md +468 -0
  17. package/.github/workflows/build-docker-image.yml +453 -0
  18. package/.github/workflows/release.yml +68 -0
  19. package/.releaserc.json +135 -0
  20. package/CHANGELOG.md +117 -0
  21. package/CLAUDE.md +336 -0
  22. package/CONTRIBUTING.md +339 -0
  23. package/Dockerfile +217 -0
  24. package/README.md +1527 -0
  25. package/STYLE.md +521 -0
  26. package/add-js-extensions.d.ts +9 -0
  27. package/add-js-extensions.d.ts.map +1 -0
  28. package/add-js-extensions.js.map +1 -0
  29. package/dist/add-js-extensions.d.ts +9 -0
  30. package/dist/add-js-extensions.d.ts.map +1 -0
  31. package/dist/add-js-extensions.js +52 -0
  32. package/dist/add-js-extensions.js.map +1 -0
  33. package/dist/ansi-colors.d.ts +26 -0
  34. package/dist/ansi-colors.d.ts.map +1 -0
  35. package/dist/ansi-colors.js +51 -0
  36. package/dist/ansi-colors.js.map +1 -0
  37. package/dist/cli/BaseCommand.d.ts +18 -0
  38. package/dist/cli/BaseCommand.d.ts.map +1 -0
  39. package/dist/cli/BaseCommand.js +31 -0
  40. package/dist/cli/BaseCommand.js.map +1 -0
  41. package/dist/cli/KasekiCLI.d.ts +30 -0
  42. package/dist/cli/KasekiCLI.d.ts.map +1 -0
  43. package/dist/cli/KasekiCLI.js +134 -0
  44. package/dist/cli/KasekiCLI.js.map +1 -0
  45. package/dist/cli/commands/ConfigCommand.d.ts +13 -0
  46. package/dist/cli/commands/ConfigCommand.d.ts.map +1 -0
  47. package/dist/cli/commands/ConfigCommand.js +131 -0
  48. package/dist/cli/commands/ConfigCommand.js.map +1 -0
  49. package/dist/cli/commands/DoctorCommand.d.ts +45 -0
  50. package/dist/cli/commands/DoctorCommand.d.ts.map +1 -0
  51. package/dist/cli/commands/DoctorCommand.js +309 -0
  52. package/dist/cli/commands/DoctorCommand.js.map +1 -0
  53. package/dist/cli/commands/ListCommand.d.ts +9 -0
  54. package/dist/cli/commands/ListCommand.d.ts.map +1 -0
  55. package/dist/cli/commands/ListCommand.js +81 -0
  56. package/dist/cli/commands/ListCommand.js.map +1 -0
  57. package/dist/cli/commands/ReportCommand.d.ts +9 -0
  58. package/dist/cli/commands/ReportCommand.d.ts.map +1 -0
  59. package/dist/cli/commands/ReportCommand.js +98 -0
  60. package/dist/cli/commands/ReportCommand.js.map +1 -0
  61. package/dist/cli/commands/RunCommand.d.ts +13 -0
  62. package/dist/cli/commands/RunCommand.d.ts.map +1 -0
  63. package/dist/cli/commands/RunCommand.js +191 -0
  64. package/dist/cli/commands/RunCommand.js.map +1 -0
  65. package/dist/cli/commands/SecretsCommand.d.ts +9 -0
  66. package/dist/cli/commands/SecretsCommand.d.ts.map +1 -0
  67. package/dist/cli/commands/SecretsCommand.js +109 -0
  68. package/dist/cli/commands/SecretsCommand.js.map +1 -0
  69. package/dist/cli/commands/ServeCommand.d.ts +9 -0
  70. package/dist/cli/commands/ServeCommand.d.ts.map +1 -0
  71. package/dist/cli/commands/ServeCommand.js +50 -0
  72. package/dist/cli/commands/ServeCommand.js.map +1 -0
  73. package/dist/cli/commands/SetupCommand.d.ts +42 -0
  74. package/dist/cli/commands/SetupCommand.d.ts.map +1 -0
  75. package/dist/cli/commands/SetupCommand.js +249 -0
  76. package/dist/cli/commands/SetupCommand.js.map +1 -0
  77. package/dist/cli.d.ts +9 -0
  78. package/dist/cli.d.ts.map +1 -0
  79. package/dist/cli.js +130 -0
  80. package/dist/cli.js.map +1 -0
  81. package/dist/config/ConfigManager.d.ts +395 -0
  82. package/dist/config/ConfigManager.d.ts.map +1 -0
  83. package/dist/config/ConfigManager.js +446 -0
  84. package/dist/config/ConfigManager.js.map +1 -0
  85. package/dist/docker/DockerManager.d.ts +69 -0
  86. package/dist/docker/DockerManager.d.ts.map +1 -0
  87. package/dist/docker/DockerManager.js +266 -0
  88. package/dist/docker/DockerManager.js.map +1 -0
  89. package/dist/event-aggregator.d.ts +71 -0
  90. package/dist/event-aggregator.d.ts.map +1 -0
  91. package/dist/event-aggregator.js +95 -0
  92. package/dist/event-aggregator.js.map +1 -0
  93. package/dist/github-app-token.d.ts +16 -0
  94. package/dist/github-app-token.d.ts.map +1 -0
  95. package/dist/github-app-token.js +148 -0
  96. package/dist/github-app-token.js.map +1 -0
  97. package/dist/idempotency-store.d.ts +61 -0
  98. package/dist/idempotency-store.d.ts.map +1 -0
  99. package/dist/idempotency-store.js +321 -0
  100. package/dist/idempotency-store.js.map +1 -0
  101. package/dist/index.d.ts +25 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +31 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/instance/InstanceManager.d.ts +81 -0
  106. package/dist/instance/InstanceManager.d.ts.map +1 -0
  107. package/dist/instance/InstanceManager.js +220 -0
  108. package/dist/instance/InstanceManager.js.map +1 -0
  109. package/dist/instance-metadata-reader.d.ts +48 -0
  110. package/dist/instance-metadata-reader.d.ts.map +1 -0
  111. package/dist/instance-metadata-reader.js +94 -0
  112. package/dist/instance-metadata-reader.js.map +1 -0
  113. package/dist/instance-state-derivation.d.ts +42 -0
  114. package/dist/instance-state-derivation.d.ts.map +1 -0
  115. package/dist/instance-state-derivation.js +133 -0
  116. package/dist/instance-state-derivation.js.map +1 -0
  117. package/dist/job-scheduler.d.ts +124 -0
  118. package/dist/job-scheduler.d.ts.map +1 -0
  119. package/dist/job-scheduler.js +992 -0
  120. package/dist/job-scheduler.js.map +1 -0
  121. package/dist/kaseki-api-client.d.ts +89 -0
  122. package/dist/kaseki-api-client.d.ts.map +1 -0
  123. package/dist/kaseki-api-client.js +405 -0
  124. package/dist/kaseki-api-client.js.map +1 -0
  125. package/dist/kaseki-api-config.d.ts +34 -0
  126. package/dist/kaseki-api-config.d.ts.map +1 -0
  127. package/dist/kaseki-api-config.js +113 -0
  128. package/dist/kaseki-api-config.js.map +1 -0
  129. package/dist/kaseki-api-routes.d.ts +13 -0
  130. package/dist/kaseki-api-routes.d.ts.map +1 -0
  131. package/dist/kaseki-api-routes.js +559 -0
  132. package/dist/kaseki-api-routes.js.map +1 -0
  133. package/dist/kaseki-api-service-wrapper.d.ts +43 -0
  134. package/dist/kaseki-api-service-wrapper.d.ts.map +1 -0
  135. package/dist/kaseki-api-service-wrapper.js +150 -0
  136. package/dist/kaseki-api-service-wrapper.js.map +1 -0
  137. package/dist/kaseki-api-service.d.ts +16 -0
  138. package/dist/kaseki-api-service.d.ts.map +1 -0
  139. package/dist/kaseki-api-service.js +143 -0
  140. package/dist/kaseki-api-service.js.map +1 -0
  141. package/dist/kaseki-api-types.d.ts +440 -0
  142. package/dist/kaseki-api-types.d.ts.map +1 -0
  143. package/dist/kaseki-api-types.js +64 -0
  144. package/dist/kaseki-api-types.js.map +1 -0
  145. package/dist/kaseki-cli-lib.d.ts +219 -0
  146. package/dist/kaseki-cli-lib.d.ts.map +1 -0
  147. package/dist/kaseki-cli-lib.js +523 -0
  148. package/dist/kaseki-cli-lib.js.map +1 -0
  149. package/dist/kaseki-cli.d.ts +38 -0
  150. package/dist/kaseki-cli.d.ts.map +1 -0
  151. package/dist/kaseki-cli.js +559 -0
  152. package/dist/kaseki-cli.js.map +1 -0
  153. package/dist/kaseki-report.d.ts +3 -0
  154. package/dist/kaseki-report.d.ts.map +1 -0
  155. package/dist/kaseki-report.js +140 -0
  156. package/dist/kaseki-report.js.map +1 -0
  157. package/dist/lib/subprocess-helpers.d.ts +98 -0
  158. package/dist/lib/subprocess-helpers.d.ts.map +1 -0
  159. package/dist/lib/subprocess-helpers.js +136 -0
  160. package/dist/lib/subprocess-helpers.js.map +1 -0
  161. package/dist/logger.d.ts +39 -0
  162. package/dist/logger.d.ts.map +1 -0
  163. package/dist/logger.js +79 -0
  164. package/dist/logger.js.map +1 -0
  165. package/dist/metrics.d.ts +19 -0
  166. package/dist/metrics.d.ts.map +1 -0
  167. package/dist/metrics.js +59 -0
  168. package/dist/metrics.js.map +1 -0
  169. package/dist/middleware/job-lookup.d.ts +27 -0
  170. package/dist/middleware/job-lookup.d.ts.map +1 -0
  171. package/dist/middleware/job-lookup.js +28 -0
  172. package/dist/middleware/job-lookup.js.map +1 -0
  173. package/dist/pi-event-filter.d.ts +3 -0
  174. package/dist/pi-event-filter.d.ts.map +1 -0
  175. package/dist/pi-event-filter.js +126 -0
  176. package/dist/pi-event-filter.js.map +1 -0
  177. package/dist/pi-progress-stream.d.ts +3 -0
  178. package/dist/pi-progress-stream.d.ts.map +1 -0
  179. package/dist/pi-progress-stream.js +205 -0
  180. package/dist/pi-progress-stream.js.map +1 -0
  181. package/dist/pi-progress-summarizer.d.ts +61 -0
  182. package/dist/pi-progress-summarizer.d.ts.map +1 -0
  183. package/dist/pi-progress-summarizer.js +246 -0
  184. package/dist/pi-progress-summarizer.js.map +1 -0
  185. package/dist/pre-flight-validator.d.ts +72 -0
  186. package/dist/pre-flight-validator.d.ts.map +1 -0
  187. package/dist/pre-flight-validator.js +513 -0
  188. package/dist/pre-flight-validator.js.map +1 -0
  189. package/dist/progress-stream-utils.d.ts +3 -0
  190. package/dist/progress-stream-utils.d.ts.map +1 -0
  191. package/dist/progress-stream-utils.js +15 -0
  192. package/dist/progress-stream-utils.js.map +1 -0
  193. package/dist/result-cache.d.ts +52 -0
  194. package/dist/result-cache.d.ts.map +1 -0
  195. package/dist/result-cache.js +134 -0
  196. package/dist/result-cache.js.map +1 -0
  197. package/dist/routes/artifact-routes.d.ts +10 -0
  198. package/dist/routes/artifact-routes.d.ts.map +1 -0
  199. package/dist/routes/artifact-routes.js +126 -0
  200. package/dist/routes/artifact-routes.js.map +1 -0
  201. package/dist/routes/log-routes.d.ts +8 -0
  202. package/dist/routes/log-routes.d.ts.map +1 -0
  203. package/dist/routes/log-routes.js +345 -0
  204. package/dist/routes/log-routes.js.map +1 -0
  205. package/dist/routes/status-routes.d.ts +8 -0
  206. package/dist/routes/status-routes.d.ts.map +1 -0
  207. package/dist/routes/status-routes.js +82 -0
  208. package/dist/routes/status-routes.js.map +1 -0
  209. package/dist/routes/webhook-routes.d.ts +6 -0
  210. package/dist/routes/webhook-routes.d.ts.map +1 -0
  211. package/dist/routes/webhook-routes.js +86 -0
  212. package/dist/routes/webhook-routes.js.map +1 -0
  213. package/dist/run-artifact-metadata-cache.d.ts +42 -0
  214. package/dist/run-artifact-metadata-cache.d.ts.map +1 -0
  215. package/dist/run-artifact-metadata-cache.js +139 -0
  216. package/dist/run-artifact-metadata-cache.js.map +1 -0
  217. package/dist/secret-value-cache.d.ts +13 -0
  218. package/dist/secret-value-cache.d.ts.map +1 -0
  219. package/dist/secret-value-cache.js +44 -0
  220. package/dist/secret-value-cache.js.map +1 -0
  221. package/dist/secrets/SecretsManager.d.ts +80 -0
  222. package/dist/secrets/SecretsManager.d.ts.map +1 -0
  223. package/dist/secrets/SecretsManager.js +306 -0
  224. package/dist/secrets/SecretsManager.js.map +1 -0
  225. package/dist/test-utils.d.ts +55 -0
  226. package/dist/test-utils.d.ts.map +1 -0
  227. package/dist/test-utils.js +48 -0
  228. package/dist/test-utils.js.map +1 -0
  229. package/dist/timestamp-tracker.d.ts +75 -0
  230. package/dist/timestamp-tracker.d.ts.map +1 -0
  231. package/dist/timestamp-tracker.js +121 -0
  232. package/dist/timestamp-tracker.js.map +1 -0
  233. package/dist/utils/failure-artifact-writer.d.ts +29 -0
  234. package/dist/utils/failure-artifact-writer.d.ts.map +1 -0
  235. package/dist/utils/failure-artifact-writer.js +157 -0
  236. package/dist/utils/failure-artifact-writer.js.map +1 -0
  237. package/dist/utils/file-helpers.d.ts +41 -0
  238. package/dist/utils/file-helpers.d.ts.map +1 -0
  239. package/dist/utils/file-helpers.js +143 -0
  240. package/dist/utils/file-helpers.js.map +1 -0
  241. package/dist/utils/http-client-factory.d.ts +46 -0
  242. package/dist/utils/http-client-factory.d.ts.map +1 -0
  243. package/dist/utils/http-client-factory.js +114 -0
  244. package/dist/utils/http-client-factory.js.map +1 -0
  245. package/dist/utils/progress-normalizer.d.ts +13 -0
  246. package/dist/utils/progress-normalizer.d.ts.map +1 -0
  247. package/dist/utils/progress-normalizer.js +57 -0
  248. package/dist/utils/progress-normalizer.js.map +1 -0
  249. package/dist/utils/response-helpers.d.ts +34 -0
  250. package/dist/utils/response-helpers.d.ts.map +1 -0
  251. package/dist/utils/response-helpers.js +78 -0
  252. package/dist/utils/response-helpers.js.map +1 -0
  253. package/dist/utils/route-helpers.d.ts +17 -0
  254. package/dist/utils/route-helpers.d.ts.map +1 -0
  255. package/dist/utils/route-helpers.js +22 -0
  256. package/dist/utils/route-helpers.js.map +1 -0
  257. package/dist/utils/status-response-builder.d.ts +23 -0
  258. package/dist/utils/status-response-builder.d.ts.map +1 -0
  259. package/dist/utils/status-response-builder.js +144 -0
  260. package/dist/utils/status-response-builder.js.map +1 -0
  261. package/dist/utils/type-guards.d.ts +37 -0
  262. package/dist/utils/type-guards.d.ts.map +1 -0
  263. package/dist/utils/type-guards.js +45 -0
  264. package/dist/utils/type-guards.js.map +1 -0
  265. package/dist/utils/utf8-helpers.d.ts +32 -0
  266. package/dist/utils/utf8-helpers.d.ts.map +1 -0
  267. package/dist/utils/utf8-helpers.js +97 -0
  268. package/dist/utils/utf8-helpers.js.map +1 -0
  269. package/dist/utils/webhook-event-builder.d.ts +26 -0
  270. package/dist/utils/webhook-event-builder.d.ts.map +1 -0
  271. package/dist/utils/webhook-event-builder.js +77 -0
  272. package/dist/utils/webhook-event-builder.js.map +1 -0
  273. package/dist/webhook-manager.d.ts +56 -0
  274. package/dist/webhook-manager.d.ts.map +1 -0
  275. package/dist/webhook-manager.js +359 -0
  276. package/dist/webhook-manager.js.map +1 -0
  277. package/docker/workspace-cache/package-lock.json +13 -0
  278. package/docker/workspace-cache/package.json +7 -0
  279. package/docker-compose.yml +53 -0
  280. package/docs/API.md +708 -0
  281. package/docs/BACKLOG.md +19 -0
  282. package/docs/BUILD_STRATEGY.md +404 -0
  283. package/docs/CLI.md +569 -0
  284. package/docs/DEPLOYMENT.md +521 -0
  285. package/docs/DEVELOPMENT.md +459 -0
  286. package/docs/DOCKER_SETUP.md +522 -0
  287. package/docs/ENHANCED_PROGRESS_LOGS.md +264 -0
  288. package/docs/IMPLEMENTATION_SUMMARY.md +549 -0
  289. package/docs/INTEGRATION_EXAMPLE.md +217 -0
  290. package/docs/NPM_SETUP.md +468 -0
  291. package/docs/PHASE1-4_IMPLEMENTATION.md +302 -0
  292. package/docs/PHASE1_COMPLETION.md +192 -0
  293. package/docs/PHASE2_COMPLETION.md +134 -0
  294. package/docs/PHASE6_MIGRATION.md +392 -0
  295. package/docs/PRINTF_SAFETY_FIX.md +282 -0
  296. package/docs/QUALITY_GATES.md +369 -0
  297. package/docs/SETUP_GUIDE.md +482 -0
  298. package/docs/TASK_PROMPT_TEMPLATES.md +533 -0
  299. package/docs/VALIDATION_FIX.md +139 -0
  300. package/docs/VERIFICATION_CHECKLIST.md +335 -0
  301. package/docs/repo-maturity.md +760 -0
  302. package/fix-tests.d.ts +9 -0
  303. package/fix-tests.d.ts.map +1 -0
  304. package/fix-tests.js.map +1 -0
  305. package/fix-tests.ts +53 -0
  306. package/jest.config.ts +31 -0
  307. package/kaseki +183 -0
  308. package/kaseki-agent.sh +1961 -0
  309. package/ops/logrotate/kaseki +10 -0
  310. package/package.json +83 -0
  311. package/perf/README.md +54 -0
  312. package/perf/pi-event-filter.benchmark.test.ts +98 -0
  313. package/run-kaseki-json.test.sh +106 -0
  314. package/run-kaseki.sh +990 -0
  315. package/scripts/allowlist-helper.sh +56 -0
  316. package/scripts/cleanup-kaseki.sh +168 -0
  317. package/scripts/deploy-pi-template.sh +293 -0
  318. package/scripts/docker-entrypoint.sh +71 -0
  319. package/scripts/dry-run-allowlist.sh +161 -0
  320. package/scripts/kaseki-activate.sh +396 -0
  321. package/scripts/kaseki-api.service +62 -0
  322. package/scripts/kaseki-container-entrypoint-wrapper.sh +119 -0
  323. package/scripts/kaseki-container-setup-remote.sh +172 -0
  324. package/scripts/kaseki-container-setup.sh +193 -0
  325. package/scripts/kaseki-healthcheck.sh +95 -0
  326. package/scripts/kaseki-install.sh +50 -0
  327. package/scripts/kaseki-maturity-score.sh +291 -0
  328. package/scripts/kaseki-performance-metrics.sh +122 -0
  329. package/scripts/kaseki-preflight.sh +270 -0
  330. package/scripts/kaseki-setup.sh +265 -0
  331. package/scripts/pi-setup-remote.sh +213 -0
  332. package/scripts/setup-github-labels.sh +42 -0
  333. package/scripts/suggest-allowlist.sh +68 -0
  334. package/scripts/templates/MULTI_HOST_DISTRIBUTED.md +337 -0
  335. package/scripts/templates/REST_API_SERVICE.md +490 -0
  336. package/scripts/templates/SINGLE_HOST_CLI.md +194 -0
  337. package/scripts/test-github-app.sh +248 -0
  338. package/src/add-js-extensions.ts +61 -0
  339. package/src/ansi-colors.test.ts +62 -0
  340. package/src/ansi-colors.ts +67 -0
  341. package/src/cli/BaseCommand.ts +40 -0
  342. package/src/cli/KasekiCLI.ts +154 -0
  343. package/src/cli/commands/ConfigCommand.ts +145 -0
  344. package/src/cli/commands/DoctorCommand.ts +329 -0
  345. package/src/cli/commands/ListCommand.ts +105 -0
  346. package/src/cli/commands/ReportCommand.ts +110 -0
  347. package/src/cli/commands/RunCommand.ts +218 -0
  348. package/src/cli/commands/SecretsCommand.ts +120 -0
  349. package/src/cli/commands/ServeCommand.ts +62 -0
  350. package/src/cli/commands/SetupCommand.ts +301 -0
  351. package/src/cli.ts +138 -0
  352. package/src/config/ConfigManager.ts +476 -0
  353. package/src/docker/DockerManager.ts +319 -0
  354. package/src/docker-entrypoint-packaging.test.ts +33 -0
  355. package/src/event-aggregator.test.ts +117 -0
  356. package/src/event-aggregator.ts +126 -0
  357. package/src/github-app-token.ts +215 -0
  358. package/src/idempotency-store.test.ts +117 -0
  359. package/src/idempotency-store.ts +385 -0
  360. package/src/index.ts +89 -0
  361. package/src/instance/InstanceManager.ts +285 -0
  362. package/src/instance-metadata-reader.test.ts +190 -0
  363. package/src/instance-metadata-reader.ts +129 -0
  364. package/src/instance-state-derivation.test.ts +263 -0
  365. package/src/instance-state-derivation.ts +148 -0
  366. package/src/job-scheduler.test.ts +1236 -0
  367. package/src/job-scheduler.ts +1117 -0
  368. package/src/kaseki-api-client.ts +488 -0
  369. package/src/kaseki-api-config.test.ts +315 -0
  370. package/src/kaseki-api-config.ts +175 -0
  371. package/src/kaseki-api-routes.test.ts +1615 -0
  372. package/src/kaseki-api-routes.ts +643 -0
  373. package/src/kaseki-api-service-wrapper.ts +188 -0
  374. package/src/kaseki-api-service.test.ts +418 -0
  375. package/src/kaseki-api-service.ts +192 -0
  376. package/src/kaseki-api-types.ts +320 -0
  377. package/src/kaseki-cli-lib.test.ts +552 -0
  378. package/src/kaseki-cli-lib.ts +760 -0
  379. package/src/kaseki-cli.ts +682 -0
  380. package/src/kaseki-report.test.ts +118 -0
  381. package/src/kaseki-report.ts +192 -0
  382. package/src/lib/subprocess-helpers.ts +177 -0
  383. package/src/logger.ts +114 -0
  384. package/src/metrics.ts +66 -0
  385. package/src/middleware/job-lookup.test.ts +113 -0
  386. package/src/middleware/job-lookup.ts +45 -0
  387. package/src/pi-event-filter.test.ts +183 -0
  388. package/src/pi-event-filter.ts +183 -0
  389. package/src/pi-progress-stream.ts +287 -0
  390. package/src/pi-progress-summarizer.test.ts +302 -0
  391. package/src/pi-progress-summarizer.ts +287 -0
  392. package/src/pre-flight-validator.test.ts +512 -0
  393. package/src/pre-flight-validator.ts +618 -0
  394. package/src/progress-stream-utils.test.ts +35 -0
  395. package/src/progress-stream-utils.ts +14 -0
  396. package/src/result-cache.test.ts +195 -0
  397. package/src/result-cache.ts +181 -0
  398. package/src/routes/artifact-routes.ts +169 -0
  399. package/src/routes/log-routes.ts +391 -0
  400. package/src/routes/status-routes.ts +92 -0
  401. package/src/routes/webhook-routes.ts +97 -0
  402. package/src/run-artifact-metadata-cache.test.ts +80 -0
  403. package/src/run-artifact-metadata-cache.ts +184 -0
  404. package/src/secret-value-cache.test.ts +66 -0
  405. package/src/secret-value-cache.ts +55 -0
  406. package/src/secrets/SecretsManager.ts +343 -0
  407. package/src/test-utils.ts +81 -0
  408. package/src/timestamp-tracker.test.ts +134 -0
  409. package/src/timestamp-tracker.ts +132 -0
  410. package/src/utils/failure-artifact-writer.ts +187 -0
  411. package/src/utils/file-helpers.test.ts +235 -0
  412. package/src/utils/file-helpers.ts +150 -0
  413. package/src/utils/http-client-factory.test.ts +245 -0
  414. package/src/utils/http-client-factory.ts +157 -0
  415. package/src/utils/progress-normalizer.test.ts +442 -0
  416. package/src/utils/progress-normalizer.ts +68 -0
  417. package/src/utils/response-helpers.test.ts +122 -0
  418. package/src/utils/response-helpers.ts +101 -0
  419. package/src/utils/route-helpers.ts +30 -0
  420. package/src/utils/status-response-builder.ts +159 -0
  421. package/src/utils/type-guards.ts +52 -0
  422. package/src/utils/utf8-helpers.ts +102 -0
  423. package/src/utils/webhook-event-builder.test.ts +143 -0
  424. package/src/utils/webhook-event-builder.ts +87 -0
  425. package/src/webhook-manager.test.ts +152 -0
  426. package/src/webhook-manager.ts +445 -0
  427. package/templates/allowlist-api-route.txt +7 -0
  428. package/templates/allowlist-comprehensive.txt +8 -0
  429. package/templates/allowlist-parser-fix.txt +6 -0
  430. package/templates/allowlist-ui-component.txt +9 -0
  431. package/templates/allowlist-utility.txt +9 -0
  432. package/test/actual-model-metadata.test.sh +102 -0
  433. package/test/dry-run.test.sh +131 -0
  434. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-0.json +1 -0
  435. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-1.json +1 -0
  436. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-invalid.json +1 -0
  437. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-0.json +1 -0
  438. package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-1.json +1 -0
  439. package/test/kaseki-api.integration.test.sh +165 -0
  440. package/test/pi-event-filter-failure.test.sh +83 -0
  441. package/test/printf-safety-focused.test.sh +99 -0
  442. package/test/printf-safety-results/results/restoration.jsonl +10 -0
  443. package/test/printf-safety-results/results/test.jsonl +0 -0
  444. package/test/printf-safety.test.sh +297 -0
  445. package/test/validation-fix.test.sh +79 -0
  446. package/test/validation-integration.test.sh +109 -0
  447. package/tests/allowlist-glob.test.sh +61 -0
  448. package/tests/dependency-cache-key.test.sh +48 -0
  449. package/tests/dependency-restore-mode.test.sh +48 -0
  450. package/tests/doctor-template-parity.test.sh +95 -0
  451. package/tests/github-operations.test.sh +142 -0
  452. package/tests/npm-install-flags.test.sh +58 -0
  453. package/tests/quality-gates.test.sh +178 -0
  454. package/tests/repo-memory.test.sh +103 -0
  455. package/tests/restore-disallowed-changes.test.sh +80 -0
  456. package/tests/validation-missing-npm-scripts.test.sh +93 -0
  457. package/tests/validation-strict-mode.test.sh +118 -0
  458. package/tsconfig.changed.json +7 -0
  459. package/tsconfig.json +39 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Kaseki API Service - Express server wrapper
3
+ *
4
+ * Encapsulates the complete REST API service for async execution
5
+ */
6
+
7
+ import express from 'express';
8
+ import type { Server } from 'http';
9
+ import { loadConfig } from './kaseki-api-config';
10
+ import { JobScheduler } from './job-scheduler';
11
+ import { WebhookManager } from './webhook-manager';
12
+ import { IdempotencyStore } from './idempotency-store';
13
+ import { PreFlightValidator } from './pre-flight-validator';
14
+ import { createApiRouter } from './kaseki-api-routes';
15
+ import { createEventLogger } from './logger';
16
+ import { ResultCache } from './result-cache';
17
+ import { createGracefulShutdown, assertSupportedNodeVersion } from './kaseki-api-service';
18
+
19
+ interface KasekiAPIServiceOptions {
20
+ port?: number;
21
+ apiKeys?: string[];
22
+ logLevel?: string;
23
+ }
24
+
25
+ export class KasekiAPIServiceImpl {
26
+ private server: Server | null = null;
27
+ private logger = createEventLogger('kaseki-api');
28
+ private scheduler: JobScheduler | null = null;
29
+ private webhookManager: WebhookManager | null = null;
30
+ private idempotencyStore: IdempotencyStore | null = null;
31
+ private artifactCache: ResultCache | null = null;
32
+ private config = loadConfig();
33
+
34
+ constructor(options: KasekiAPIServiceOptions = {}) {
35
+ // Override config port if provided
36
+ if (options.port) {
37
+ this.config.port = options.port;
38
+ }
39
+
40
+ // Override log level if provided
41
+ if (options.logLevel) {
42
+ const validLevels: Record<string, 'debug' | 'info' | 'warn' | 'error'> = {
43
+ debug: 'debug',
44
+ info: 'info',
45
+ warn: 'warn',
46
+ error: 'error',
47
+ };
48
+ const level = validLevels[options.logLevel] || 'info';
49
+ this.config.logLevel = level;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Start the API service
55
+ */
56
+ async start(): Promise<void> {
57
+ try {
58
+ // Validate Node.js version
59
+ assertSupportedNodeVersion();
60
+
61
+ // Log startup configuration
62
+ this.logger.event('service_startup_config', {
63
+ port: this.config.port,
64
+ logLevel: this.config.logLevel,
65
+ maxConcurrentRuns: this.config.maxConcurrentRuns,
66
+ resultsDir: this.config.resultsDir,
67
+ nodeVersion: process.versions.node,
68
+ });
69
+
70
+ // Create Express app
71
+ const app = express();
72
+ app.use(express.json());
73
+
74
+ // Create managers
75
+ this.artifactCache = new ResultCache({
76
+ maxEntries: this.config.artifactCacheMaxEntries,
77
+ ttlMs: this.config.artifactCacheTtlMs,
78
+ maxFileBytes: this.config.artifactCacheMaxFileBytes,
79
+ });
80
+
81
+ this.webhookManager = new WebhookManager(this.config.resultsDir);
82
+ this.idempotencyStore = new IdempotencyStore(this.config.resultsDir, 24);
83
+ const preFlightValidator = new PreFlightValidator();
84
+
85
+ // Create scheduler
86
+ this.scheduler = new JobScheduler(
87
+ this.config,
88
+ this.webhookManager,
89
+ this.artifactCache
90
+ );
91
+
92
+ // Mount API routes
93
+ const apiRouter = createApiRouter(
94
+ this.scheduler,
95
+ this.config,
96
+ this.idempotencyStore,
97
+ preFlightValidator,
98
+ this.artifactCache
99
+ );
100
+ app.use('/api', apiRouter);
101
+ app.use('/', apiRouter);
102
+
103
+ // Start server
104
+ this.server = app.listen(this.config.port, () => {
105
+ this.logger.event('service_started', {
106
+ port: this.config.port,
107
+ logLevel: this.config.logLevel,
108
+ maxConcurrentRuns: this.config.maxConcurrentRuns,
109
+ });
110
+ console.log(`\n✓ Kaseki API service started on port ${this.config.port}`);
111
+ console.log(` Health check: http://localhost:${this.config.port}/health`);
112
+ console.log(` API routes: http://localhost:${this.config.port}/api/\n`);
113
+ });
114
+
115
+ // Setup graceful shutdown
116
+ if (this.server && this.scheduler && this.webhookManager && this.idempotencyStore) {
117
+ const gracefulShutdown = createGracefulShutdown({
118
+ server: this.server,
119
+ scheduler: this.scheduler,
120
+ webhookManager: this.webhookManager,
121
+ idempotencyStore: this.idempotencyStore,
122
+ });
123
+
124
+ process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
125
+ process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
126
+ }
127
+
128
+ // Catch unhandled errors
129
+ process.on('uncaughtException', (err) => {
130
+ this.logger.error('Uncaught exception:', {
131
+ error: String(err),
132
+ stack: err instanceof Error ? err.stack : undefined,
133
+ });
134
+ process.exit(1);
135
+ });
136
+
137
+ process.on('unhandledRejection', (reason) => {
138
+ this.logger.error('Unhandled rejection:', { reason: String(reason) });
139
+ process.exit(1);
140
+ });
141
+ } catch (error) {
142
+ this.logger.error('Failed to start service:', { error: String(error) });
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Stop the API service
149
+ */
150
+ async stop(): Promise<void> {
151
+ return new Promise((resolve) => {
152
+ if (this.server) {
153
+ this.server.close(() => {
154
+ this.logger.info('API service stopped');
155
+ resolve();
156
+ });
157
+ } else {
158
+ resolve();
159
+ }
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Get service port
165
+ */
166
+ getPort(): number {
167
+ return this.config.port;
168
+ }
169
+
170
+ /**
171
+ * Get service status
172
+ */
173
+ getStatus(): {
174
+ running: boolean;
175
+ port: number;
176
+ uptime: number;
177
+ } {
178
+ return {
179
+ running: this.server?.listening ?? false,
180
+ port: this.config.port,
181
+ uptime: process.uptime(),
182
+ };
183
+ }
184
+ }
185
+
186
+ // Export both class name styles for compatibility
187
+ export const KasekiAPIService = KasekiAPIServiceImpl;
188
+ export default KasekiAPIServiceImpl;
@@ -0,0 +1,418 @@
1
+ import * as fs from 'fs';
2
+ import type { Server } from 'http';
3
+ import { assertSupportedNodeVersion, createGracefulShutdown } from './kaseki-api-service';
4
+ import { loadConfig } from './kaseki-api-config';
5
+ import { JobScheduler } from './job-scheduler';
6
+ import { WebhookManager } from './webhook-manager';
7
+ import { RunRequestSchema } from './kaseki-api-types';
8
+
9
+ describe('Kaseki API Configuration', () => {
10
+ const originalEnv = process.env;
11
+
12
+ beforeEach(() => {
13
+ process.env = { ...originalEnv };
14
+ });
15
+
16
+ afterEach(() => {
17
+ process.env = originalEnv;
18
+ });
19
+
20
+ test('loadConfig requires KASEKI_API_KEYS or KASEKI_API_KEYS_FILE', async () => {
21
+ delete process.env.KASEKI_API_KEYS;
22
+ delete process.env.KASEKI_API_KEYS_FILE;
23
+ process.env.KASEKI_RESULTS_DIR = '/tmp';
24
+
25
+ expect(() => loadConfig()).toThrow(/KASEKI_API_KEYS.*required/i);
26
+ });
27
+
28
+ test.each([
29
+ {
30
+ name: 'rejects non-numeric port string',
31
+ port: 'invalid',
32
+ expectedError: 'KASEKI_API_PORT must be a valid port number, got: invalid',
33
+ },
34
+ {
35
+ name: 'rejects zero port',
36
+ port: '0',
37
+ expectedError: 'KASEKI_API_PORT must be a valid port number, got: 0',
38
+ },
39
+ {
40
+ name: 'rejects negative port',
41
+ port: '-1',
42
+ expectedError: 'KASEKI_API_PORT must be a valid port number, got: -1',
43
+ },
44
+ {
45
+ name: 'rejects port above 65535',
46
+ port: '65536',
47
+ expectedError: 'KASEKI_API_PORT must be a valid port number, got: 65536',
48
+ },
49
+ {
50
+ name: 'accepts minimum valid port',
51
+ port: '1',
52
+ expectedPort: 1,
53
+ },
54
+ {
55
+ name: 'accepts maximum valid port',
56
+ port: '65535',
57
+ expectedPort: 65535,
58
+ },
59
+ ])('loadConfig port boundaries: $name', ({ port, expectedError, expectedPort }) => {
60
+ process.env.KASEKI_API_KEYS = 'test-key';
61
+ process.env.KASEKI_API_PORT = port;
62
+ process.env.KASEKI_RESULTS_DIR = '/tmp';
63
+
64
+ if (expectedError) {
65
+ expect(() => loadConfig()).toThrow(expectedError);
66
+ return;
67
+ }
68
+
69
+ const config = loadConfig();
70
+ expect(config.port).toBe(expectedPort);
71
+ });
72
+
73
+ describe.each([
74
+ {
75
+ name: 'parses numeric strings and trims comma-delimited API keys',
76
+ env: {
77
+ KASEKI_API_KEYS: ' key1 , key2 , key3 ',
78
+ KASEKI_API_PORT: '3001',
79
+ KASEKI_API_MAX_CONCURRENT_RUNS: '7',
80
+ },
81
+ expectedConfig: {
82
+ port: 3001,
83
+ apiKeys: ['key1', 'key2', 'key3'],
84
+ maxConcurrentRuns: 7,
85
+ },
86
+ },
87
+ {
88
+ name: 'accepts boundary max port and keeps defaults for omitted values',
89
+ env: {
90
+ KASEKI_API_KEYS: 'solo-key',
91
+ KASEKI_API_PORT: '65535',
92
+ },
93
+ expectedConfig: {
94
+ port: 65535,
95
+ apiKeys: ['solo-key'],
96
+ maxConcurrentRuns: 3,
97
+ },
98
+ },
99
+ {
100
+ name: 'uses default port when unset and normalizes sparse key list',
101
+ env: {
102
+ KASEKI_API_KEYS: 'alpha,, beta, ,gamma ',
103
+ },
104
+ expectedConfig: {
105
+ port: 8080,
106
+ apiKeys: ['alpha', 'beta', 'gamma'],
107
+ maxConcurrentRuns: 3,
108
+ },
109
+ },
110
+ ])('loadConfig normalization: $name', ({ env, expectedConfig }) => {
111
+ delete process.env.KASEKI_API_PORT;
112
+ delete process.env.KASEKI_API_MAX_CONCURRENT_RUNS;
113
+ process.env.KASEKI_RESULTS_DIR = '/tmp';
114
+ Object.assign(process.env, env);
115
+
116
+ const config = loadConfig();
117
+
118
+ expect({
119
+ port: config.port,
120
+ apiKeys: config.apiKeys,
121
+ maxConcurrentRuns: config.maxConcurrentRuns,
122
+ }).toEqual(expectedConfig);
123
+ });
124
+
125
+ test('loadConfig parses API keys from file', async () => {
126
+ const keysFile = '/tmp/test-keys.txt';
127
+ fs.writeFileSync(keysFile, 'key1\n# comment\nkey2\n');
128
+
129
+ delete process.env.KASEKI_API_KEYS;
130
+ process.env.KASEKI_API_KEYS_FILE = keysFile;
131
+ process.env.KASEKI_RESULTS_DIR = '/tmp';
132
+
133
+ const config = loadConfig();
134
+ expect(config.apiKeys).toEqual(['key1', 'key2']);
135
+
136
+ fs.unlinkSync(keysFile);
137
+ });
138
+ });
139
+
140
+ describe('Kaseki API Request Validation', () => {
141
+ test.each([
142
+ {
143
+ name: 'accepts minimal required fields',
144
+ request: { repoUrl: 'https://github.com/org/repo', ref: 'main' },
145
+ expected: { repoUrl: 'https://github.com/org/repo', ref: 'main' },
146
+ },
147
+ {
148
+ name: 'applies default ref when omitted',
149
+ request: { repoUrl: 'https://github.com/org/repo' },
150
+ expected: { repoUrl: 'https://github.com/org/repo', ref: 'main' },
151
+ },
152
+ {
153
+ name: 'accepts startup check mode',
154
+ request: { repoUrl: 'https://github.com/org/repo', startupCheck: true },
155
+ expected: { repoUrl: 'https://github.com/org/repo', ref: 'main', startupCheck: true },
156
+ },
157
+ {
158
+ name: 'accepts explicit draft PR publishing mode',
159
+ request: { repoUrl: 'https://github.com/org/repo', publishMode: 'draft_pr' },
160
+ expected: { repoUrl: 'https://github.com/org/repo', ref: 'main', publishMode: 'draft_pr' },
161
+ },
162
+ {
163
+ name: 'accepts controller-style allowlist and validation aliases',
164
+ request: {
165
+ repoUrl: 'https://github.com/org/repo',
166
+ allowlist: { include: ['src/lib/parser.ts'] },
167
+ validation: { commands: ['npm test -- parser'] },
168
+ },
169
+ expected: {
170
+ repoUrl: 'https://github.com/org/repo',
171
+ ref: 'main',
172
+ allowlist: { include: ['src/lib/parser.ts'] },
173
+ validation: { commands: ['npm test -- parser'] },
174
+ },
175
+ },
176
+ ])('RunRequestSchema success cases: $name', ({ request, expected }) => {
177
+ const result = RunRequestSchema.parse(request);
178
+ expect(result).toMatchObject(expected);
179
+ });
180
+
181
+ test.each([
182
+ {
183
+ name: 'rejects invalid URL',
184
+ request: { repoUrl: 'not-a-url', ref: 'main' },
185
+ expectedIssue: {
186
+ path: ['repoUrl'],
187
+ messagePattern: /url/i,
188
+ value: 'not-a-url',
189
+ },
190
+ },
191
+ {
192
+ name: 'rejects invalid taskMode enum',
193
+ request: { repoUrl: 'https://github.com/org/repo', taskMode: 'invalid' },
194
+ expectedIssue: {
195
+ path: ['taskMode'],
196
+ messagePattern: /invalid (option|enum)|expected/i,
197
+ value: 'invalid',
198
+ },
199
+ },
200
+ {
201
+ name: 'rejects invalid publishMode enum',
202
+ request: { repoUrl: 'https://github.com/org/repo', publishMode: 'pr' },
203
+ expectedIssue: {
204
+ path: ['publishMode'],
205
+ messagePattern: /invalid (option|enum)|expected/i,
206
+ value: 'pr',
207
+ },
208
+ },
209
+ ])('RunRequestSchema rejects invalid payloads: $name', ({ request, expectedIssue }) => {
210
+ const result = RunRequestSchema.safeParse(request);
211
+
212
+ expect(result.success).toBe(false);
213
+ if (result.success) {
214
+ return;
215
+ }
216
+
217
+ const matchingIssue = result.error.issues.find((issue) =>
218
+ expectedIssue.path.every((segment, index) => issue.path[index] === segment),
219
+ );
220
+
221
+ expect(matchingIssue).toBeDefined();
222
+ expect(matchingIssue?.path).toEqual(expectedIssue.path);
223
+ expect(matchingIssue?.message).toMatch(expectedIssue.messagePattern);
224
+ const valueAtIssuePath = expectedIssue.path.reduce<any>((acc, segment) =>
225
+ (acc as any)?.[segment],
226
+ request as any,
227
+ );
228
+ expect(valueAtIssuePath).toBe(expectedIssue.value);
229
+ });
230
+ });
231
+
232
+ describe('Job Scheduler', () => {
233
+ let scheduler: JobScheduler;
234
+ let resultsDir: string;
235
+
236
+ beforeEach(() => {
237
+ resultsDir = fs.mkdtempSync('/tmp/kaseki-api-service-test-');
238
+ const config = {
239
+ port: 8080,
240
+ apiKeys: ['test-key'],
241
+ resultsDir,
242
+ maxConcurrentRuns: 2,
243
+ defaultTaskMode: 'patch' as const,
244
+ maxDiffBytes: 200000,
245
+ agentTimeoutSeconds: 1200,
246
+ logLevel: 'info' as const,
247
+ };
248
+
249
+ const webhookManager = new WebhookManager(resultsDir);
250
+ scheduler = new JobScheduler(config, webhookManager);
251
+ });
252
+
253
+ afterEach(() => {
254
+ scheduler.shutdown();
255
+ fs.rmSync(resultsDir, { recursive: true, force: true });
256
+ });
257
+
258
+ test('submitJob creates a queued job', async () => {
259
+ // Saturate scheduler concurrency so a newly submitted job remains queued.
260
+ (scheduler as any).running.add('existing-running-job');
261
+ (scheduler as any).running.add('second-existing-running-job');
262
+
263
+ const request = {
264
+ repoUrl: 'https://github.com/org/repo',
265
+ ref: 'main',
266
+ };
267
+
268
+ const submitted = await scheduler.submitJob(request);
269
+
270
+ // Contract outcome: job is queued when concurrency limit is reached.
271
+ expect(submitted.status).toBe('queued');
272
+ expect(submitted.request).toEqual(request);
273
+
274
+ // Contract outcome: queued job is visible via status and list/get endpoints.
275
+ expect(scheduler.getQueueStatus()).toEqual({
276
+ pending: 1,
277
+ running: 2,
278
+ maxConcurrent: 2,
279
+ });
280
+
281
+ const retrieved = scheduler.getJob(submitted.id);
282
+ const jobs = scheduler.listJobs();
283
+
284
+ expect(retrieved).toBeDefined();
285
+ expect(retrieved?.id).toBe(submitted.id);
286
+ expect(retrieved?.status).toBe('queued');
287
+ expect(jobs.some((job) => job.id === submitted.id && job.status === 'queued')).toBe(true);
288
+ });
289
+
290
+ test('submit/get/list keep job identity, request payload, and queue visibility coherent', async () => {
291
+ const request = {
292
+ repoUrl: 'https://github.com/org/repo',
293
+ ref: 'main',
294
+ };
295
+
296
+ const submitted = await scheduler.submitJob(request);
297
+ const retrieved = scheduler.getJob(submitted.id);
298
+ const jobs = scheduler.listJobs();
299
+
300
+ expect(retrieved).toBeDefined();
301
+ expect(retrieved?.id).toBe(submitted.id);
302
+ expect(retrieved?.request).toEqual(request);
303
+ expect(jobs).toHaveLength(1);
304
+ expect(jobs[0].id).toBe(submitted.id);
305
+ expect(jobs[0].request).toEqual(request);
306
+ });
307
+
308
+ test('listJobs returns all jobs sorted by creation time (newest first)', async () => {
309
+ const request1 = { repoUrl: 'https://github.com/org/repo1', ref: 'main' };
310
+ const request2 = { repoUrl: 'https://github.com/org/repo2', ref: 'main' };
311
+
312
+ await scheduler.submitJob(request1);
313
+ await scheduler.submitJob(request2);
314
+
315
+ const jobs = scheduler.listJobs();
316
+ expect(jobs.length).toBe(2);
317
+ expect(jobs[0].request.repoUrl).toBe(request2.repoUrl); // Newest first
318
+ expect(jobs[1].request.repoUrl).toBe(request1.repoUrl);
319
+ });
320
+
321
+ test('getQueueStatus reports pending and running count', async () => {
322
+ await scheduler.submitJob({ repoUrl: 'https://github.com/org/repo', ref: 'main' });
323
+
324
+ const status = scheduler.getQueueStatus();
325
+ expect(status).toEqual({
326
+ pending: 0,
327
+ running: 1,
328
+ maxConcurrent: 2,
329
+ });
330
+ });
331
+ });
332
+
333
+ describe('Kaseki API graceful shutdown', () => {
334
+ test('waits for server close before scheduler shutdown and exit', async () => {
335
+ const callOrder: string[] = [];
336
+ let closeCallback: ((err?: Error) => void) | undefined;
337
+
338
+ const server = {
339
+ close: jest.fn((cb: (err?: Error) => void) => {
340
+ closeCallback = cb;
341
+ }),
342
+ } as unknown as Server;
343
+
344
+ const scheduler = {
345
+ shutdown: jest.fn(() => {
346
+ callOrder.push('scheduler.shutdown');
347
+ }),
348
+ };
349
+
350
+ const webhookManager = {
351
+ shutdown: jest.fn(),
352
+ } as any;
353
+
354
+ const idempotencyStore = {
355
+ shutdown: jest.fn(),
356
+ } as any;
357
+
358
+ const exit = jest.fn((code: number) => {
359
+ callOrder.push(`exit:${code}`);
360
+ return undefined as never;
361
+ }) as unknown as (code: number) => never;
362
+
363
+ const gracefulShutdown = createGracefulShutdown({
364
+ server,
365
+ scheduler,
366
+ webhookManager,
367
+ idempotencyStore,
368
+ forceExitAfterMs: 1000,
369
+ exit,
370
+ });
371
+
372
+ const shutdownPromise = gracefulShutdown('SIGTERM');
373
+
374
+ expect(server.close).toHaveBeenCalledTimes(1);
375
+ expect(scheduler.shutdown).not.toHaveBeenCalled();
376
+ expect(exit).not.toHaveBeenCalled();
377
+
378
+ closeCallback?.();
379
+
380
+ await shutdownPromise;
381
+
382
+ expect(callOrder).toEqual(['scheduler.shutdown', 'exit:0']);
383
+ });
384
+ });
385
+
386
+ describe('Node runtime precheck', () => {
387
+ const originalExit = process.exit;
388
+
389
+ afterEach(() => {
390
+ process.exit = originalExit;
391
+ jest.restoreAllMocks();
392
+ });
393
+
394
+ test('allows supported Node major versions', async () => {
395
+ expect(() => assertSupportedNodeVersion('24.0.0')).not.toThrow();
396
+ expect(() => assertSupportedNodeVersion('25.1.2')).not.toThrow();
397
+ });
398
+
399
+ test.each(['x.y.z', '24.x.1', 'v24.0.0', '24.0.0-beta'])(
400
+ 'exits early for malformed Node version string %s',
401
+ (version) => {
402
+ const exitMock = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
403
+ throw new Error(`exit:${code}`);
404
+ }) as never);
405
+
406
+ expect(() => assertSupportedNodeVersion(version)).toThrow('exit:1');
407
+ expect(exitMock).toHaveBeenCalledWith(1);
408
+ },
409
+ );
410
+ test('exits early for unsupported Node major versions', async () => {
411
+ const exitMock = jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
412
+ throw new Error(`exit:${code}`);
413
+ }) as never);
414
+
415
+ expect(() => assertSupportedNodeVersion('22.22.2')).toThrow('exit:1');
416
+ expect(exitMock).toHaveBeenCalledWith(1);
417
+ });
418
+ });