dd-trace 6.7.0 → 6.9.0

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 (272) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +14 -8
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/playwright.js +5 -5
  20. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  21. package/ci/test-optimization-validation/generated-files.js +47 -37
  22. package/ci/test-optimization-validation/generated-test-contract.js +9 -7
  23. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  24. package/ci/test-optimization-validation/literal-glob.js +52 -0
  25. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  26. package/ci/test-optimization-validation/manifest-scaffold.js +301 -52
  27. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  28. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  29. package/ci/test-optimization-validation/offline-output.js +6 -6
  30. package/ci/test-optimization-validation/package-check.js +94 -0
  31. package/ci/test-optimization-validation/plan-writer.js +183 -10
  32. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  33. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  34. package/ci/test-optimization-validation/redaction.js +18 -27
  35. package/ci/test-optimization-validation/report-writer.js +182 -48
  36. package/ci/test-optimization-validation/result-semantics.js +20 -3
  37. package/ci/test-optimization-validation/runner-command.js +41 -21
  38. package/ci/test-optimization-validation/runner-contract.js +79 -43
  39. package/ci/test-optimization-validation/safe-files.js +4 -4
  40. package/ci/test-optimization-validation/scenarios/basic-reporting.js +60 -6
  41. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  42. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  43. package/ci/test-optimization-validation/scenarios/test-management.js +2 -2
  44. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  45. package/ci/test-optimization-validation/test-output.js +2 -1
  46. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  47. package/ci/vitest-no-worker-init-setup.mjs +377 -84
  48. package/index.d.ts +175 -14
  49. package/init.js +1 -17
  50. package/initialize.mjs +11 -0
  51. package/loader-hook.mjs +6 -4
  52. package/package.json +12 -4
  53. package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
  54. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  55. package/packages/datadog-esbuild/index.js +4 -2
  56. package/packages/datadog-esbuild/src/utils.js +17 -2
  57. package/packages/datadog-instrumentations/src/ai.js +61 -34
  58. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +85 -9
  60. package/packages/datadog-instrumentations/src/cucumber.js +131 -51
  61. package/packages/datadog-instrumentations/src/dns.js +21 -2
  62. package/packages/datadog-instrumentations/src/electron.js +1 -0
  63. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  64. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  65. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  66. package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
  68. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  69. package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
  70. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  71. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  72. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +175 -0
  73. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  75. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  76. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
  77. package/packages/datadog-instrumentations/src/helpers/router-helper.js +38 -7
  78. package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
  79. package/packages/datadog-instrumentations/src/http/client.js +1 -3
  80. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  81. package/packages/datadog-instrumentations/src/jest.js +986 -246
  82. package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
  83. package/packages/datadog-instrumentations/src/mocha/main.js +109 -51
  84. package/packages/datadog-instrumentations/src/mocha/utils.js +120 -32
  85. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +44 -0
  86. package/packages/datadog-instrumentations/src/mocha/worker.js +441 -43
  87. package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
  88. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  89. package/packages/datadog-instrumentations/src/next.js +155 -23
  90. package/packages/datadog-instrumentations/src/openai.js +14 -0
  91. package/packages/datadog-instrumentations/src/pg.js +180 -6
  92. package/packages/datadog-instrumentations/src/playwright.js +176 -81
  93. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  94. package/packages/datadog-instrumentations/src/promise.js +3 -3
  95. package/packages/datadog-instrumentations/src/rhea.js +1 -1
  96. package/packages/datadog-instrumentations/src/router.js +195 -19
  97. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +567 -65
  98. package/packages/datadog-instrumentations/src/vitest-main.js +438 -54
  99. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  100. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  101. package/packages/datadog-instrumentations/src/webdriverio.js +1090 -0
  102. package/packages/datadog-instrumentations/src/when.js +3 -3
  103. package/packages/datadog-instrumentations/src/ws.js +5 -1
  104. package/packages/datadog-plugin-aerospike/src/index.js +1 -3
  105. package/packages/datadog-plugin-amqplib/src/client.js +1 -1
  106. package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
  107. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  108. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
  109. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
  110. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
  111. package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
  112. package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
  113. package/packages/datadog-plugin-child_process/src/index.js +3 -3
  114. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -11
  115. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  116. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  117. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
  118. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  119. package/packages/datadog-plugin-electron/src/net.js +10 -4
  120. package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
  121. package/packages/datadog-plugin-fs/src/index.js +0 -4
  122. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
  123. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
  124. package/packages/datadog-plugin-graphql/src/execute.js +1 -0
  125. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
  126. package/packages/datadog-plugin-http/src/client.js +3 -2
  127. package/packages/datadog-plugin-http2/src/client.js +1 -1
  128. package/packages/datadog-plugin-jest/src/index.js +3 -21
  129. package/packages/datadog-plugin-jest/src/util.js +4 -1
  130. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  131. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  132. package/packages/datadog-plugin-mocha/src/index.js +342 -6
  133. package/packages/datadog-plugin-next/src/index.js +57 -9
  134. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  135. package/packages/datadog-plugin-pg/src/index.js +65 -1
  136. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  137. package/packages/datadog-plugin-prisma/src/index.js +5 -1
  138. package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
  139. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  140. package/packages/datadog-plugin-undici/src/index.js +2 -1
  141. package/packages/datadog-plugin-vitest/src/index.js +179 -18
  142. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  143. package/packages/datadog-webpack/index.js +2 -2
  144. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  145. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  146. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
  147. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  148. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  149. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  150. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  151. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  152. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
  153. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
  154. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  155. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
  156. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
  157. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +2 -2
  158. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
  159. package/packages/dd-trace/src/appsec/index.js +48 -476
  160. package/packages/dd-trace/src/appsec/sdk/track_event.js +10 -1
  161. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  162. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  163. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  164. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
  165. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +29 -15
  166. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +19 -1
  167. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +25 -1
  168. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
  169. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
  170. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  171. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  172. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
  173. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  174. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +2 -2
  175. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  176. package/packages/dd-trace/src/config/defaults.js +14 -6
  177. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
  178. package/packages/dd-trace/src/config/helper.js +2 -0
  179. package/packages/dd-trace/src/config/index.js +31 -5
  180. package/packages/dd-trace/src/config/parsers.js +19 -1
  181. package/packages/dd-trace/src/config/remote_config.js +1 -1
  182. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  183. package/packages/dd-trace/src/datastreams/encoding.js +3 -4
  184. package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
  185. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
  186. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
  187. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
  188. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
  189. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  190. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  191. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  192. package/packages/dd-trace/src/exporters/common/url.js +3 -5
  193. package/packages/dd-trace/src/exporters/electron/index.js +4 -1
  194. package/packages/dd-trace/src/external-logger/src/index.js +1 -2
  195. package/packages/dd-trace/src/flare/index.js +2 -2
  196. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  197. package/packages/dd-trace/src/guardrails/log.js +1 -10
  198. package/packages/dd-trace/src/id.js +1 -1
  199. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
  200. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  201. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
  202. package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
  203. package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
  204. package/packages/dd-trace/src/llmobs/experiments/index.js +66 -19
  205. package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
  206. package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
  207. package/packages/dd-trace/src/llmobs/experiments/util.js +220 -0
  208. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  209. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  210. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  211. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
  212. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  213. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
  214. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  215. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  216. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  217. package/packages/dd-trace/src/llmobs/sdk.js +152 -78
  218. package/packages/dd-trace/src/llmobs/span_processor.js +9 -7
  219. package/packages/dd-trace/src/llmobs/tagger.js +1 -2
  220. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  221. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  222. package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
  223. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  224. package/packages/dd-trace/src/log/channels.js +1 -9
  225. package/packages/dd-trace/src/log/levels.js +12 -0
  226. package/packages/dd-trace/src/noop/proxy.js +2 -2
  227. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  228. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  229. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  230. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  231. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  232. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +3 -2
  233. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  234. package/packages/dd-trace/src/opentracing/span.js +1 -1
  235. package/packages/dd-trace/src/opentracing/span_context.js +1 -1
  236. package/packages/dd-trace/src/plugins/ci_plugin.js +124 -23
  237. package/packages/dd-trace/src/plugins/index.js +2 -0
  238. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  239. package/packages/dd-trace/src/plugins/tracing.js +1 -0
  240. package/packages/dd-trace/src/plugins/util/ci.js +13 -6
  241. package/packages/dd-trace/src/plugins/util/git.js +7 -5
  242. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +3 -3
  243. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  244. package/packages/dd-trace/src/plugins/util/llm.js +2 -2
  245. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  246. package/packages/dd-trace/src/plugins/util/tags.js +2 -0
  247. package/packages/dd-trace/src/plugins/util/test.js +356 -86
  248. package/packages/dd-trace/src/plugins/util/url.js +5 -5
  249. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  250. package/packages/dd-trace/src/plugins/util/web.js +24 -2
  251. package/packages/dd-trace/src/priority_sampler.js +6 -3
  252. package/packages/dd-trace/src/profiler.js +1 -1
  253. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  254. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  255. package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
  256. package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
  257. package/packages/dd-trace/src/proxy.js +1 -0
  258. package/packages/dd-trace/src/ritm.js +10 -20
  259. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  260. package/packages/dd-trace/src/sampling_rule.js +4 -2
  261. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  262. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  263. package/packages/dd-trace/src/startup-log.js +3 -0
  264. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  265. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
  266. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  267. package/packages/dd-trace/src/util.js +16 -0
  268. package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
  269. package/vendor/dist/pprof-format/index.js +1 -1
  270. package/vendor/dist/source-map/index.js +1 -1
  271. package/version.js +10 -8
  272. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -1,5 +1,10 @@
