@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.
- package/.dockerignore +54 -0
- package/.eslintignore +11 -0
- package/.eslintrc.json +95 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +53 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +53 -0
- package/.github/ISSUE_TEMPLATE/security.md +51 -0
- package/.github/PULL_REQUEST_TEMPLATE/default.md +71 -0
- package/.github/dependabot.yml +38 -0
- package/.github/skills/dependency-cache-optimization/SKILL.md +526 -0
- package/.github/skills/docker-image-management/SKILL.md +532 -0
- package/.github/skills/frontend-design/SKILL.md +782 -0
- package/.github/skills/prompt-engineering/SKILL.md +360 -0
- package/.github/skills/quality-gate-config/SKILL.md +591 -0
- package/.github/skills/result-report-analysis/SKILL.md +576 -0
- package/.github/skills/test-automation/SKILL.md +593 -0
- package/.github/skills/workflow-diagnosis/SKILL.md +468 -0
- package/.github/workflows/build-docker-image.yml +453 -0
- package/.github/workflows/release.yml +68 -0
- package/.releaserc.json +135 -0
- package/CHANGELOG.md +117 -0
- package/CLAUDE.md +336 -0
- package/CONTRIBUTING.md +339 -0
- package/Dockerfile +217 -0
- package/README.md +1527 -0
- package/STYLE.md +521 -0
- package/add-js-extensions.d.ts +9 -0
- package/add-js-extensions.d.ts.map +1 -0
- package/add-js-extensions.js.map +1 -0
- package/dist/add-js-extensions.d.ts +9 -0
- package/dist/add-js-extensions.d.ts.map +1 -0
- package/dist/add-js-extensions.js +52 -0
- package/dist/add-js-extensions.js.map +1 -0
- package/dist/ansi-colors.d.ts +26 -0
- package/dist/ansi-colors.d.ts.map +1 -0
- package/dist/ansi-colors.js +51 -0
- package/dist/ansi-colors.js.map +1 -0
- package/dist/cli/BaseCommand.d.ts +18 -0
- package/dist/cli/BaseCommand.d.ts.map +1 -0
- package/dist/cli/BaseCommand.js +31 -0
- package/dist/cli/BaseCommand.js.map +1 -0
- package/dist/cli/KasekiCLI.d.ts +30 -0
- package/dist/cli/KasekiCLI.d.ts.map +1 -0
- package/dist/cli/KasekiCLI.js +134 -0
- package/dist/cli/KasekiCLI.js.map +1 -0
- package/dist/cli/commands/ConfigCommand.d.ts +13 -0
- package/dist/cli/commands/ConfigCommand.d.ts.map +1 -0
- package/dist/cli/commands/ConfigCommand.js +131 -0
- package/dist/cli/commands/ConfigCommand.js.map +1 -0
- package/dist/cli/commands/DoctorCommand.d.ts +45 -0
- package/dist/cli/commands/DoctorCommand.d.ts.map +1 -0
- package/dist/cli/commands/DoctorCommand.js +309 -0
- package/dist/cli/commands/DoctorCommand.js.map +1 -0
- package/dist/cli/commands/ListCommand.d.ts +9 -0
- package/dist/cli/commands/ListCommand.d.ts.map +1 -0
- package/dist/cli/commands/ListCommand.js +81 -0
- package/dist/cli/commands/ListCommand.js.map +1 -0
- package/dist/cli/commands/ReportCommand.d.ts +9 -0
- package/dist/cli/commands/ReportCommand.d.ts.map +1 -0
- package/dist/cli/commands/ReportCommand.js +98 -0
- package/dist/cli/commands/ReportCommand.js.map +1 -0
- package/dist/cli/commands/RunCommand.d.ts +13 -0
- package/dist/cli/commands/RunCommand.d.ts.map +1 -0
- package/dist/cli/commands/RunCommand.js +191 -0
- package/dist/cli/commands/RunCommand.js.map +1 -0
- package/dist/cli/commands/SecretsCommand.d.ts +9 -0
- package/dist/cli/commands/SecretsCommand.d.ts.map +1 -0
- package/dist/cli/commands/SecretsCommand.js +109 -0
- package/dist/cli/commands/SecretsCommand.js.map +1 -0
- package/dist/cli/commands/ServeCommand.d.ts +9 -0
- package/dist/cli/commands/ServeCommand.d.ts.map +1 -0
- package/dist/cli/commands/ServeCommand.js +50 -0
- package/dist/cli/commands/ServeCommand.js.map +1 -0
- package/dist/cli/commands/SetupCommand.d.ts +42 -0
- package/dist/cli/commands/SetupCommand.d.ts.map +1 -0
- package/dist/cli/commands/SetupCommand.js +249 -0
- package/dist/cli/commands/SetupCommand.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +130 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/ConfigManager.d.ts +395 -0
- package/dist/config/ConfigManager.d.ts.map +1 -0
- package/dist/config/ConfigManager.js +446 -0
- package/dist/config/ConfigManager.js.map +1 -0
- package/dist/docker/DockerManager.d.ts +69 -0
- package/dist/docker/DockerManager.d.ts.map +1 -0
- package/dist/docker/DockerManager.js +266 -0
- package/dist/docker/DockerManager.js.map +1 -0
- package/dist/event-aggregator.d.ts +71 -0
- package/dist/event-aggregator.d.ts.map +1 -0
- package/dist/event-aggregator.js +95 -0
- package/dist/event-aggregator.js.map +1 -0
- package/dist/github-app-token.d.ts +16 -0
- package/dist/github-app-token.d.ts.map +1 -0
- package/dist/github-app-token.js +148 -0
- package/dist/github-app-token.js.map +1 -0
- package/dist/idempotency-store.d.ts +61 -0
- package/dist/idempotency-store.d.ts.map +1 -0
- package/dist/idempotency-store.js +321 -0
- package/dist/idempotency-store.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/instance/InstanceManager.d.ts +81 -0
- package/dist/instance/InstanceManager.d.ts.map +1 -0
- package/dist/instance/InstanceManager.js +220 -0
- package/dist/instance/InstanceManager.js.map +1 -0
- package/dist/instance-metadata-reader.d.ts +48 -0
- package/dist/instance-metadata-reader.d.ts.map +1 -0
- package/dist/instance-metadata-reader.js +94 -0
- package/dist/instance-metadata-reader.js.map +1 -0
- package/dist/instance-state-derivation.d.ts +42 -0
- package/dist/instance-state-derivation.d.ts.map +1 -0
- package/dist/instance-state-derivation.js +133 -0
- package/dist/instance-state-derivation.js.map +1 -0
- package/dist/job-scheduler.d.ts +124 -0
- package/dist/job-scheduler.d.ts.map +1 -0
- package/dist/job-scheduler.js +992 -0
- package/dist/job-scheduler.js.map +1 -0
- package/dist/kaseki-api-client.d.ts +89 -0
- package/dist/kaseki-api-client.d.ts.map +1 -0
- package/dist/kaseki-api-client.js +405 -0
- package/dist/kaseki-api-client.js.map +1 -0
- package/dist/kaseki-api-config.d.ts +34 -0
- package/dist/kaseki-api-config.d.ts.map +1 -0
- package/dist/kaseki-api-config.js +113 -0
- package/dist/kaseki-api-config.js.map +1 -0
- package/dist/kaseki-api-routes.d.ts +13 -0
- package/dist/kaseki-api-routes.d.ts.map +1 -0
- package/dist/kaseki-api-routes.js +559 -0
- package/dist/kaseki-api-routes.js.map +1 -0
- package/dist/kaseki-api-service-wrapper.d.ts +43 -0
- package/dist/kaseki-api-service-wrapper.d.ts.map +1 -0
- package/dist/kaseki-api-service-wrapper.js +150 -0
- package/dist/kaseki-api-service-wrapper.js.map +1 -0
- package/dist/kaseki-api-service.d.ts +16 -0
- package/dist/kaseki-api-service.d.ts.map +1 -0
- package/dist/kaseki-api-service.js +143 -0
- package/dist/kaseki-api-service.js.map +1 -0
- package/dist/kaseki-api-types.d.ts +440 -0
- package/dist/kaseki-api-types.d.ts.map +1 -0
- package/dist/kaseki-api-types.js +64 -0
- package/dist/kaseki-api-types.js.map +1 -0
- package/dist/kaseki-cli-lib.d.ts +219 -0
- package/dist/kaseki-cli-lib.d.ts.map +1 -0
- package/dist/kaseki-cli-lib.js +523 -0
- package/dist/kaseki-cli-lib.js.map +1 -0
- package/dist/kaseki-cli.d.ts +38 -0
- package/dist/kaseki-cli.d.ts.map +1 -0
- package/dist/kaseki-cli.js +559 -0
- package/dist/kaseki-cli.js.map +1 -0
- package/dist/kaseki-report.d.ts +3 -0
- package/dist/kaseki-report.d.ts.map +1 -0
- package/dist/kaseki-report.js +140 -0
- package/dist/kaseki-report.js.map +1 -0
- package/dist/lib/subprocess-helpers.d.ts +98 -0
- package/dist/lib/subprocess-helpers.d.ts.map +1 -0
- package/dist/lib/subprocess-helpers.js +136 -0
- package/dist/lib/subprocess-helpers.js.map +1 -0
- package/dist/logger.d.ts +39 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +79 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +19 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +59 -0
- package/dist/metrics.js.map +1 -0
- package/dist/middleware/job-lookup.d.ts +27 -0
- package/dist/middleware/job-lookup.d.ts.map +1 -0
- package/dist/middleware/job-lookup.js +28 -0
- package/dist/middleware/job-lookup.js.map +1 -0
- package/dist/pi-event-filter.d.ts +3 -0
- package/dist/pi-event-filter.d.ts.map +1 -0
- package/dist/pi-event-filter.js +126 -0
- package/dist/pi-event-filter.js.map +1 -0
- package/dist/pi-progress-stream.d.ts +3 -0
- package/dist/pi-progress-stream.d.ts.map +1 -0
- package/dist/pi-progress-stream.js +205 -0
- package/dist/pi-progress-stream.js.map +1 -0
- package/dist/pi-progress-summarizer.d.ts +61 -0
- package/dist/pi-progress-summarizer.d.ts.map +1 -0
- package/dist/pi-progress-summarizer.js +246 -0
- package/dist/pi-progress-summarizer.js.map +1 -0
- package/dist/pre-flight-validator.d.ts +72 -0
- package/dist/pre-flight-validator.d.ts.map +1 -0
- package/dist/pre-flight-validator.js +513 -0
- package/dist/pre-flight-validator.js.map +1 -0
- package/dist/progress-stream-utils.d.ts +3 -0
- package/dist/progress-stream-utils.d.ts.map +1 -0
- package/dist/progress-stream-utils.js +15 -0
- package/dist/progress-stream-utils.js.map +1 -0
- package/dist/result-cache.d.ts +52 -0
- package/dist/result-cache.d.ts.map +1 -0
- package/dist/result-cache.js +134 -0
- package/dist/result-cache.js.map +1 -0
- package/dist/routes/artifact-routes.d.ts +10 -0
- package/dist/routes/artifact-routes.d.ts.map +1 -0
- package/dist/routes/artifact-routes.js +126 -0
- package/dist/routes/artifact-routes.js.map +1 -0
- package/dist/routes/log-routes.d.ts +8 -0
- package/dist/routes/log-routes.d.ts.map +1 -0
- package/dist/routes/log-routes.js +345 -0
- package/dist/routes/log-routes.js.map +1 -0
- package/dist/routes/status-routes.d.ts +8 -0
- package/dist/routes/status-routes.d.ts.map +1 -0
- package/dist/routes/status-routes.js +82 -0
- package/dist/routes/status-routes.js.map +1 -0
- package/dist/routes/webhook-routes.d.ts +6 -0
- package/dist/routes/webhook-routes.d.ts.map +1 -0
- package/dist/routes/webhook-routes.js +86 -0
- package/dist/routes/webhook-routes.js.map +1 -0
- package/dist/run-artifact-metadata-cache.d.ts +42 -0
- package/dist/run-artifact-metadata-cache.d.ts.map +1 -0
- package/dist/run-artifact-metadata-cache.js +139 -0
- package/dist/run-artifact-metadata-cache.js.map +1 -0
- package/dist/secret-value-cache.d.ts +13 -0
- package/dist/secret-value-cache.d.ts.map +1 -0
- package/dist/secret-value-cache.js +44 -0
- package/dist/secret-value-cache.js.map +1 -0
- package/dist/secrets/SecretsManager.d.ts +80 -0
- package/dist/secrets/SecretsManager.d.ts.map +1 -0
- package/dist/secrets/SecretsManager.js +306 -0
- package/dist/secrets/SecretsManager.js.map +1 -0
- package/dist/test-utils.d.ts +55 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +48 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/timestamp-tracker.d.ts +75 -0
- package/dist/timestamp-tracker.d.ts.map +1 -0
- package/dist/timestamp-tracker.js +121 -0
- package/dist/timestamp-tracker.js.map +1 -0
- package/dist/utils/failure-artifact-writer.d.ts +29 -0
- package/dist/utils/failure-artifact-writer.d.ts.map +1 -0
- package/dist/utils/failure-artifact-writer.js +157 -0
- package/dist/utils/failure-artifact-writer.js.map +1 -0
- package/dist/utils/file-helpers.d.ts +41 -0
- package/dist/utils/file-helpers.d.ts.map +1 -0
- package/dist/utils/file-helpers.js +143 -0
- package/dist/utils/file-helpers.js.map +1 -0
- package/dist/utils/http-client-factory.d.ts +46 -0
- package/dist/utils/http-client-factory.d.ts.map +1 -0
- package/dist/utils/http-client-factory.js +114 -0
- package/dist/utils/http-client-factory.js.map +1 -0
- package/dist/utils/progress-normalizer.d.ts +13 -0
- package/dist/utils/progress-normalizer.d.ts.map +1 -0
- package/dist/utils/progress-normalizer.js +57 -0
- package/dist/utils/progress-normalizer.js.map +1 -0
- package/dist/utils/response-helpers.d.ts +34 -0
- package/dist/utils/response-helpers.d.ts.map +1 -0
- package/dist/utils/response-helpers.js +78 -0
- package/dist/utils/response-helpers.js.map +1 -0
- package/dist/utils/route-helpers.d.ts +17 -0
- package/dist/utils/route-helpers.d.ts.map +1 -0
- package/dist/utils/route-helpers.js +22 -0
- package/dist/utils/route-helpers.js.map +1 -0
- package/dist/utils/status-response-builder.d.ts +23 -0
- package/dist/utils/status-response-builder.d.ts.map +1 -0
- package/dist/utils/status-response-builder.js +144 -0
- package/dist/utils/status-response-builder.js.map +1 -0
- package/dist/utils/type-guards.d.ts +37 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +45 -0
- package/dist/utils/type-guards.js.map +1 -0
- package/dist/utils/utf8-helpers.d.ts +32 -0
- package/dist/utils/utf8-helpers.d.ts.map +1 -0
- package/dist/utils/utf8-helpers.js +97 -0
- package/dist/utils/utf8-helpers.js.map +1 -0
- package/dist/utils/webhook-event-builder.d.ts +26 -0
- package/dist/utils/webhook-event-builder.d.ts.map +1 -0
- package/dist/utils/webhook-event-builder.js +77 -0
- package/dist/utils/webhook-event-builder.js.map +1 -0
- package/dist/webhook-manager.d.ts +56 -0
- package/dist/webhook-manager.d.ts.map +1 -0
- package/dist/webhook-manager.js +359 -0
- package/dist/webhook-manager.js.map +1 -0
- package/docker/workspace-cache/package-lock.json +13 -0
- package/docker/workspace-cache/package.json +7 -0
- package/docker-compose.yml +53 -0
- package/docs/API.md +708 -0
- package/docs/BACKLOG.md +19 -0
- package/docs/BUILD_STRATEGY.md +404 -0
- package/docs/CLI.md +569 -0
- package/docs/DEPLOYMENT.md +521 -0
- package/docs/DEVELOPMENT.md +459 -0
- package/docs/DOCKER_SETUP.md +522 -0
- package/docs/ENHANCED_PROGRESS_LOGS.md +264 -0
- package/docs/IMPLEMENTATION_SUMMARY.md +549 -0
- package/docs/INTEGRATION_EXAMPLE.md +217 -0
- package/docs/NPM_SETUP.md +468 -0
- package/docs/PHASE1-4_IMPLEMENTATION.md +302 -0
- package/docs/PHASE1_COMPLETION.md +192 -0
- package/docs/PHASE2_COMPLETION.md +134 -0
- package/docs/PHASE6_MIGRATION.md +392 -0
- package/docs/PRINTF_SAFETY_FIX.md +282 -0
- package/docs/QUALITY_GATES.md +369 -0
- package/docs/SETUP_GUIDE.md +482 -0
- package/docs/TASK_PROMPT_TEMPLATES.md +533 -0
- package/docs/VALIDATION_FIX.md +139 -0
- package/docs/VERIFICATION_CHECKLIST.md +335 -0
- package/docs/repo-maturity.md +760 -0
- package/fix-tests.d.ts +9 -0
- package/fix-tests.d.ts.map +1 -0
- package/fix-tests.js.map +1 -0
- package/fix-tests.ts +53 -0
- package/jest.config.ts +31 -0
- package/kaseki +183 -0
- package/kaseki-agent.sh +1961 -0
- package/ops/logrotate/kaseki +10 -0
- package/package.json +83 -0
- package/perf/README.md +54 -0
- package/perf/pi-event-filter.benchmark.test.ts +98 -0
- package/run-kaseki-json.test.sh +106 -0
- package/run-kaseki.sh +990 -0
- package/scripts/allowlist-helper.sh +56 -0
- package/scripts/cleanup-kaseki.sh +168 -0
- package/scripts/deploy-pi-template.sh +293 -0
- package/scripts/docker-entrypoint.sh +71 -0
- package/scripts/dry-run-allowlist.sh +161 -0
- package/scripts/kaseki-activate.sh +396 -0
- package/scripts/kaseki-api.service +62 -0
- package/scripts/kaseki-container-entrypoint-wrapper.sh +119 -0
- package/scripts/kaseki-container-setup-remote.sh +172 -0
- package/scripts/kaseki-container-setup.sh +193 -0
- package/scripts/kaseki-healthcheck.sh +95 -0
- package/scripts/kaseki-install.sh +50 -0
- package/scripts/kaseki-maturity-score.sh +291 -0
- package/scripts/kaseki-performance-metrics.sh +122 -0
- package/scripts/kaseki-preflight.sh +270 -0
- package/scripts/kaseki-setup.sh +265 -0
- package/scripts/pi-setup-remote.sh +213 -0
- package/scripts/setup-github-labels.sh +42 -0
- package/scripts/suggest-allowlist.sh +68 -0
- package/scripts/templates/MULTI_HOST_DISTRIBUTED.md +337 -0
- package/scripts/templates/REST_API_SERVICE.md +490 -0
- package/scripts/templates/SINGLE_HOST_CLI.md +194 -0
- package/scripts/test-github-app.sh +248 -0
- package/src/add-js-extensions.ts +61 -0
- package/src/ansi-colors.test.ts +62 -0
- package/src/ansi-colors.ts +67 -0
- package/src/cli/BaseCommand.ts +40 -0
- package/src/cli/KasekiCLI.ts +154 -0
- package/src/cli/commands/ConfigCommand.ts +145 -0
- package/src/cli/commands/DoctorCommand.ts +329 -0
- package/src/cli/commands/ListCommand.ts +105 -0
- package/src/cli/commands/ReportCommand.ts +110 -0
- package/src/cli/commands/RunCommand.ts +218 -0
- package/src/cli/commands/SecretsCommand.ts +120 -0
- package/src/cli/commands/ServeCommand.ts +62 -0
- package/src/cli/commands/SetupCommand.ts +301 -0
- package/src/cli.ts +138 -0
- package/src/config/ConfigManager.ts +476 -0
- package/src/docker/DockerManager.ts +319 -0
- package/src/docker-entrypoint-packaging.test.ts +33 -0
- package/src/event-aggregator.test.ts +117 -0
- package/src/event-aggregator.ts +126 -0
- package/src/github-app-token.ts +215 -0
- package/src/idempotency-store.test.ts +117 -0
- package/src/idempotency-store.ts +385 -0
- package/src/index.ts +89 -0
- package/src/instance/InstanceManager.ts +285 -0
- package/src/instance-metadata-reader.test.ts +190 -0
- package/src/instance-metadata-reader.ts +129 -0
- package/src/instance-state-derivation.test.ts +263 -0
- package/src/instance-state-derivation.ts +148 -0
- package/src/job-scheduler.test.ts +1236 -0
- package/src/job-scheduler.ts +1117 -0
- package/src/kaseki-api-client.ts +488 -0
- package/src/kaseki-api-config.test.ts +315 -0
- package/src/kaseki-api-config.ts +175 -0
- package/src/kaseki-api-routes.test.ts +1615 -0
- package/src/kaseki-api-routes.ts +643 -0
- package/src/kaseki-api-service-wrapper.ts +188 -0
- package/src/kaseki-api-service.test.ts +418 -0
- package/src/kaseki-api-service.ts +192 -0
- package/src/kaseki-api-types.ts +320 -0
- package/src/kaseki-cli-lib.test.ts +552 -0
- package/src/kaseki-cli-lib.ts +760 -0
- package/src/kaseki-cli.ts +682 -0
- package/src/kaseki-report.test.ts +118 -0
- package/src/kaseki-report.ts +192 -0
- package/src/lib/subprocess-helpers.ts +177 -0
- package/src/logger.ts +114 -0
- package/src/metrics.ts +66 -0
- package/src/middleware/job-lookup.test.ts +113 -0
- package/src/middleware/job-lookup.ts +45 -0
- package/src/pi-event-filter.test.ts +183 -0
- package/src/pi-event-filter.ts +183 -0
- package/src/pi-progress-stream.ts +287 -0
- package/src/pi-progress-summarizer.test.ts +302 -0
- package/src/pi-progress-summarizer.ts +287 -0
- package/src/pre-flight-validator.test.ts +512 -0
- package/src/pre-flight-validator.ts +618 -0
- package/src/progress-stream-utils.test.ts +35 -0
- package/src/progress-stream-utils.ts +14 -0
- package/src/result-cache.test.ts +195 -0
- package/src/result-cache.ts +181 -0
- package/src/routes/artifact-routes.ts +169 -0
- package/src/routes/log-routes.ts +391 -0
- package/src/routes/status-routes.ts +92 -0
- package/src/routes/webhook-routes.ts +97 -0
- package/src/run-artifact-metadata-cache.test.ts +80 -0
- package/src/run-artifact-metadata-cache.ts +184 -0
- package/src/secret-value-cache.test.ts +66 -0
- package/src/secret-value-cache.ts +55 -0
- package/src/secrets/SecretsManager.ts +343 -0
- package/src/test-utils.ts +81 -0
- package/src/timestamp-tracker.test.ts +134 -0
- package/src/timestamp-tracker.ts +132 -0
- package/src/utils/failure-artifact-writer.ts +187 -0
- package/src/utils/file-helpers.test.ts +235 -0
- package/src/utils/file-helpers.ts +150 -0
- package/src/utils/http-client-factory.test.ts +245 -0
- package/src/utils/http-client-factory.ts +157 -0
- package/src/utils/progress-normalizer.test.ts +442 -0
- package/src/utils/progress-normalizer.ts +68 -0
- package/src/utils/response-helpers.test.ts +122 -0
- package/src/utils/response-helpers.ts +101 -0
- package/src/utils/route-helpers.ts +30 -0
- package/src/utils/status-response-builder.ts +159 -0
- package/src/utils/type-guards.ts +52 -0
- package/src/utils/utf8-helpers.ts +102 -0
- package/src/utils/webhook-event-builder.test.ts +143 -0
- package/src/utils/webhook-event-builder.ts +87 -0
- package/src/webhook-manager.test.ts +152 -0
- package/src/webhook-manager.ts +445 -0
- package/templates/allowlist-api-route.txt +7 -0
- package/templates/allowlist-comprehensive.txt +8 -0
- package/templates/allowlist-parser-fix.txt +6 -0
- package/templates/allowlist-ui-component.txt +9 -0
- package/templates/allowlist-utility.txt +9 -0
- package/test/actual-model-metadata.test.sh +102 -0
- package/test/dry-run.test.sh +131 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-0.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-1.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-invalid.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-0.json +1 -0
- package/test/fixtures/kaseki-report-exit-codes/metadata-exit-str-1.json +1 -0
- package/test/kaseki-api.integration.test.sh +165 -0
- package/test/pi-event-filter-failure.test.sh +83 -0
- package/test/printf-safety-focused.test.sh +99 -0
- package/test/printf-safety-results/results/restoration.jsonl +10 -0
- package/test/printf-safety-results/results/test.jsonl +0 -0
- package/test/printf-safety.test.sh +297 -0
- package/test/validation-fix.test.sh +79 -0
- package/test/validation-integration.test.sh +109 -0
- package/tests/allowlist-glob.test.sh +61 -0
- package/tests/dependency-cache-key.test.sh +48 -0
- package/tests/dependency-restore-mode.test.sh +48 -0
- package/tests/doctor-template-parity.test.sh +95 -0
- package/tests/github-operations.test.sh +142 -0
- package/tests/npm-install-flags.test.sh +58 -0
- package/tests/quality-gates.test.sh +178 -0
- package/tests/repo-memory.test.sh +103 -0
- package/tests/restore-disallowed-changes.test.sh +80 -0
- package/tests/validation-missing-npm-scripts.test.sh +93 -0
- package/tests/validation-strict-mode.test.sh +118 -0
- package/tsconfig.changed.json +7 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import {
|
|
5
|
+
isNonEmptyFile,
|
|
6
|
+
readFirstLine,
|
|
7
|
+
readTailLines,
|
|
8
|
+
readLastJsonlEvent,
|
|
9
|
+
fileExists,
|
|
10
|
+
readFileContent,
|
|
11
|
+
getFileStats,
|
|
12
|
+
commandOutput,
|
|
13
|
+
} from './file-helpers';
|
|
14
|
+
|
|
15
|
+
describe('file-helpers', () => {
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
if (fs.existsSync(tempDir)) {
|
|
24
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('isNonEmptyFile', () => {
|
|
29
|
+
it('should return true for non-empty files', () => {
|
|
30
|
+
const filePath = path.join(tempDir, 'test.txt');
|
|
31
|
+
fs.writeFileSync(filePath, 'content');
|
|
32
|
+
|
|
33
|
+
expect(isNonEmptyFile(filePath)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should keep empty-file coverage in the owning helper suite', () => {
|
|
37
|
+
const filePath = path.join(tempDir, 'empty.txt');
|
|
38
|
+
fs.writeFileSync(filePath, '');
|
|
39
|
+
|
|
40
|
+
expect(isNonEmptyFile(filePath)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return false for a missing file in the temporary test directory', () => {
|
|
44
|
+
const missingPath = path.join(tempDir, 'nonexistent.txt');
|
|
45
|
+
|
|
46
|
+
expect(fs.existsSync(tempDir)).toBe(true);
|
|
47
|
+
expect(fs.existsSync(missingPath)).toBe(false);
|
|
48
|
+
expect(isNonEmptyFile(missingPath)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('readFirstLine', () => {
|
|
53
|
+
it('should read the first line of a file', () => {
|
|
54
|
+
const filePath = path.join(tempDir, 'multiline.txt');
|
|
55
|
+
fs.writeFileSync(filePath, 'first line\nsecond line\nthird line');
|
|
56
|
+
|
|
57
|
+
expect(readFirstLine(filePath)).toBe('first line');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle single-line files', () => {
|
|
61
|
+
const filePath = path.join(tempDir, 'single.txt');
|
|
62
|
+
fs.writeFileSync(filePath, 'only line');
|
|
63
|
+
|
|
64
|
+
expect(readFirstLine(filePath)).toBe('only line');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should trim whitespace from file boundaries', () => {
|
|
68
|
+
const filePath = path.join(tempDir, 'whitespace.txt');
|
|
69
|
+
fs.writeFileSync(filePath, ' trimmed \nsecond');
|
|
70
|
+
|
|
71
|
+
// trim() applies to the whole file content, so it removes leading/trailing whitespace
|
|
72
|
+
// but the first line keeps its internal trailing spaces
|
|
73
|
+
expect(readFirstLine(filePath)).toBe('trimmed ');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should return undefined for non-existent files', () => {
|
|
77
|
+
expect(readFirstLine(path.join(tempDir, 'nonexistent.txt'))).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return undefined for empty files', () => {
|
|
81
|
+
const filePath = path.join(tempDir, 'empty.txt');
|
|
82
|
+
fs.writeFileSync(filePath, '');
|
|
83
|
+
|
|
84
|
+
expect(readFirstLine(filePath)).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('readTailLines', () => {
|
|
89
|
+
it('should return last N lines', () => {
|
|
90
|
+
const content = 'line1\nline2\nline3\nline4\nline5';
|
|
91
|
+
|
|
92
|
+
expect(readTailLines(content, 2)).toBe('line4\nline5');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return all lines if content has fewer lines than requested', () => {
|
|
96
|
+
const content = 'line1\nline2\nline3';
|
|
97
|
+
|
|
98
|
+
expect(readTailLines(content, 5)).toBe(content);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return empty string for maxLines <= 0', () => {
|
|
102
|
+
const content = 'line1\nline2\nline3';
|
|
103
|
+
|
|
104
|
+
expect(readTailLines(content, 0)).toBe('');
|
|
105
|
+
expect(readTailLines(content, -1)).toBe('');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle CRLF line endings', () => {
|
|
109
|
+
const content = 'line1\r\nline2\r\nline3\r\nline4';
|
|
110
|
+
|
|
111
|
+
// The function splits on /\r?\n/ which handles both CRLF and LF
|
|
112
|
+
// Then joins with \n, so CRLF is converted to LF in the output
|
|
113
|
+
expect(readTailLines(content, 2)).toBe('line3\nline4');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('readLastJsonlEvent', () => {
|
|
118
|
+
it('should parse a small single-line JSONL file', () => {
|
|
119
|
+
const filePath = path.join(tempDir, 'small.jsonl');
|
|
120
|
+
fs.writeFileSync(filePath, JSON.stringify({ stage: 'small', percentComplete: 10 }));
|
|
121
|
+
|
|
122
|
+
expect(readLastJsonlEvent(filePath)).toEqual({ stage: 'small', percentComplete: 10 });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should parse the last complete event from a large JSONL file without requiring the whole file', () => {
|
|
126
|
+
const filePath = path.join(tempDir, 'large.jsonl');
|
|
127
|
+
const filler = `${JSON.stringify({ stage: 'filler', message: 'x'.repeat(70000) })}\n`;
|
|
128
|
+
const lastEvent = { stage: 'large-tail', percentComplete: 99 };
|
|
129
|
+
fs.writeFileSync(filePath, `${filler}${JSON.stringify(lastEvent)}\n`);
|
|
130
|
+
|
|
131
|
+
expect(readLastJsonlEvent(filePath)).toEqual(lastEvent);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should ignore trailing newline characters and parse the previous event', () => {
|
|
135
|
+
const filePath = path.join(tempDir, 'trailing-newline.jsonl');
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
filePath,
|
|
138
|
+
`${JSON.stringify({ stage: 'first' })}\n${JSON.stringify({ stage: 'second', percentComplete: 50 })}\n\n`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(readLastJsonlEvent(filePath)).toEqual({ stage: 'second', percentComplete: 50 });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should ignore a partial final line and parse the previous complete event', () => {
|
|
145
|
+
const filePath = path.join(tempDir, 'partial-final-line.jsonl');
|
|
146
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ stage: 'complete', percentComplete: 75 })}\n{"stage":`);
|
|
147
|
+
|
|
148
|
+
expect(readLastJsonlEvent(filePath)).toEqual({ stage: 'complete', percentComplete: 75 });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return undefined for malformed JSON on the final complete line', () => {
|
|
152
|
+
const filePath = path.join(tempDir, 'malformed.jsonl');
|
|
153
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ stage: 'older' })}\n{not-json}\n`);
|
|
154
|
+
|
|
155
|
+
expect(readLastJsonlEvent(filePath)).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return undefined for missing files', () => {
|
|
159
|
+
expect(readLastJsonlEvent(path.join(tempDir, 'missing.jsonl'))).toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('fileExists', () => {
|
|
164
|
+
it('should return true for existing files', () => {
|
|
165
|
+
const filePath = path.join(tempDir, 'exists.txt');
|
|
166
|
+
fs.writeFileSync(filePath, 'content');
|
|
167
|
+
|
|
168
|
+
expect(fileExists(filePath)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return false for non-existent files', () => {
|
|
172
|
+
expect(fileExists(path.join(tempDir, 'nonexistent.txt'))).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should return true for existing directories', () => {
|
|
176
|
+
expect(fileExists(tempDir)).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('readFileContent', () => {
|
|
181
|
+
it('should read file content as text', () => {
|
|
182
|
+
const filePath = path.join(tempDir, 'content.txt');
|
|
183
|
+
fs.writeFileSync(filePath, 'hello world');
|
|
184
|
+
|
|
185
|
+
expect(readFileContent(filePath)).toBe('hello world');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return null for non-existent files', () => {
|
|
189
|
+
expect(readFileContent(path.join(tempDir, 'nonexistent.txt'))).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle empty files', () => {
|
|
193
|
+
const filePath = path.join(tempDir, 'empty.txt');
|
|
194
|
+
fs.writeFileSync(filePath, '');
|
|
195
|
+
|
|
196
|
+
expect(readFileContent(filePath)).toBe('');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('getFileStats', () => {
|
|
201
|
+
it('should return file stats for existing files', () => {
|
|
202
|
+
const filePath = path.join(tempDir, 'test.txt');
|
|
203
|
+
fs.writeFileSync(filePath, 'content');
|
|
204
|
+
|
|
205
|
+
const stats = getFileStats(filePath);
|
|
206
|
+
expect(stats).not.toBeNull();
|
|
207
|
+
expect(stats?.size).toBe('content'.length);
|
|
208
|
+
expect(stats?.isFile()).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return null for non-existent files', () => {
|
|
212
|
+
expect(getFileStats(path.join(tempDir, 'nonexistent.txt'))).toBeNull();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should work with directories', () => {
|
|
216
|
+
const stats = getFileStats(tempDir);
|
|
217
|
+
expect(stats).not.toBeNull();
|
|
218
|
+
expect(stats?.isDirectory()).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('commandOutput', () => {
|
|
223
|
+
it('returns trimmed output, undefined for unavailable output, and respects cwd', () => {
|
|
224
|
+
const runNode = (script: string, cwd?: string) =>
|
|
225
|
+
commandOutput(process.execPath, ['-e', script], cwd);
|
|
226
|
+
|
|
227
|
+
expect(runNode("console.log(' padded text ')")).toBe('padded text');
|
|
228
|
+
expect(runNode('process.exit(1)')).toBeUndefined();
|
|
229
|
+
expect(runNode('process.exit(0)')).toBeUndefined();
|
|
230
|
+
|
|
231
|
+
const cwdScript = 'console.log(process.cwd())';
|
|
232
|
+
expect(runNode(cwdScript, tempDir)).not.toBe(runNode(cwdScript));
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { commandOutput as executeCommand } from '../lib/subprocess-helpers';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a file exists and is non-empty.
|
|
6
|
+
* Used for artifact availability checks.
|
|
7
|
+
*/
|
|
8
|
+
export function isNonEmptyFile(filePath: string): boolean {
|
|
9
|
+
try {
|
|
10
|
+
return fs.statSync(filePath).size > 0;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read the first line of a file.
|
|
18
|
+
* Used for metadata extraction.
|
|
19
|
+
*/
|
|
20
|
+
export function readFirstLine(filePath: string): string | undefined {
|
|
21
|
+
try {
|
|
22
|
+
const value = fs.readFileSync(filePath, 'utf-8').trim().split(/\r?\n/)[0];
|
|
23
|
+
return value || undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the tail of a file (last N lines).
|
|
31
|
+
* Used for log truncation.
|
|
32
|
+
*/
|
|
33
|
+
export function readTailLines(content: string, maxLines: number): string {
|
|
34
|
+
if (maxLines <= 0) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lines = content.split(/\r?\n/);
|
|
39
|
+
if (lines.length <= maxLines) {
|
|
40
|
+
return content;
|
|
41
|
+
}
|
|
42
|
+
return lines.slice(-maxLines).join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the final complete non-empty JSONL event from a file tail.
|
|
47
|
+
* Reads at most maxBytes from the end of the file so large progress logs do not
|
|
48
|
+
* need to be loaded fully for status responses.
|
|
49
|
+
*/
|
|
50
|
+
export function readLastJsonlEvent(filePath: string, maxBytes = 65536): Record<string, unknown> | undefined {
|
|
51
|
+
if (maxBytes <= 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let fd: number | undefined;
|
|
56
|
+
try {
|
|
57
|
+
fd = fs.openSync(filePath, 'r');
|
|
58
|
+
const stats = fs.fstatSync(fd);
|
|
59
|
+
if (!stats.isFile() || stats.size === 0) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const bytesToRead = Math.min(stats.size, maxBytes);
|
|
64
|
+
const start = stats.size - bytesToRead;
|
|
65
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
66
|
+
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, start);
|
|
67
|
+
if (bytesRead <= 0) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let tail = buffer.subarray(0, bytesRead).toString('utf-8');
|
|
72
|
+
|
|
73
|
+
const endsWithLineBreak = /[\r\n]$/.test(tail);
|
|
74
|
+
if (!endsWithLineBreak) {
|
|
75
|
+
const lastLineBreak = Math.max(tail.lastIndexOf('\n'), tail.lastIndexOf('\r'));
|
|
76
|
+
const shouldTreatOnlyLineAsComplete = start === 0 && lastLineBreak === -1;
|
|
77
|
+
if (!shouldTreatOnlyLineAsComplete) {
|
|
78
|
+
if (lastLineBreak === -1) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
tail = tail.slice(0, lastLineBreak + 1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lines = tail.split(/\r?\n/);
|
|
86
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
87
|
+
const line = lines[index].trim();
|
|
88
|
+
if (!line) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsed = JSON.parse(line) as unknown;
|
|
93
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : undefined;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
} finally {
|
|
98
|
+
if (fd !== undefined) {
|
|
99
|
+
try {
|
|
100
|
+
fs.closeSync(fd);
|
|
101
|
+
} catch {
|
|
102
|
+
// Ignore close errors; callers treat the event as best-effort metadata.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Execute a shell command and return its output.
|
|
112
|
+
* Delegates to subprocess-helpers for consolidated subprocess handling.
|
|
113
|
+
* Used for system diagnostics (e.g., git commands, docker info).
|
|
114
|
+
*/
|
|
115
|
+
export function commandOutput(command: string, args: string[], cwd?: string): string | undefined {
|
|
116
|
+
return executeCommand(command, args, cwd);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if a file path exists.
|
|
121
|
+
*/
|
|
122
|
+
export function fileExists(filePath: string): boolean {
|
|
123
|
+
try {
|
|
124
|
+
return fs.existsSync(filePath);
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read a file's content as text, or return null if unavailable.
|
|
132
|
+
*/
|
|
133
|
+
export function readFileContent(filePath: string): string | null {
|
|
134
|
+
try {
|
|
135
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get file stats (size, modified time, etc.).
|
|
143
|
+
*/
|
|
144
|
+
export function getFileStats(filePath: string): fs.Stats | null {
|
|
145
|
+
try {
|
|
146
|
+
return fs.statSync(filePath);
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { HttpClientFactory, RetryConfig } from './http-client-factory';
|
|
2
|
+
|
|
3
|
+
describe('http-client-factory', () => {
|
|
4
|
+
describe('HttpClientFactory', () => {
|
|
5
|
+
it('should retry transient fetch failures with the default retry config', async () => {
|
|
6
|
+
jest.useFakeTimers();
|
|
7
|
+
const originalFetch = global.fetch;
|
|
8
|
+
const jsonPayload = { id: 'run-123', status: 'ok' };
|
|
9
|
+
const fetchMock = jest.fn<ReturnType<typeof fetch>, Parameters<typeof fetch>>()
|
|
10
|
+
.mockRejectedValueOnce(new Error('temporary network error'))
|
|
11
|
+
.mockRejectedValueOnce(new Error('upstream connection reset'))
|
|
12
|
+
.mockResolvedValueOnce({
|
|
13
|
+
ok: true,
|
|
14
|
+
status: 200,
|
|
15
|
+
statusText: 'OK',
|
|
16
|
+
json: jest.fn().mockResolvedValue(jsonPayload),
|
|
17
|
+
} as unknown as Response);
|
|
18
|
+
global.fetch = fetchMock;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const requestPromise = new HttpClientFactory().request(
|
|
22
|
+
'https://api.example.test/json',
|
|
23
|
+
{ method: 'GET' },
|
|
24
|
+
(data) => ({ runId: (data as typeof jsonPayload).id }),
|
|
25
|
+
'JSON request'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await Promise.resolve();
|
|
29
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
30
|
+
|
|
31
|
+
await jest.advanceTimersByTimeAsync(1000);
|
|
32
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
33
|
+
|
|
34
|
+
await jest.advanceTimersByTimeAsync(2000);
|
|
35
|
+
await expect(requestPromise).resolves.toEqual({ runId: 'run-123' });
|
|
36
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
37
|
+
expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://api.example.test/json', { method: 'GET' });
|
|
38
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://api.example.test/json', { method: 'GET' });
|
|
39
|
+
expect(fetchMock).toHaveBeenNthCalledWith(3, 'https://api.example.test/json', { method: 'GET' });
|
|
40
|
+
} finally {
|
|
41
|
+
global.fetch = originalFetch;
|
|
42
|
+
jest.useRealTimers();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should initialize with custom retry config', async () => {
|
|
47
|
+
jest.useFakeTimers();
|
|
48
|
+
const originalFetch = global.fetch;
|
|
49
|
+
const maxAttempts = 7;
|
|
50
|
+
const retryDelayMs = 25;
|
|
51
|
+
const customConfig: Partial<RetryConfig> = {
|
|
52
|
+
maxAttempts,
|
|
53
|
+
initialDelayMs: retryDelayMs,
|
|
54
|
+
maxDelayMs: retryDelayMs,
|
|
55
|
+
};
|
|
56
|
+
const fetchMock = jest.fn<ReturnType<typeof fetch>, Parameters<typeof fetch>>();
|
|
57
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
58
|
+
fetchMock.mockRejectedValueOnce(new Error(`retryable outage ${attempt}`));
|
|
59
|
+
}
|
|
60
|
+
global.fetch = fetchMock;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const requestPromise = new HttpClientFactory(customConfig).request(
|
|
64
|
+
'https://api.example.test/custom-retry',
|
|
65
|
+
{ method: 'GET' },
|
|
66
|
+
(data) => data,
|
|
67
|
+
'Custom retry request'
|
|
68
|
+
);
|
|
69
|
+
const rejectionExpectation = expect(requestPromise).rejects.toThrow('retryable outage 7');
|
|
70
|
+
|
|
71
|
+
await Promise.resolve();
|
|
72
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
73
|
+
|
|
74
|
+
for (let expectedCalls = 2; expectedCalls <= maxAttempts; expectedCalls++) {
|
|
75
|
+
await jest.advanceTimersByTimeAsync(retryDelayMs);
|
|
76
|
+
expect(fetchMock).toHaveBeenCalledTimes(expectedCalls);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await rejectionExpectation;
|
|
80
|
+
expect(fetchMock).toHaveBeenCalledTimes(maxAttempts);
|
|
81
|
+
expect(fetchMock).toHaveBeenLastCalledWith('https://api.example.test/custom-retry', { method: 'GET' });
|
|
82
|
+
} finally {
|
|
83
|
+
global.fetch = originalFetch;
|
|
84
|
+
jest.useRealTimers();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should return text payloads exactly and surface status-based requestText errors', async () => {
|
|
89
|
+
const factory = new HttpClientFactory({ maxAttempts: 3 });
|
|
90
|
+
const originalFetch = global.fetch;
|
|
91
|
+
const textPayload = 'plain text response\nwith exact whitespace and symbols: π ✓';
|
|
92
|
+
const successText = jest.fn().mockResolvedValue(textPayload);
|
|
93
|
+
const errorText = jest.fn().mockResolvedValue('not found body should not be read');
|
|
94
|
+
const fetchMock = jest.fn<ReturnType<typeof fetch>, Parameters<typeof fetch>>()
|
|
95
|
+
.mockResolvedValueOnce({
|
|
96
|
+
ok: true,
|
|
97
|
+
status: 200,
|
|
98
|
+
statusText: 'OK',
|
|
99
|
+
text: successText,
|
|
100
|
+
} as unknown as Response)
|
|
101
|
+
.mockResolvedValueOnce({
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 404,
|
|
104
|
+
statusText: 'Not Found',
|
|
105
|
+
text: errorText,
|
|
106
|
+
} as unknown as Response);
|
|
107
|
+
global.fetch = fetchMock;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await expect(
|
|
111
|
+
factory.requestText('https://api.example.test/text', { method: 'POST', body: 'raw-body' }, 'Text request')
|
|
112
|
+
).resolves.toBe(textPayload);
|
|
113
|
+
expect(successText).toHaveBeenCalledTimes(1);
|
|
114
|
+
|
|
115
|
+
await expect(
|
|
116
|
+
factory.requestText('https://api.example.test/missing-text', { method: 'GET' }, 'Missing text request')
|
|
117
|
+
).rejects.toThrow('Missing text request failed: 404');
|
|
118
|
+
expect(errorText).not.toHaveBeenCalled();
|
|
119
|
+
|
|
120
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
121
|
+
expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://api.example.test/text', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
body: 'raw-body',
|
|
124
|
+
});
|
|
125
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://api.example.test/missing-text', { method: 'GET' });
|
|
126
|
+
} finally {
|
|
127
|
+
global.fetch = originalFetch;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return blob payloads from requestBlob', async () => {
|
|
132
|
+
const factory = new HttpClientFactory({ maxAttempts: 1 });
|
|
133
|
+
const originalFetch = global.fetch;
|
|
134
|
+
const blobText = 'known blob payload';
|
|
135
|
+
const blobPayload = new Blob([blobText], { type: 'text/plain' });
|
|
136
|
+
const blobResponse = jest.fn().mockResolvedValue(blobPayload);
|
|
137
|
+
const fetchMock = jest.fn<ReturnType<typeof fetch>, Parameters<typeof fetch>>().mockResolvedValueOnce({
|
|
138
|
+
ok: true,
|
|
139
|
+
status: 200,
|
|
140
|
+
statusText: 'OK',
|
|
141
|
+
blob: blobResponse,
|
|
142
|
+
} as unknown as Response);
|
|
143
|
+
global.fetch = fetchMock;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const result = await factory.requestBlob(
|
|
147
|
+
'https://api.example.test/blob',
|
|
148
|
+
{ method: 'GET', headers: { Accept: 'text/plain' } },
|
|
149
|
+
'Blob request'
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result.type).toBe('text/plain');
|
|
153
|
+
expect(result.size).toBe(blobPayload.size);
|
|
154
|
+
await expect(result.text()).resolves.toBe(blobText);
|
|
155
|
+
expect(blobResponse).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(fetchMock).toHaveBeenCalledWith('https://api.example.test/blob', {
|
|
158
|
+
method: 'GET',
|
|
159
|
+
headers: { Accept: 'text/plain' },
|
|
160
|
+
});
|
|
161
|
+
} finally {
|
|
162
|
+
global.fetch = originalFetch;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should handle successful requests with proper parsing and JSON request error semantics', async () => {
|
|
167
|
+
const factory = new HttpClientFactory({ maxAttempts: 1 });
|
|
168
|
+
const originalFetch = global.fetch;
|
|
169
|
+
const jsonPayload = { id: 'run-123', nested: { status: 'ok' } };
|
|
170
|
+
const jsonErrorPayload = { detail: 'invalid run id' };
|
|
171
|
+
const textPayload = 'plain text response';
|
|
172
|
+
const blobPayload = new Blob(['binary response'], { type: 'application/octet-stream' });
|
|
173
|
+
const successJson = jest.fn().mockResolvedValue(jsonPayload);
|
|
174
|
+
const errorJson = jest.fn().mockResolvedValue(jsonErrorPayload);
|
|
175
|
+
const fetchMock = jest.fn<ReturnType<typeof fetch>, Parameters<typeof fetch>>()
|
|
176
|
+
.mockResolvedValueOnce({
|
|
177
|
+
ok: true,
|
|
178
|
+
status: 200,
|
|
179
|
+
statusText: 'OK',
|
|
180
|
+
json: successJson,
|
|
181
|
+
} as unknown as Response)
|
|
182
|
+
.mockResolvedValueOnce({
|
|
183
|
+
ok: false,
|
|
184
|
+
status: 400,
|
|
185
|
+
statusText: 'Bad Request',
|
|
186
|
+
json: errorJson,
|
|
187
|
+
} as unknown as Response)
|
|
188
|
+
.mockResolvedValueOnce({
|
|
189
|
+
ok: true,
|
|
190
|
+
status: 200,
|
|
191
|
+
statusText: 'OK',
|
|
192
|
+
text: jest.fn().mockResolvedValue(textPayload),
|
|
193
|
+
} as unknown as Response)
|
|
194
|
+
.mockResolvedValueOnce({
|
|
195
|
+
ok: true,
|
|
196
|
+
status: 200,
|
|
197
|
+
statusText: 'OK',
|
|
198
|
+
blob: jest.fn().mockResolvedValue(blobPayload),
|
|
199
|
+
} as unknown as Response);
|
|
200
|
+
global.fetch = fetchMock;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await expect(
|
|
204
|
+
factory.request(
|
|
205
|
+
'https://api.example.test/json',
|
|
206
|
+
{ method: 'GET' },
|
|
207
|
+
(data) => {
|
|
208
|
+
expect(data).toEqual(jsonPayload);
|
|
209
|
+
return { runId: (data as typeof jsonPayload).id, status: (data as typeof jsonPayload).nested.status };
|
|
210
|
+
},
|
|
211
|
+
'JSON request'
|
|
212
|
+
)
|
|
213
|
+
).resolves.toEqual({ runId: 'run-123', status: 'ok' });
|
|
214
|
+
expect(successJson).toHaveBeenCalledTimes(1);
|
|
215
|
+
|
|
216
|
+
await expect(
|
|
217
|
+
factory.request(
|
|
218
|
+
'https://api.example.test/json-error',
|
|
219
|
+
{ method: 'POST', body: '{"id":"missing"}' },
|
|
220
|
+
(data) => data,
|
|
221
|
+
'JSON request'
|
|
222
|
+
)
|
|
223
|
+
).rejects.toThrow('JSON request failed: invalid run id');
|
|
224
|
+
expect(errorJson).toHaveBeenCalledTimes(1);
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
factory.requestText('https://api.example.test/text', { method: 'POST', body: '{}' }, 'Text request')
|
|
228
|
+
).resolves.toBe(textPayload);
|
|
229
|
+
await expect(
|
|
230
|
+
factory.requestBlob('https://api.example.test/blob', { method: 'DELETE' }, 'Blob request')
|
|
231
|
+
).resolves.toBe(blobPayload);
|
|
232
|
+
|
|
233
|
+
expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://api.example.test/json', { method: 'GET' });
|
|
234
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://api.example.test/json-error', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
body: '{"id":"missing"}',
|
|
237
|
+
});
|
|
238
|
+
expect(fetchMock).toHaveBeenNthCalledWith(3, 'https://api.example.test/text', { method: 'POST', body: '{}' });
|
|
239
|
+
expect(fetchMock).toHaveBeenNthCalledWith(4, 'https://api.example.test/blob', { method: 'DELETE' });
|
|
240
|
+
} finally {
|
|
241
|
+
global.fetch = originalFetch;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|