@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,391 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { JobScheduler } from '../job-scheduler';
5
+ import { KasekiApiConfig } from '../kaseki-api-config';
6
+ import { LogResponse, AnalysisResponse } from '../kaseki-api-types';
7
+ import { sendErrorResponse } from '../utils/response-helpers';
8
+ import { isNonEmptyFile } from '../utils/file-helpers';
9
+ import { decodeUtf8TailSafely, tailLogByLines, readTailBytes } from '../utils/utf8-helpers';
10
+ import { getJobOrRespond } from '../utils/route-helpers';
11
+ import { normalizeProgressEvent } from '../utils/progress-normalizer';
12
+
13
+ /**
14
+ * Create log-related routes (progress, events, logs, analysis).
15
+ */
16
+ export function createLogRoutes(scheduler: JobScheduler, config: KasekiApiConfig): Router {
17
+ const router = Router();
18
+
19
+ /**
20
+ * GET /api/runs/:id/progress - Retrieve progress events (supports Server-Sent Events streaming).
21
+ */
22
+ router.get('/runs/:id/progress', (req: Request, res: Response) => {
23
+ const job = getJobOrRespond(scheduler, req.params.id, res);
24
+ if (!job) {
25
+ return;
26
+ }
27
+
28
+ // Check if client wants SSE streaming
29
+ const wantsSSE = req.query.stream === 'sse' || req.get('Accept')?.includes('text/event-stream');
30
+
31
+ if (wantsSSE) {
32
+ // Server-Sent Events streaming
33
+ res.setHeader('Content-Type', 'text/event-stream');
34
+ res.setHeader('Cache-Control', 'no-cache');
35
+ res.setHeader('Connection', 'keep-alive');
36
+
37
+ let lastEventCount = 0;
38
+ let noChangeCount = 0;
39
+ const maxNoChangeAttempts = 10; // Stop after 10 checks with no change
40
+
41
+ const sendProgressUpdate = () => {
42
+ const progressFile = path.join(config.resultsDir, job.id, 'progress.jsonl');
43
+ if (!fs.existsSync(progressFile)) {
44
+ return;
45
+ }
46
+
47
+ try {
48
+ const content = fs.readFileSync(progressFile, 'utf-8');
49
+ const lines = content.trim().length > 0 ? content.trim().split('\n') : [];
50
+
51
+ if (lines.length > lastEventCount) {
52
+ // Send new events
53
+ const newLines = lines.slice(lastEventCount);
54
+ for (const line of newLines) {
55
+ try {
56
+ const event = normalizeProgressEvent(JSON.parse(line));
57
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
58
+ } catch {
59
+ // Skip invalid JSON lines
60
+ }
61
+ }
62
+ lastEventCount = lines.length;
63
+ noChangeCount = 0;
64
+ } else if (job.status !== 'running') {
65
+ // Job is not running anymore, send final status
66
+ const currentJob = scheduler.getJob(job.id);
67
+ if (currentJob) {
68
+ res.write(
69
+ `data: ${JSON.stringify({
70
+ type: 'status',
71
+ status: currentJob.status,
72
+ elapsed: Math.round((new Date().getTime() - (currentJob.startedAt?.getTime() || 0)) / 1000),
73
+ })}\n\n`
74
+ );
75
+ }
76
+ res.end();
77
+ return;
78
+ } else {
79
+ noChangeCount++;
80
+ if (noChangeCount >= maxNoChangeAttempts) {
81
+ // No new events for a while, close connection
82
+ res.end();
83
+ return;
84
+ }
85
+ }
86
+ } catch {
87
+ // Ignore file read errors
88
+ }
89
+ };
90
+
91
+ // Send initial status
92
+ res.write(`data: ${JSON.stringify({ type: 'start', jobId: job.id, status: job.status })}\n\n`);
93
+
94
+ // Send progress updates every 2 seconds
95
+ const interval = setInterval(() => {
96
+ if (res.destroyed) {
97
+ clearInterval(interval);
98
+ return;
99
+ }
100
+ sendProgressUpdate();
101
+ }, 2000);
102
+
103
+ // Clean up on client disconnect
104
+ req.on('close', () => {
105
+ clearInterval(interval);
106
+ });
107
+
108
+ return;
109
+ }
110
+
111
+ // Regular JSONL response
112
+ const progressFile = path.join(config.resultsDir, job.id, 'progress.jsonl');
113
+ if (!fs.existsSync(progressFile)) {
114
+ const tailParam = Number(req.query.tail ?? 25);
115
+ const tail = Number.isFinite(tailParam) ? Math.max(0, Math.floor(tailParam)) : 25;
116
+ const events =
117
+ typeof scheduler.getLiveProgressEvents === 'function'
118
+ ? scheduler.getLiveProgressEvents(job.id, tail).map((event) => normalizeProgressEvent(event))
119
+ : [];
120
+ if (events.length > 0) {
121
+ return res.json({
122
+ id: job.id,
123
+ status: job.status,
124
+ events,
125
+ total: events.length,
126
+ source: 'docker-logs',
127
+ });
128
+ }
129
+ return sendErrorResponse(res, 404, 'Not Found', 'Progress file not found');
130
+ }
131
+
132
+ try {
133
+ const content = fs.readFileSync(progressFile, 'utf-8');
134
+ const lines = content.trim().length > 0 ? content.trim().split('\n') : [];
135
+ const tailParam = Number(req.query.tail ?? lines.length);
136
+ const tail = Number.isFinite(tailParam) ? Math.max(0, Math.floor(tailParam)) : lines.length;
137
+ const selectedLines = tail > 0 ? lines.slice(-tail) : [];
138
+ const events = selectedLines
139
+ .map((line) => {
140
+ try {
141
+ return normalizeProgressEvent(JSON.parse(line));
142
+ } catch {
143
+ return null;
144
+ }
145
+ })
146
+ .filter((event): event is Record<string, unknown> => event !== null);
147
+
148
+ res.json({
149
+ id: job.id,
150
+ status: job.status,
151
+ events,
152
+ total: lines.length,
153
+ });
154
+ } catch (err) {
155
+ sendErrorResponse(res, 500, 'Internal Server Error', `Failed to read progress: ${(err as Error).message}`);
156
+ }
157
+ });
158
+
159
+ /**
160
+ * GET /api/runs/:id/events - Controller-friendly event stream snapshot.
161
+ *
162
+ * This endpoint always prefers promoted progress.jsonl events, then appends
163
+ * live Docker progress while a worker is still running.
164
+ */
165
+ router.get('/runs/:id/events', (req: Request, res: Response) => {
166
+ const job = getJobOrRespond(scheduler, req.params.id, res);
167
+ if (!job) {
168
+ return;
169
+ }
170
+
171
+ const tailParam = Number(req.query.tail ?? 50);
172
+ const tail = Number.isFinite(tailParam) ? Math.max(0, Math.floor(tailParam)) : 50;
173
+ const progressFile = path.join(config.resultsDir, job.id, 'progress.jsonl');
174
+ const events: Array<Record<string, unknown>> = [];
175
+ const sources = new Set<string>();
176
+
177
+ if (fs.existsSync(progressFile) && isNonEmptyFile(progressFile)) {
178
+ try {
179
+ const lines = fs.readFileSync(progressFile, 'utf-8').trim().split('\n');
180
+ for (const line of lines) {
181
+ try {
182
+ events.push(normalizeProgressEvent(JSON.parse(line)));
183
+ } catch {
184
+ // Skip partial or malformed progress records.
185
+ }
186
+ }
187
+ sources.add('progress.jsonl');
188
+ } catch {
189
+ // Live Docker fallback below keeps the endpoint useful while a run is active.
190
+ }
191
+ }
192
+
193
+ if (job.status === 'running' && typeof scheduler.getLiveProgressEvents === 'function') {
194
+ const liveEvents = scheduler.getLiveProgressEvents(job.id, tail);
195
+ for (const event of liveEvents) {
196
+ events.push(normalizeProgressEvent(event));
197
+ }
198
+ if (liveEvents.length > 0) {
199
+ sources.add('docker-logs');
200
+ }
201
+ }
202
+
203
+ const selectedEvents = tail > 0 ? events.slice(-tail) : [];
204
+ res.json({
205
+ id: job.id,
206
+ status: job.status,
207
+ events: selectedEvents,
208
+ total: events.length,
209
+ sources: Array.from(sources),
210
+ });
211
+ });
212
+
213
+ /**
214
+ * GET /api/runs/:id/logs/:logtype - Retrieve logs.
215
+ */
216
+ router.get('/runs/:id/logs/:logtype', (req: Request, res: Response) => {
217
+ const job = getJobOrRespond(scheduler, req.params.id, res);
218
+ if (!job) {
219
+ return;
220
+ }
221
+
222
+ const logType = req.params.logtype;
223
+ const validLogTypes = ['stdout', 'stderr', 'validation', 'progress', 'quality', 'secret-scan'];
224
+
225
+ if (!validLogTypes.includes(logType)) {
226
+ return sendErrorResponse(
227
+ res,
228
+ 400,
229
+ 'Bad Request',
230
+ `Unknown log type: ${logType}. Valid types: ${validLogTypes.join(', ')}`
231
+ );
232
+ }
233
+
234
+ try {
235
+ const logFile = path.join(config.resultsDir, job.id, logType === 'stdout' ? 'stdout.log' : `${logType}.log`);
236
+
237
+ if (!fs.existsSync(logFile)) {
238
+ if (
239
+ job.status === 'running' &&
240
+ (logType === 'stdout' || logType === 'stderr' || logType === 'progress') &&
241
+ typeof scheduler.getLiveDockerLogTail === 'function'
242
+ ) {
243
+ const liveContent = scheduler.getLiveDockerLogTail(job.id, 300);
244
+ if (liveContent) {
245
+ const response: LogResponse = {
246
+ logType: logType as any,
247
+ content: liveContent,
248
+ size: Buffer.byteLength(liveContent, 'utf-8'),
249
+ };
250
+ return res.json(response);
251
+ }
252
+ }
253
+ if (logType === 'stderr' && job.status === 'failed') {
254
+ const syntheticStderr = [
255
+ '[kaseki] Synthetic stderr fallback',
256
+ `job id: ${job.id}`,
257
+ `exit code: ${job.exitCode ?? 'unknown'}`,
258
+ `failure class: ${job.failureClass ?? 'unknown'}`,
259
+ `job.error: ${job.error ?? 'unknown'}`,
260
+ 'canonical stderr.log was not generated for this failed run.',
261
+ ].join('\n');
262
+
263
+ const fallbackResponse: LogResponse = {
264
+ logType: 'stderr',
265
+ content: syntheticStderr,
266
+ size: Buffer.byteLength(syntheticStderr, 'utf-8'),
267
+ };
268
+
269
+ return res.status(200).json(fallbackResponse);
270
+ }
271
+ return sendErrorResponse(res, 404, 'Not Found', `Log file not found: ${logType}`);
272
+ }
273
+
274
+ const stat = fs.statSync(logFile);
275
+ const size = stat.size;
276
+
277
+ // For large files, just return metadata and a truncated tail
278
+ const maxSize = 1024 * 100; // 100 KB
279
+ let content = '';
280
+
281
+ if (size > maxSize) {
282
+ const truncated = readTailBytes(logFile, size, maxSize);
283
+
284
+ let tailContent = decodeUtf8TailSafely(truncated);
285
+ if (req.query.tail === 'lines') {
286
+ const lineCount = Number(req.query.lines ?? 200);
287
+ const maxLines = Number.isFinite(lineCount) ? Math.max(1, Math.floor(lineCount)) : 200;
288
+ tailContent = tailLogByLines(tailContent, maxLines);
289
+ }
290
+
291
+ content = `[... truncated, showing last ${maxSize} bytes ...]\n${tailContent}`;
292
+ } else {
293
+ content = fs.readFileSync(logFile, 'utf-8');
294
+ }
295
+
296
+ const response: LogResponse = {
297
+ logType: logType as any,
298
+ content,
299
+ size,
300
+ };
301
+
302
+ res.json(response);
303
+ } catch (err) {
304
+ sendErrorResponse(res, 500, 'Internal Server Error', `Failed to read log: ${(err as Error).message}`);
305
+ }
306
+ });
307
+
308
+ /**
309
+ * GET /api/runs/:id/analysis - Comprehensive run analysis.
310
+ */
311
+ router.get('/runs/:id/analysis', (req: Request, res: Response) => {
312
+ const job = getJobOrRespond(scheduler, req.params.id, res);
313
+ if (!job) {
314
+ return;
315
+ }
316
+
317
+ try {
318
+ const response: AnalysisResponse = {
319
+ id: job.id,
320
+ status: job.status,
321
+ createdAt: job.createdAt.toISOString(),
322
+ completedAt: job.completedAt?.toISOString(),
323
+ exitCode: job.exitCode,
324
+ failureClass: job.failureClass,
325
+ };
326
+
327
+ // Add timing
328
+ if (job.startedAt) {
329
+ const elapsed = (job.completedAt || new Date()).getTime() - job.startedAt.getTime();
330
+ response.elapsedSeconds = Math.round(elapsed / 1000);
331
+ }
332
+
333
+ // Try to read metadata
334
+ const metadataPath = path.join(config.resultsDir, job.id, 'metadata.json');
335
+ if (fs.existsSync(metadataPath)) {
336
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
337
+ response.metadata = {
338
+ model: metadata.model,
339
+ instance: metadata.instance,
340
+ repo: metadata.repo,
341
+ ref: metadata.ref,
342
+ };
343
+ }
344
+
345
+ // Try to read changed files
346
+ const changedFilesPath = path.join(config.resultsDir, job.id, 'changed-files.txt');
347
+ if (fs.existsSync(changedFilesPath)) {
348
+ const changedFiles = fs
349
+ .readFileSync(changedFilesPath, 'utf-8')
350
+ .trim()
351
+ .split('\n')
352
+ .filter((f) => f);
353
+
354
+ const diffPath = path.join(config.resultsDir, job.id, 'git.diff');
355
+ const diffSize = fs.existsSync(diffPath) ? fs.statSync(diffPath).size : 0;
356
+
357
+ response.changes = {
358
+ changedFiles,
359
+ diffSize,
360
+ };
361
+ }
362
+
363
+ // Try to read validation results
364
+ const validationPath = path.join(config.resultsDir, job.id, 'validation-timings.tsv');
365
+ if (fs.existsSync(validationPath)) {
366
+ const lines = fs.readFileSync(validationPath, 'utf-8').trim().split('\n');
367
+ const commandResults = lines
368
+ .slice(1) // Skip header
369
+ .map((line) => {
370
+ const [command, exitCode, elapsed] = line.split('\t');
371
+ return {
372
+ command,
373
+ exitCode: parseInt(exitCode, 10),
374
+ elapsed: parseInt(elapsed, 10),
375
+ };
376
+ });
377
+
378
+ response.validation = {
379
+ passed: commandResults.every((r) => r.exitCode === 0),
380
+ commandResults,
381
+ };
382
+ }
383
+
384
+ res.json(response);
385
+ } catch (err) {
386
+ sendErrorResponse(res, 500, 'Internal Server Error', `Failed to analyze run: ${(err as Error).message}`);
387
+ }
388
+ });
389
+
390
+ return router;
391
+ }
@@ -0,0 +1,92 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { JobScheduler } from '../job-scheduler';
5
+ import { DEFAULT_JOB_INDEX_MAX_ENTRIES, KasekiApiConfig } from '../kaseki-api-config';
6
+ import { Job, RunsListResponse } from '../kaseki-api-types';
7
+ import { resolveInstanceExitCode } from '../instance-state-derivation';
8
+ import { sendErrorResponse } from '../utils/response-helpers';
9
+ import { getJobOrRespond } from '../utils/route-helpers';
10
+ import { StatusResponseBuilder } from '../utils/status-response-builder';
11
+
12
+ /**
13
+ * Create status-related routes (runs list, status, cancel).
14
+ */
15
+ export function createStatusRoutes(scheduler: JobScheduler, config: KasekiApiConfig): Router {
16
+ const router = Router();
17
+ const statusBuilder = new StatusResponseBuilder(scheduler, config);
18
+
19
+ /**
20
+ * GET /api/runs - List all runs.
21
+ */
22
+ router.get('/runs', (_req: Request, res: Response) => {
23
+ const allJobs = scheduler.listJobs();
24
+
25
+ const response: RunsListResponse = {
26
+ runs: allJobs.map((job) => ({
27
+ id: job.id,
28
+ status: job.status,
29
+ createdAt: job.createdAt.toISOString(),
30
+ completedAt: job.completedAt?.toISOString(),
31
+ resultDir: job.resultDir,
32
+ exitCode: resolveJobExitCode(job, config),
33
+ failureClass: job.failureClass,
34
+ error: job.error,
35
+ })),
36
+ total: allJobs.length,
37
+ retention: {
38
+ terminalJobIndexMaxEntries: config.jobIndexMaxEntries ?? DEFAULT_JOB_INDEX_MAX_ENTRIES,
39
+ note: 'Older terminal runs may be omitted from this API index after compaction; their artifacts remain on disk under the results directory.',
40
+ },
41
+ };
42
+
43
+ res.json(response);
44
+ });
45
+
46
+ /**
47
+ * GET /api/runs/:id/status - Get run status.
48
+ */
49
+ router.get('/runs/:id/status', (req: Request, res: Response) => {
50
+ const job = getJobOrRespond(scheduler, req.params.id, res);
51
+ if (!job) {
52
+ return;
53
+ }
54
+
55
+ const response = statusBuilder.buildStatus(job);
56
+ res.json(response);
57
+ });
58
+
59
+ /**
60
+ * POST /api/runs/:id/cancel - Cancel a queued or running run.
61
+ */
62
+ router.post('/runs/:id/cancel', (req: Request, res: Response) => {
63
+ const job = scheduler.cancelJob(req.params.id);
64
+ if (!job) {
65
+ return sendErrorResponse(res, 404, 'Not Found', `Run not found: ${req.params.id}`);
66
+ }
67
+
68
+ const response = statusBuilder.buildStatus(job);
69
+ res.json(response);
70
+ });
71
+
72
+ return router;
73
+ }
74
+
75
+ function resolveJobExitCode(job: Job, config: KasekiApiConfig): number | undefined {
76
+ if (job.exitCode !== undefined && job.exitCode !== null) {
77
+ return job.exitCode;
78
+ }
79
+ if (!(job.status === 'completed' || job.status === 'failed')) {
80
+ return undefined;
81
+ }
82
+ const runDir = job.resultDir || path.join(config.resultsDir, job.id);
83
+ try {
84
+ const metadataPath = path.join(runDir, 'metadata.json');
85
+ const metadata = fs.existsSync(metadataPath)
86
+ ? JSON.parse(fs.readFileSync(metadataPath, 'utf-8'))
87
+ : {};
88
+ return resolveInstanceExitCode(runDir, metadata) ?? undefined;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
@@ -0,0 +1,97 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import * as crypto from 'crypto';
3
+ import { sendErrorResponse } from '../utils/response-helpers';
4
+ import { createEventLogger } from '../logger';
5
+
6
+ const logger = createEventLogger('api');
7
+
8
+ /**
9
+ * Create webhook-related routes.
10
+ */
11
+ export function createWebhookRoutes(): Router {
12
+ const router = Router();
13
+
14
+ /**
15
+ * POST /api/webhooks/test - Test webhook configuration.
16
+ */
17
+ router.post('/webhooks/test', async (req: Request, res: Response) => {
18
+ try {
19
+ const { url, secret } = req.body;
20
+
21
+ if (!url || typeof url !== 'string') {
22
+ return sendErrorResponse(res, 400, 'Bad Request', 'Webhook URL is required');
23
+ }
24
+
25
+ // Validate URL format
26
+ try {
27
+ new URL(url);
28
+ } catch {
29
+ return sendErrorResponse(res, 400, 'Bad Request', 'Invalid webhook URL format');
30
+ }
31
+
32
+ // Send test webhook
33
+ let statusCode: number | undefined;
34
+ let error: string | undefined;
35
+ let durationMs = 0;
36
+ const startTime = Date.now();
37
+
38
+ try {
39
+ const testPayload = {
40
+ eventType: 'webhook.test',
41
+ jobId: 'test',
42
+ timestamp: new Date().toISOString(),
43
+ data: { message: 'This is a test webhook from kaseki-agent API' },
44
+ };
45
+
46
+ // Generate HMAC signature if secret provided
47
+ let signature: string | null = null;
48
+ if (secret && typeof secret === 'string') {
49
+ const body = JSON.stringify(testPayload);
50
+ signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
51
+ }
52
+
53
+ const response = await fetch(url, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ 'X-Kaseki-Event': 'webhook.test',
58
+ 'X-Kaseki-Job-Id': 'test',
59
+ ...(signature && { 'X-Kaseki-Signature': `sha256=${signature}` }),
60
+ },
61
+ body: JSON.stringify(testPayload),
62
+ signal: AbortSignal.timeout(10000),
63
+ });
64
+
65
+ durationMs = Date.now() - startTime;
66
+ statusCode = response.status;
67
+
68
+ if (!response.ok) {
69
+ error = `HTTP ${response.status} ${response.statusText}`;
70
+ }
71
+ } catch (err) {
72
+ durationMs = Date.now() - startTime;
73
+ error = err instanceof Error ? err.message : String(err);
74
+ }
75
+
76
+ const result = {
77
+ url,
78
+ statusCode,
79
+ durationMs,
80
+ success: !error,
81
+ error,
82
+ };
83
+
84
+ logger.event('webhook_test', result);
85
+
86
+ res.json(result);
87
+ } catch (err) {
88
+ logger.event('api_error', {
89
+ path: '/webhooks/test',
90
+ error: (err as Error).message,
91
+ });
92
+ return sendErrorResponse(res, 400, 'Bad Request', (err as Error).message);
93
+ }
94
+ });
95
+
96
+ return router;
97
+ }
@@ -0,0 +1,80 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { RunArtifactMetadataCache } from './run-artifact-metadata-cache';
5
+
6
+ describe('RunArtifactMetadataCache', () => {
7
+ let resultsDir: string;
8
+
9
+ beforeEach(() => {
10
+ resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kaseki-artifact-metadata-cache-'));
11
+ });
12
+
13
+ afterEach(() => {
14
+ fs.rmSync(resultsDir, { recursive: true, force: true });
15
+ jest.restoreAllMocks();
16
+ });
17
+
18
+ test('terminal metadata requests populate and reuse a cache entry', () => {
19
+ const cache = new RunArtifactMetadataCache();
20
+ const jobDir = path.join(resultsDir, 'kaseki-terminal-cache-hit');
21
+ fs.mkdirSync(jobDir, { recursive: true });
22
+ fs.writeFileSync(path.join(jobDir, 'metadata.json'), '{}');
23
+
24
+ const first = cache.get('kaseki-terminal-cache-hit', jobDir, ['metadata.json'], true);
25
+ const second = cache.get('kaseki-terminal-cache-hit', jobDir, ['metadata.json'], true);
26
+
27
+ expect(first['metadata.json']).toEqual(second['metadata.json']);
28
+ expect(second['metadata.json']).toMatchObject({ exists: true, size: 2 });
29
+ expect(cache.getStats()).toEqual({ entries: 1 });
30
+ });
31
+
32
+ test('non-terminal metadata requests are not cached', () => {
33
+ const cache = new RunArtifactMetadataCache();
34
+ const jobDir = path.join(resultsDir, 'kaseki-running-no-cache');
35
+ const metadataPath = path.join(jobDir, 'metadata.json');
36
+ fs.mkdirSync(jobDir, { recursive: true });
37
+ fs.writeFileSync(metadataPath, '{}');
38
+
39
+ const first = cache.get('kaseki-running-no-cache', jobDir, ['metadata.json'], false);
40
+ fs.writeFileSync(metadataPath, '{"updated":true}');
41
+ const second = cache.get('kaseki-running-no-cache', jobDir, ['metadata.json'], false);
42
+
43
+ expect(first['metadata.json'].size).toBe(2);
44
+ expect(second['metadata.json'].size).toBe(16);
45
+ expect(cache.getStats()).toEqual({ entries: 0 });
46
+ });
47
+
48
+ test('terminal cache entries invalidate when artifact size and mtime change', () => {
49
+ const cache = new RunArtifactMetadataCache();
50
+ const jobDir = path.join(resultsDir, 'kaseki-terminal-cache-invalidate');
51
+ const summaryPath = path.join(jobDir, 'result-summary.md');
52
+ fs.mkdirSync(jobDir, { recursive: true });
53
+ fs.writeFileSync(summaryPath, 'one');
54
+
55
+ const first = cache.get('kaseki-terminal-cache-invalidate', jobDir, ['result-summary.md'], true);
56
+ const nextMtime = new Date(Date.now() + 10_000);
57
+ fs.writeFileSync(summaryPath, 'one plus more');
58
+ fs.utimesSync(summaryPath, nextMtime, nextMtime);
59
+ const second = cache.get('kaseki-terminal-cache-invalidate', jobDir, ['result-summary.md'], true);
60
+
61
+ expect(first['result-summary.md'].size).toBe(3);
62
+ expect(second['result-summary.md'].size).toBe(13);
63
+ expect(second['result-summary.md'].mtimeMs).not.toBe(first['result-summary.md'].mtimeMs);
64
+ expect(cache.getStats()).toEqual({ entries: 1 });
65
+ });
66
+
67
+ test('clear removes a terminal cache entry for a job result directory', () => {
68
+ const cache = new RunArtifactMetadataCache();
69
+ const jobDir = path.join(resultsDir, 'kaseki-clear-cache');
70
+ fs.mkdirSync(jobDir, { recursive: true });
71
+ fs.writeFileSync(path.join(jobDir, 'failure.json'), '{}');
72
+
73
+ cache.get('kaseki-clear-cache', jobDir, ['failure.json'], true);
74
+ expect(cache.getStats()).toEqual({ entries: 1 });
75
+
76
+ cache.clear('kaseki-clear-cache', jobDir);
77
+
78
+ expect(cache.getStats()).toEqual({ entries: 0 });
79
+ });
80
+ });