1
1
  'use strict'
2
2
 
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const { BLOCKER_CATEGORIES } = require('./blocker-category')
7
+ const { isProjectBuildArtifactPath } = require('./project-build-artifact')
3
8
  const { stripAnsi } = require('./test-output')
4
9
 
5
10
  const FILESYSTEM_PERMISSION_PATTERN = /\b(?:EACCES|EPERM|Operation not permitted|Permission denied)\b/i
@@ -8,22 +13,39 @@ const CONNECTION_REFUSED_PATTERN = /\bECONNREFUSED\b|\bconnection refused\b/i
8
13
  const NO_TESTS_FOUND_PATTERN =
9
14
  /\b(?:No test files? found|No tests? found|No test files? were found|0 tests? collected)\b/i
10
15
  const MODULE_OR_TRANSFORM_PATTERN =
11
- /\b(?:Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_PACKAGE_PATH_NOT_EXPORTED|Package subpath\b[\s\S]*\bnot defined by "exports"|Could not resolve|transform failed)\b/i
16
+ /\b(?:Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_PACKAGE_PATH_NOT_EXPORTED|Package subpath\b[\s\S]+\bnot defined by "exports"|Could not resolve|transform failed)\b/i
17
+ const BUILD_ARTIFACT_PATH_PATTERN = String.raw`(?:^|[\\/])(?:build|dist|generated)(?:[\\/]|['"])`
18
+ const MISSING_ARTIFACT_MESSAGE_PATTERN =
19
+ '(?:Cannot find (?:module|package)|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ENOENT|no such file or directory|' +
20
+ 'is not found|does not exist|could not be found)'
21
+ const BUILD_ARTIFACT_MISSING_PATTERN = new RegExp(
22
+ String.raw`(?:${MISSING_ARTIFACT_MESSAGE_PATTERN}[^\r\n]{0,500}${BUILD_ARTIFACT_PATH_PATTERN}|` +
23
+ String.raw`${BUILD_ARTIFACT_PATH_PATTERN}[^\r\n]{0,500}${MISSING_ARTIFACT_MESSAGE_PATTERN})`,
24
+ 'im'
25
+ )
26
+ const FILE_PATH_REFERENCE_PATTERN = /(?:^|[\s('"`])((?:\.{1,2}[\\/]|[\\/]|[A-Za-z]:[\\/])[^'"`\s),;]*)/g
27
+ const CUCUMBER_CONFIG_OPTION_UNSUPPORTED_PATTERN = /unknown option ['"]--config['"]/i
12
28
  const CYPRESS_BINARY_PATTERN =
13
- /(?:Cypress executable not found|Cypress binary is missing|Cypress failed to start|Please reinstall Cypress)/i
29
+ /Cypress executable not found|Cypress binary is missing|Cypress failed to start|Please reinstall Cypress/i
14
30
  const PLAYWRIGHT_BROWSER_PATTERN = new RegExp(
15
- String.raw`(?:browserType\.launch: Executable doesn't exist|` +
16
- 'Please run the following command to download new browsers|playwright install)',
31
+ String.raw`browserType\.launch: Executable doesn't exist|` +
32
+ 'Please run the following command to download new browsers|playwright install',
17
33
  'i'
18
34
  )
19
35
  const PLAYWRIGHT_BROWSER_LAUNCH_PATTERN =
20
- /(?:browserType\.launch: Failed to launch the browser process|bootstrap_check_in|MachPortRendezvous)/i
36
+ /Failed to launch the browser process|bootstrap_check_in|MachPortRendezvous/i
21
37
  const PLAYWRIGHT_BROWSER_ABORT_PATTERN =
22
38
  /(?:browserType\.launch: Target page, context or browser has been closed|Browser logs:)[\s\S]*?(?:signal=SIGABRT|Received signal 6|Abort trap: 6)/i
39
+ const PUPPETEER_BROWSER_PATTERN =
40
+ /(?:Could not find (?:Chrome|Chromium)|Failed to launch the browser process|Troubleshooting:\s*https:\/\/pptr\.dev)/i
41
+ const PUPPETEER_BROWSER_ABORT_PATTERN =
42
+ /Failed to launch the browser process[\s\S]*?(?:signal=SIGABRT|Received signal 6|Abort trap: 6)/i
43
+ const CUCUMBER_BROWSER_FAILURE_PATTERN =
44
+ /(?:(?:browser|puppeteer|playwright|webdriver)[^\r\n]{0,160}\b(?:aborted|closed|crashed|failed|terminated|unavailable)\b|(?:Failed|Unable) to (?:connect to|launch|start) (?:the )?browser)/i
23
45
  const VITEST_BROWSER_PROVIDER_PATTERN =
24
- /(?:Cannot find (?:module|package).*@vitest\/browser|@vitest\/browser-[^\s'"]+.*(?:missing|not (?:found|installed)))/i
46
+ /Cannot find (?:module|package).*@vitest\/browser|@vitest\/browser-[^\s'"].*(?:missing|not (?:found|installed))/i
25
47
  const RUNNER_COMMAND_NOT_FOUND_PATTERN =
26
- /(?:command not found|is not recognized as an internal or external command|spawn [^\r\n]+ ENOENT)/i
48
+ /command not found|is not recognized as an internal or external command|spawn [^\r\n]+ ENOENT/i
27
49
 
28
50
  /**
29
51
  * Identifies toolchain and execution-environment failures that happen before tests start.
@@ -34,6 +56,7 @@ const RUNNER_COMMAND_NOT_FOUND_PATTERN =
34
56
  * @param {object} [options] classification options
35
57
  * @param {boolean} [options.browserRequired] whether the selected runner uses browser mode
36
58
  * @param {string} [options.framework] test framework identifier
59
+ * @param {string} [options.packageJson] approval-bound project package.json
37
60
  * @param {boolean} [options.testsRan] whether reliable test output was observed
38
61
  * @returns {object|undefined} structured blocker diagnosis
39
62
  */
@@ -41,6 +64,7 @@ function getCommandBlocker (result, options = {}) {
41
64
  const output = `${result.stdout || ''}\n${result.stderr || ''}`
42
65
  if (Array.isArray(result.missingRequiredEnvVars) && result.missingRequiredEnvVars.length > 0) {
43
66
  return {
67
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
44
68
  kind: 'project-command-environment-missing',
45
69
  summary: 'The selected project test command requires approved non-secret environment variables that are not ' +
46
70
  'available in the validator process. No Test Optimization conclusion was reached.',
@@ -50,14 +74,29 @@ function getCommandBlocker (result, options = {}) {
50
74
  toolchainBlocked: true,
51
75
  }
52
76
  }
77
+ if (options.framework === 'cucumber' && result.exitCode !== 0 &&
78
+ CUCUMBER_CONFIG_OPTION_UNSUPPORTED_PATTERN.test(output)) {
79
+ return {
80
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
81
+ kind: 'cucumber-config-isolation-unsupported',
82
+ summary: 'The installed Cucumber CLI rejected the validator-owned configuration-isolation option. This is a ' +
83
+ 'validator compatibility limitation, not a project test or Test Optimization failure.',
84
+ recommendation: 'Report this Cucumber version to validator engineering. Use the static CI audit and do not ' +
85
+ 'change the customer profile, command, or working directory to bypass this limitation.',
86
+ signals: getMatchingLines(output, CUCUMBER_CONFIG_OPTION_UNSUPPORTED_PATTERN),
87
+ validatorBlocked: true,
88
+ }
89
+ }
53
90
  if (LOCAL_SOCKET_PATTERN.test(output) && FILESYSTEM_PERMISSION_PATTERN.test(output)) {
54
91
  return {
92
+ blockerCategory: BLOCKER_CATEGORIES.EXECUTION_ENVIRONMENT_BLOCKED,
55
93
  kind: 'local-test-socket-blocked',
56
94
  summary: 'The selected project test could not start its localhost listener in this execution environment. ' +
57
95
  'The offline Datadog validator did not open this socket. No Test Optimization conclusion was reached.',
58
96
  recommendation: 'Run the same approved plan in an environment that permits the project test to use its ' +
59
- 'required localhost socket. This may require normal project-test permissions. Do not request broader ' +
60
- 'permissions automatically or interpret this as a Test Optimization failure.',
97
+ 'required localhost socket. Use the unchanged command and SHA from execution-plan.md. This may require ' +
98
+ 'normal project-test permissions. Do not request broader permissions automatically or interpret this as a ' +
99
+ 'Test Optimization failure.',
61
100
  signals: getMatchingLines(
62
101
  output,
63
102
  /127\.0\.0\.1|localhost|listen|EACCES|EPERM|Operation not permitted|Permission denied/i
@@ -68,6 +107,7 @@ function getCommandBlocker (result, options = {}) {
68
107
  if (options.framework === 'cypress' && result.exitCode !== 0 &&
69
108
  CONNECTION_REFUSED_PATTERN.test(output) && LOCAL_SOCKET_PATTERN.test(output)) {
70
109
  return {
110
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
71
111
  kind: 'cypress-application-unavailable',
72
112
  summary: 'The selected Cypress spec could not connect to its localhost application. Discovery does not start ' +
73
113
  'customer services, so no Test Optimization conclusion was reached.',
@@ -79,6 +119,7 @@ function getCommandBlocker (result, options = {}) {
79
119
  }
80
120
  if (options.testsRan !== true && NO_TESTS_FOUND_PATTERN.test(output)) {
81
121
  return {
122
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
82
123
  kind: 'no-tests-collected',
83
124
  summary: 'The selected representative was not collected by the project test runner. No Test Optimization ' +
84
125
  'conclusion was reached.',
@@ -91,21 +132,24 @@ function getCommandBlocker (result, options = {}) {
91
132
  }
92
133
 
93
134
  const browserMode = options.framework === 'playwright' ||
135
+ (options.framework === 'cucumber' && options.browserRequired === true) ||
94
136
  (options.framework === 'vitest' && options.browserRequired === true)
95
137
  if (browserMode &&
96
138
  result.exitCode !== 0 &&
97
139
  PLAYWRIGHT_BROWSER_LAUNCH_PATTERN.test(output) && FILESYSTEM_PERMISSION_PATTERN.test(output)) {
98
- const runner = options.framework === 'vitest' ? 'Vitest browser mode' : 'Playwright'
140
+ const runner = getBrowserRunnerName(options.framework)
99
141
  return {
142
+ blockerCategory: BLOCKER_CATEGORIES.EXECUTION_ENVIRONMENT_BLOCKED,
100
143
  kind: `${options.framework}-browser-launch-blocked`,
101
- summary: `${runner} needs to launch the project browser, but the current agent sandbox denied that launch. ` +
144
+ summary: `${runner} needs to launch the project browser, but the current execution environment denied that ` +
145
+ 'launch. ' +
102
146
  'No Test Optimization conclusion was reached.',
103
147
  recommendation: 'Retry the same approved plan from a host shell or another environment where the installed ' +
104
- 'project browser can launch. Do not request broader permissions automatically or change the command, ' +
105
- 'approval file, or approval SHA.',
148
+ 'project browser can launch. Use the unchanged command and SHA from execution-plan.md. Do not request ' +
149
+ 'broader permissions automatically or change the command, approval file, or approval SHA.',
106
150
  signals: getMatchingLines(
107
151
  output,
108
- /browserType\.launch|bootstrap_check_in|MachPortRendezvous|EACCES|EPERM|Operation not permitted|Permission denied/i
152
+ /Failed to launch the browser process|bootstrap_check_in|MachPortRendezvous|EACCES|EPERM|Operation not permitted|Permission denied/i
109
153
  ),
110
154
  blockedByExecutionEnvironment: true,
111
155
  }
@@ -114,6 +158,7 @@ function getCommandBlocker (result, options = {}) {
114
158
  if (options.framework === 'playwright' && result.exitCode !== 0 &&
115
159
  PLAYWRIGHT_BROWSER_ABORT_PATTERN.test(stripAnsi(output))) {
116
160
  return {
161
+ blockerCategory: BLOCKER_CATEGORIES.CLEAN_TEST_FAILED,
117
162
  kind: 'playwright-browser-process-aborted',
118
163
  summary: 'A browser launched by Playwright aborted before the selected tests could produce reliable results. ' +
119
164
  'The available evidence does not identify whether the browser/runtime setup, the project, the execution ' +
@@ -129,9 +174,28 @@ function getCommandBlocker (result, options = {}) {
129
174
  }
130
175
  }
131
176
 
177
+ if (options.framework === 'cucumber' && options.browserRequired === true && result.exitCode !== 0 &&
178
+ PUPPETEER_BROWSER_ABORT_PATTERN.test(stripAnsi(output))) {
179
+ return {
180
+ blockerCategory: BLOCKER_CATEGORIES.CLEAN_TEST_FAILED,
181
+ kind: 'cucumber-browser-process-aborted',
182
+ summary: 'A browser launched by the selected Cucumber support code aborted before a reliable scenario result ' +
183
+ 'was observed. No Test Optimization conclusion was reached.',
184
+ recommendation: 'Run the same approved plan where the project\'s normal Cucumber browser tests can launch. ' +
185
+ 'Collect the browser and operating-system crash diagnostics if it still fails; do not treat this result as ' +
186
+ 'a Test Optimization failure.',
187
+ signals: getMatchingLines(
188
+ output,
189
+ /Failed to launch the browser process|signal=SIGABRT|Received signal 6|Abort trap: 6/i
190
+ ),
191
+ localRuntimeBlocked: true,
192
+ }
193
+ }
194
+
132
195
  if (options.framework === 'cypress' && result.exitCode !== 0 &&
133
196
  options.testsRan !== true && CYPRESS_BINARY_PATTERN.test(output)) {
134
197
  return {
198
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
135
199
  kind: 'cypress-runtime-missing',
136
200
  summary: 'The Cypress npm package is installed, but its application binary is missing or could not start. ' +
137
201
  'No Test Optimization conclusion was reached.',
@@ -143,17 +207,24 @@ function getCommandBlocker (result, options = {}) {
143
207
  }
144
208
 
145
209
  if (browserMode && result.exitCode !== 0 && options.testsRan !== true &&
146
- PLAYWRIGHT_BROWSER_PATTERN.test(output)) {
210
+ (PLAYWRIGHT_BROWSER_PATTERN.test(output) || PUPPETEER_BROWSER_PATTERN.test(output))) {
147
211
  const vitestBrowser = options.framework === 'vitest' && options.browserRequired === true
212
+ const cucumberBrowser = options.framework === 'cucumber'
213
+ const browserPattern = cucumberBrowser ? PUPPETEER_BROWSER_PATTERN : PLAYWRIGHT_BROWSER_PATTERN
148
214
  return {
149
- kind: vitestBrowser ? 'vitest-browser-provider-missing' : 'playwright-browser-missing',
150
- summary: `${vitestBrowser ? 'Vitest browser mode' : 'Playwright Test'} is installed, but the selected project ` +
215
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
216
+ kind: vitestBrowser
217
+ ? 'vitest-browser-provider-missing'
218
+ : cucumberBrowser
219
+ ? 'cucumber-browser-missing'
220
+ : 'playwright-browser-missing',
221
+ summary: `${getBrowserRunnerName(options.framework)} is installed, but the selected project ` +
151
222
  'test requires a browser binary or provider that is not installed. ' +
152
223
  'No Test Optimization conclusion was reached.',
153
- recommendation: `Complete the project's normal ${vitestBrowser ? 'Vitest browser' : 'Playwright browser'} ` +
154
- 'setup, then render and approve a fresh ' +
224
+ recommendation: `Complete the project's normal ${getBrowserRunnerName(options.framework)} setup, then render ` +
225
+ 'and approve a fresh ' +
155
226
  'validation plan. The validator does not download browsers automatically.',
156
- signals: getMatchingLines(output, PLAYWRIGHT_BROWSER_PATTERN),
227
+ signals: getMatchingLines(output, browserPattern),
157
228
  toolchainBlocked: true,
158
229
  }
159
230
  }
@@ -161,6 +232,7 @@ function getCommandBlocker (result, options = {}) {
161
232
  if (result.exitCode !== 0 && options.testsRan !== true && options.framework === 'vitest' &&
162
233
  options.browserRequired === true && VITEST_BROWSER_PROVIDER_PATTERN.test(output)) {
163
234
  return {
235
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
164
236
  kind: 'vitest-browser-provider-missing',
165
237
  summary: 'Vitest browser mode could not load its configured browser provider. Local Test Optimization ' +
166
238
  'compatibility was not tested. No Test Optimization conclusion was reached.',
@@ -173,6 +245,7 @@ function getCommandBlocker (result, options = {}) {
173
245
 
174
246
  if (result.exitCode !== 0 && options.testsRan !== true && RUNNER_COMMAND_NOT_FOUND_PATTERN.test(output)) {
175
247
  return {
248
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
176
249
  kind: 'test-runner-command-missing',
177
250
  summary: 'The selected project test command could not find its test-runner executable, so local Test ' +
178
251
  'Optimization compatibility was not tested. No Test Optimization conclusion was reached.',
@@ -183,8 +256,25 @@ function getCommandBlocker (result, options = {}) {
183
256
  }
184
257
  }
185
258
 
259
+ const projectRoot = typeof options.packageJson === 'string' ? path.dirname(options.packageJson) : undefined
260
+ if (result.exitCode !== 0 && hasMissingProjectBuildArtifact(output, projectRoot)) {
261
+ const buildScript = getBuildScript(options.packageJson)
262
+ return {
263
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
264
+ kind: 'project-build-artifact-missing',
265
+ summary: 'The selected test requires a conventional build or generated output that is missing. The validator ' +
266
+ 'does not run project build commands, so no Test Optimization conclusion was reached.',
267
+ recommendation: `Complete the project's normal build workflow${
268
+ buildScript ? `; package.json declares "build": ${JSON.stringify(buildScript)}` : ''
269
+ }. Confirm the selected test passes normally, then render and approve a fresh validation plan.`,
270
+ signals: getMatchingLines(output, BUILD_ARTIFACT_MISSING_PATTERN),
271
+ blockedByProjectSetup: true,
272
+ }
273
+ }
274
+
186
275
  if (result.exitCode !== 0 && options.testsRan !== true && MODULE_OR_TRANSFORM_PATTERN.test(output)) {
187
276
  return {
277
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
188
278
  kind: 'project-command-initialization-failed',
189
279
  summary: 'The selected project test command failed during module resolution, transformation, or runner ' +
190
280
  'initialization before a reliable test result was observed. No Test Optimization conclusion was reached.',
@@ -197,6 +287,7 @@ function getCommandBlocker (result, options = {}) {
197
287
 
198
288
  if (options.framework === 'cypress' && result.exitCode === 134 && options.testsRan !== true) {
199
289
  return {
290
+ blockerCategory: BLOCKER_CATEGORIES.CLEAN_TEST_FAILED,
200
291
  kind: 'cypress-process-aborted',
201
292
  summary: 'Cypress exited with code 134 before any test result was observed. The available evidence does not ' +
202
293
  'identify whether Cypress, its browser/runtime setup, the project, or the execution environment caused the ' +
@@ -211,6 +302,54 @@ function getCommandBlocker (result, options = {}) {
211
302
  localRuntimeBlocked: true,
212
303
  }
213
304
  }
305
+
306
+ if (options.framework === 'cucumber' && options.browserRequired === true &&
307
+ result.exitCode !== 0 && options.testsRan !== true &&
308
+ CUCUMBER_BROWSER_FAILURE_PATTERN.test(output)) {
309
+ return {
310
+ blockerCategory: BLOCKER_CATEGORIES.CLEAN_TEST_FAILED,
311
+ kind: 'cucumber-browser-execution-incomplete',
312
+ summary: 'The selected Cucumber scenario uses browser-backed support code and exited before a reliable ' +
313
+ 'scenario result was observed. A secondary Cucumber formatter exception is not enough to identify the root ' +
314
+ 'cause. No Test Optimization conclusion was reached.',
315
+ recommendation: 'Run the same approved plan where the project\'s normal Cucumber browser tests can launch, ' +
316
+ 'then create a fresh plan. If it still fails, preserve the earliest browser or project error rather than ' +
317
+ 'treating a later formatter exception as the cause.',
318
+ signals: getMatchingLines(
319
+ output,
320
+ /Failed to launch|Received signal|TypeError|Cannot read|browser|puppeteer|playwright/i
321
+ ),
322
+ localRuntimeBlocked: true,
323
+ }
324
+ }
325
+ }
326
+
327
+ function hasMissingProjectBuildArtifact (output, projectRoot) {
328
+ for (const line of output.split(/\r?\n/)) {
329
+ if (!BUILD_ARTIFACT_MISSING_PATTERN.test(line)) continue
330
+ for (const match of line.matchAll(FILE_PATH_REFERENCE_PATTERN)) {
331
+ if (isProjectBuildArtifactPath(match[1], projectRoot)) return true
332
+ }
333
+ }
334
+ return false
335
+ }
336
+
337
+ function getBuildScript (packageJson) {
338
+ if (typeof packageJson !== 'string') return
339
+ try {
340
+ const stat = fs.statSync(packageJson)
341
+ if (!stat.isFile() || stat.size > 512 * 1024) return
342
+ const script = JSON.parse(fs.readFileSync(packageJson, 'utf8'))?.scripts?.build
343
+ if (typeof script !== 'string' || !script.trim() || Buffer.byteLength(script) > 500 ||
344
+ /[\0\r\n]/.test(script)) return
345
+ return script.trim()
346
+ } catch {}
347
+ }
348
+
349
+ function getBrowserRunnerName (framework) {
350
+ if (framework === 'cucumber') return 'Cucumber browser support'
351
+ if (framework === 'vitest') return 'Vitest browser mode'
352
+ return 'Playwright Test'
214
353
  }
215
354
 
216
355
  /**
@@ -3,6 +3,8 @@
3
3
  const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
 
6
+ const { createValidationBlocker } = require('./validation-blocker')
7
+
6
8
  /**
7
9
  * Returns command-created paths that must be declared and removed after validation.
8
10
  *
@@ -32,10 +34,14 @@ function prepareCommandOutputs ({ command, artifactRoot, repositoryRoot }) {
32
34
  }
33
35
  for (const outputPath of outputPaths) {
34
36
  if (pathExists(outputPath)) {
35
- throw new Error(
37
+ throw createValidationBlocker(
36
38
  `Command output path already exists and will not be moved or overwritten: ${outputPath}. ` +
37
- 'The validator will not delete pre-existing output. Inspect and remove it manually, or choose a fresh ' +
38
- 'output path, then render a new approval plan.'
39
+ 'The validator will not delete pre-existing output.',
40
+ {
41
+ kind: 'command-output-exists',
42
+ recommendation: `Inspect ${outputPath}. Remove it manually only if it is disposable validator or test ` +
43
+ 'output, or choose a fresh output path, then render and approve a fresh plan.',
44
+ }
39
45
  )
40
46
  }
41
47
  states.push({
@@ -71,21 +77,21 @@ function cleanupCommandOutputs (states) {
71
77
  *
72
78
  * @param {string} outputPath output path
73
79
  * @param {string} repositoryRoot repository root
74
- * @returns {{path: string, dev: number, ino: number}[]} parent identities
80
+ * @returns {{path: string, dev: bigint, ino: bigint}[]} parent identities
75
81
  */
76
82
  function captureExistingParentIdentities (outputPath, repositoryRoot) {
77
83
  const identities = []
78
84
  const relative = path.relative(repositoryRoot, path.dirname(outputPath))
79
85
  let current = repositoryRoot
80
86
  for (const segment of relative ? relative.split(path.sep) : []) {
81
- const stat = fs.lstatSync(current)
87
+ const stat = fs.lstatSync(current, { bigint: true })
82
88
  assertRegularDirectory(stat, current)
83
89
  identities.push({ path: current, dev: stat.dev, ino: stat.ino })
84
90
  current = path.join(current, segment)
85
91
  if (!pathExists(current)) return identities
86
92
  }
87
93
 
88
- const stat = fs.lstatSync(current)
94
+ const stat = fs.lstatSync(current, { bigint: true })
89
95
  assertRegularDirectory(stat, current)
90
96
  identities.push({ path: current, dev: stat.dev, ino: stat.ino })
91
97
  return identities
@@ -98,7 +104,7 @@ function captureExistingParentIdentities (outputPath, repositoryRoot) {
98
104
  */
99
105
  function assertOutputParentsUnchanged (state) {
100
106
  for (const identity of state.parentIdentities) {
101
- const stat = fs.lstatSync(identity.path)
107
+ const stat = fs.lstatSync(identity.path, { bigint: true })
102
108
  assertRegularDirectory(stat, identity.path)
103
109
  if (stat.dev !== identity.dev || stat.ino !== identity.ino) {
104
110
  throw new Error(`Refusing command output cleanup because a parent directory changed: ${identity.path}`)
@@ -118,7 +124,7 @@ function assertOutputParentsUnchanged (state) {
118
124
  /**
119
125
  * Refuses symbolic links and non-directory parent components.
120
126
  *
121
- * @param {fs.Stats} stat path status
127
+ * @param {fs.Stats | fs.BigIntStats} stat path status
122
128
  * @param {string} directory directory path
123
129
  */
124
130
  function assertRegularDirectory (stat, directory) {
@@ -522,7 +522,7 @@ function buildOfflineCaptureEnv ({ fixture, outputRoot }) {
522
522
  * @param {object} input offline validation inputs
523
523
  * @param {{manifestPath: string}} input.fixture authoritative cache fixture
524
524
  * @param {string} input.outputRoot pre-created payload output root
525
- * @returns {NodeJS.ProcessEnv} validation transport environment
525
+ * @returns {typeof process.env} validation transport environment
526
526
  */
527
527
  function buildOfflineValidationEnv ({ fixture, outputRoot }) {
528
528
  return {
@@ -551,15 +551,17 @@ function buildOfflineValidationEnv ({ fixture, outputRoot }) {
551
551
  * Rejects command-local assignments that can bypass validator-controlled offline routing.
552
552
  *
553
553
  * @param {object} command command to execute
554
- * @param {NodeJS.ProcessEnv} env validator environment overrides
554
+ * @param {typeof process.env} env validator environment overrides
555
555
  */
556
556
  function assertNoInlineValidationEnvOverrides (command, env) {
557
557
  if (!env[VALIDATION_MODE_ENV]) return
558
- for (const name of Object.keys(command.env || {})) {
559
- const normalized = process.platform === 'win32' ? name.toUpperCase() : name
560
- if (VALIDATION_RESERVED_ENV_NAMES.some(reserved => environmentNamesEqual(reserved, name)) ||
561
- isDatadogEnvironmentName(name) || normalized.startsWith('OTEL_')) {
562
- throw new Error(`Direct-runner adapter must not override validator-controlled environment variable ${name}.`)
558
+ if (command.env) {
559
+ for (const name of Object.keys(command.env)) {
560
+ const normalized = process.platform === 'win32' ? name.toUpperCase() : name
561
+ if (VALIDATION_RESERVED_ENV_NAMES.some(reserved => environmentNamesEqual(reserved, name)) ||
562
+ isDatadogEnvironmentName(name) || normalized.startsWith('OTEL_')) {
563
+ throw new Error(`Direct-runner adapter must not override validator-controlled environment variable ${name}.`)
564
+ }
563
565
  }
564
566
  }
565
567
  }
@@ -63,8 +63,10 @@ function setEnvironmentValue (environment, name, value, platform = process.platf
63
63
  * @returns {void}
64
64
  */
65
65
  function mergeEnvironment (target, source, platform = process.platform) {
66
- for (const [name, value] of Object.entries(source || {})) {
67
- setEnvironmentValue(target, name, value, platform)
66
+ if (source) {
67
+ for (const [name, value] of Object.entries(source)) {
68
+ setEnvironmentValue(target, name, value, platform)
69
+ }
68
70
  }
69
71
  }
70
72
 
@@ -40,19 +40,21 @@ function getResolvedExecutable (command) {
40
40
  */
41
41
  function bindManifestExecutables (manifest) {
42
42
  const identities = []
43
- for (const framework of manifest.frameworks || []) {
44
- if (framework.status !== 'runnable') continue
45
- const command = getManifestCommands({ frameworks: [framework] })[0]?.[1]
46
- try {
47
- const identity = getCommandExecutableIdentity(command, manifest.repository.root)
48
- bindApprovedExecutable(framework.validation, identity)
49
- identities.push({ id: `framework:${framework.id}`, ...identity })
50
- } catch (error) {
51
- identities.push({
52
- id: `framework:${framework.id}`,
53
- unavailable: true,
54
- reason: error?.message || String(error),
55
- })
43
+ if (manifest.frameworks) {
44
+ for (const framework of manifest.frameworks) {
45
+ if (framework.status !== 'runnable') continue
46
+ const command = getManifestCommands({ frameworks: [framework] })[0]?.[1]
47
+ try {
48
+ const identity = getCommandExecutableIdentity(command, manifest.repository.root)
49
+ bindApprovedExecutable(framework.validation, identity)
50
+ identities.push({ id: `framework:${framework.id}`, ...identity })
51
+ } catch (error) {
52
+ identities.push({
53
+ id: `framework:${framework.id}`,
54
+ unavailable: true,
55
+ reason: error?.message || String(error),
56
+ })
57
+ }
56
58
  }
57
59
  }
58
60
  return identities
@@ -0,0 +1,70 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const { createFileSafely } = require('./safe-files')
7
+ const { createValidationBlocker } = require('./validation-blocker')
8
+
9
+ const EXECUTION_LOCK_FILENAME = '.dd-test-optimization-validation.lock'
10
+
11
+ function getExecutionLockPath (out) {
12
+ return path.join(path.resolve(out), EXECUTION_LOCK_FILENAME)
13
+ }
14
+
15
+ // Locks are never reclaimed automatically; only the creator may release the exact file identity.
16
+ function acquireExecutionLock ({ out, approvedPlanSha256 }) {
17
+ const lockPath = getExecutionLockPath(out)
18
+ const content = `${JSON.stringify({
19
+ approvedPlanSha256,
20
+ pid: process.pid,
21
+ startedAt: new Date().toISOString(),
22
+ }, null, 2)}\n`
23
+ try {
24
+ createFileSafely(out, lockPath, content, 'validation execution lock')
25
+ } catch (error) {
26
+ if (error.code === 'EEXIST') throw getExistingLockError(lockPath)
27
+ throw error
28
+ }
29
+
30
+ const stat = fs.lstatSync(lockPath, { bigint: true })
31
+ if (!stat.isFile() || stat.isSymbolicLink()) {
32
+ throw new Error(`Validation execution lock is not a regular file: ${lockPath}`)
33
+ }
34
+ return { dev: stat.dev, ino: stat.ino, path: lockPath }
35
+ }
36
+
37
+ function releaseExecutionLock (lock) {
38
+ let stat
39
+ try {
40
+ stat = fs.lstatSync(lock.path, { bigint: true })
41
+ } catch (error) {
42
+ if (error.code === 'ENOENT') {
43
+ throw new Error(`Validation execution lock disappeared before safe cleanup: ${lock.path}`)
44
+ }
45
+ throw error
46
+ }
47
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== lock.dev || stat.ino !== lock.ino) {
48
+ throw new Error(`Refusing to remove a changed validation execution lock: ${lock.path}`)
49
+ }
50
+ fs.unlinkSync(lock.path)
51
+ }
52
+
53
+ function getExistingLockError (lockPath) {
54
+ return createValidationBlocker(
55
+ `Another validation may be active, or an interrupted run left its execution lock: ${lockPath}.`,
56
+ {
57
+ kind: 'execution-lock-exists',
58
+ recommendation: `Inspect ${lockPath}. Remove only that lock after confirming no validation process is active, ` +
59
+ 'then render and approve a fresh plan.',
60
+ suppressReport: true,
61
+ }
62
+ )
63
+ }
64
+
65
+ module.exports = {
66
+ EXECUTION_LOCK_FILENAME,
67
+ acquireExecutionLock,
68
+ getExecutionLockPath,
69
+ releaseExecutionLock,
70
+ }