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
@@ -3,6 +3,7 @@
3
3
  const path = require('node:path')
4
4
 
5
5
  const { getArtifactId } = require('./artifact-id')
6
+ const { BLOCKER_CATEGORY_VALUES } = require('./blocker-category')
6
7
  const {
7
8
  MAX_GENERATED_FILES,
8
9
  getGeneratedFileContentError,
@@ -111,6 +112,12 @@ function validateFrameworks (manifest, errors) {
111
112
  requiredString(framework, 'id', prefix, errors)
112
113
  requiredString(framework, 'framework', prefix, errors)
113
114
  enumString(framework, 'status', STATUSES, prefix, errors)
115
+ if (framework.blockerCategory !== undefined &&
116
+ !BLOCKER_CATEGORY_VALUES.has(framework.blockerCategory)) {
117
+ errors.push(
118
+ `${prefix}.blockerCategory must be one of ${[...BLOCKER_CATEGORY_VALUES].join(', ')} when provided.`
119
+ )
120
+ }
114
121
  requiredObject(framework, 'project', prefix, errors)
115
122
  rejectExecutionFields(framework, prefix, errors)
116
123
 
@@ -132,6 +139,12 @@ function validateFrameworks (manifest, errors) {
132
139
  validateCiWiring(manifest.repository?.root, framework.ciWiring, `${prefix}.ciWiring`, errors)
133
140
 
134
141
  if (framework.status === 'runnable') {
142
+ if (typeof framework.allCandidatesRequireLocalSocket !== 'boolean') {
143
+ errors.push(`${prefix}.allCandidatesRequireLocalSocket must be a boolean.`)
144
+ }
145
+ if (typeof framework.buildArtifactRequired !== 'boolean') {
146
+ errors.push(`${prefix}.buildArtifactRequired must be a boolean.`)
147
+ }
135
148
  validateRunnableFramework(manifest.repository?.root, framework, prefix, generatedPaths, errors)
136
149
  } else {
137
150
  for (const field of ['validation', 'preflight', 'generatedTestStrategy']) {
@@ -198,12 +211,13 @@ function validateRunnableFramework (repositoryRoot, framework, prefix, generated
198
211
  }
199
212
  containedPath(repositoryRoot, validation.runner, `${prefix}.validation.runner`, errors)
200
213
  containedPath(repositoryRoot, validation.testFile, `${prefix}.validation.testFile`, errors)
214
+ validateFallbackTests(repositoryRoot, validation, `${prefix}.validation`, errors)
201
215
  const runnerArgsError = getRunnerArgsError(framework.framework, validation.runnerArgs)
202
216
  if (runnerArgsError) errors.push(`${prefix}.validation.runnerArgs ${runnerArgsError}.`)
203
217
  if (validation.omittedRunnerOptions !== undefined) {
204
218
  validateStringArray(validation.omittedRunnerOptions, `${prefix}.validation.omittedRunnerOptions`, errors)
205
219
  for (const option of Array.isArray(validation.omittedRunnerOptions) ? validation.omittedRunnerOptions : []) {
206
- if (!['--run', '--typecheck'].includes(option)) {
220
+ if (!['-R', '--reporter', '--run', '--typecheck'].includes(option)) {
207
221
  errors.push(`${prefix}.validation.omittedRunnerOptions contains unsupported option ${option}.`)
208
222
  }
209
223
  }
@@ -225,21 +239,23 @@ function validateRunnableFramework (repositoryRoot, framework, prefix, generated
225
239
  if (runnerInputError) errors.push(`${prefix}.validation runner configuration ${runnerInputError}.`)
226
240
  }
227
241
  validateStringArray(validation.requiredEnvVars, `${prefix}.validation.requiredEnvVars`, errors)
228
- for (const name of validation.requiredEnvVars || []) {
229
- if (!ENV_NAME_PATTERN.test(name)) {
230
- errors.push(`${prefix}.validation.requiredEnvVars contains an invalid environment name.`)
231
- }
232
- if (/^(?:DD_|DATADOG_|OTEL_|NODE_OPTIONS$|TS_NODE_PROJECT$)/i.test(name)) {
233
- errors.push(
234
- `${prefix}.validation.requiredEnvVars must not inherit Datadog, OpenTelemetry, NODE_OPTIONS, or ` +
235
- 'TS_NODE_PROJECT.'
236
- )
237
- }
238
- if (SECRET_ENV_PATTERN.test(name)) {
239
- errors.push(`${prefix}.validation.requiredEnvVars must not inherit secret-like environment variables.`)
240
- }
241
- if (EXECUTION_ENV_PATTERN.test(name)) {
242
- errors.push(`${prefix}.validation.requiredEnvVars must not inherit executable-loading environment variables.`)
242
+ if (validation.requiredEnvVars) {
243
+ for (const name of validation.requiredEnvVars) {
244
+ if (!ENV_NAME_PATTERN.test(name)) {
245
+ errors.push(`${prefix}.validation.requiredEnvVars contains an invalid environment name.`)
246
+ }
247
+ if (/^(?:DD_|DATADOG_|OTEL_|NODE_OPTIONS$|TS_NODE_PROJECT$)/i.test(name)) {
248
+ errors.push(
249
+ `${prefix}.validation.requiredEnvVars must not inherit Datadog, OpenTelemetry, NODE_OPTIONS, or ` +
250
+ 'TS_NODE_PROJECT.'
251
+ )
252
+ }
253
+ if (SECRET_ENV_PATTERN.test(name)) {
254
+ errors.push(`${prefix}.validation.requiredEnvVars must not inherit secret-like environment variables.`)
255
+ }
256
+ if (EXECUTION_ENV_PATTERN.test(name)) {
257
+ errors.push(`${prefix}.validation.requiredEnvVars must not inherit executable-loading environment variables.`)
258
+ }
243
259
  }
244
260
  }
245
261
  if (!Number.isInteger(validation.timeoutMs) || validation.timeoutMs < 1 || validation.timeoutMs > MAX_TIMEOUT_MS) {
@@ -253,6 +269,51 @@ function validateRunnableFramework (repositoryRoot, framework, prefix, generated
253
269
  validateGeneratedStrategy(repositoryRoot, framework, prefix, generatedPaths, errors)
254
270
  }
255
271
 
272
+ /**
273
+ * Validates bounded fallback test paths and their statically discovered prerequisites.
274
+ *
275
+ * @param {string} repositoryRoot repository root
276
+ * @param {object} validation direct-runner validation data
277
+ * @param {string} prefix error prefix
278
+ * @param {{push: function(string): void}} errors error collector
279
+ * @returns {void}
280
+ */
281
+ function validateFallbackTests (repositoryRoot, validation, prefix, errors) {
282
+ if (validation.fallbackTests === undefined) return
283
+ if (!Array.isArray(validation.fallbackTests)) {
284
+ errors.push(`${prefix}.fallbackTests must be an array.`)
285
+ return
286
+ }
287
+ if (validation.fallbackTests.length > 2) {
288
+ errors.push(`${prefix}.fallbackTests must contain at most 2 entries.`)
289
+ }
290
+
291
+ const testFiles = new Set()
292
+ for (const [index, fallback] of validation.fallbackTests.slice(0, 2).entries()) {
293
+ const fallbackPrefix = `${prefix}.fallbackTests[${index}]`
294
+ if (!isObject(fallback)) {
295
+ errors.push(`${fallbackPrefix} must be an object.`)
296
+ continue
297
+ }
298
+ rejectExecutionFields(fallback, fallbackPrefix, errors)
299
+ requiredAbsolutePath(fallback, 'testFile', fallbackPrefix, errors)
300
+ containedPath(repositoryRoot, fallback.testFile, `${fallbackPrefix}.testFile`, errors)
301
+ if (typeof fallback.buildArtifactRequired !== 'boolean') {
302
+ errors.push(`${fallbackPrefix}.buildArtifactRequired must be a boolean.`)
303
+ }
304
+ if (typeof fallback.localSocketRequired !== 'boolean') {
305
+ errors.push(`${fallbackPrefix}.localSocketRequired must be a boolean.`)
306
+ }
307
+ if (fallback.testFile === validation.testFile) {
308
+ errors.push(`${fallbackPrefix}.testFile must differ from ${prefix}.testFile.`)
309
+ }
310
+ if (testFiles.has(fallback.testFile)) {
311
+ errors.push(`${prefix}.fallbackTests must contain unique testFile values.`)
312
+ }
313
+ testFiles.add(fallback.testFile)
314
+ }
315
+ }
316
+
256
317
  /**
257
318
  * Validates canonical generated test data.
258
319
  *
@@ -450,7 +511,9 @@ function validateAllStrings (value, prefix, errors) {
450
511
  if (errors.full()) return
451
512
  if (typeof value === 'string') {
452
513
  if (Buffer.byteLength(value) > MAX_STRING_BYTES) errors.push(`${prefix} exceeds the string size limit.`)
453
- if (hasUnsafeInvisibleCharacter(value)) errors.push(`${prefix} contains an unsafe invisible character.`)
514
+ const inertCiLabel = /\.ciWiring\.(?:job|step)$/.test(prefix)
515
+ const text = inertCiLabel ? value.replaceAll(/[\uFE0E\uFE0F]/g, '') : value
516
+ if (hasUnsafeInvisibleCharacter(text)) errors.push(`${prefix} contains an unsafe invisible character.`)
454
517
  return
455
518
  }
456
519
  if (Array.isArray(value)) {
@@ -7,6 +7,7 @@ const path = require('node:path')
7
7
 
8
8
  const { getArtifactId } = require('./artifact-id')
9
9
  const { createFileSafely, ensureSafeDirectory } = require('./safe-files')
10
+ const { createValidationBlocker } = require('./validation-blocker')
10
11
 
11
12
  const MAX_FIXTURE_FILE_BYTES = 1024 * 1024
12
13
  const OFFLINE_FIXTURE_NONCE_PATTERN = /^[a-f0-9]{32}$/
@@ -71,9 +72,13 @@ function createOfflineFixture ({
71
72
  }
72
73
  ensurePrivateDirectory(base)
73
74
  if (fs.existsSync(root)) {
74
- throw new Error(
75
- `Offline validation fixture already exists and will not be replaced or deleted: ${root}. ` +
76
- 'Inspect and remove it manually, then render a fresh approval plan.'
75
+ throw createValidationBlocker(
76
+ `Offline validation fixture already exists and will not be replaced or deleted: ${root}.`,
77
+ {
78
+ kind: 'offline-fixture-exists',
79
+ recommendation: `Inspect ${root}. Remove only that validator fixture after confirming no validation process ` +
80
+ 'is active, then render and approve a fresh plan.',
81
+ }
77
82
  )
78
83
  }
79
84
  try {
@@ -244,7 +249,11 @@ function getFixtureSettingsOverrides (scenarioName) {
244
249
  function cleanupOfflineFixture (fixtureRoot) {
245
250
  const parent = path.dirname(path.dirname(fixtureRoot))
246
251
  ensureSafeDirectory(parent, fixtureRoot, 'offline validation fixture cleanup')
247
- fs.rmSync(fixtureRoot, { recursive: true })
252
+ try {
253
+ fs.rmSync(fixtureRoot, { recursive: true })
254
+ } catch (error) {
255
+ if (error.code !== 'ENOENT') throw error
256
+ }
248
257
  removeEmptyParents(path.dirname(fixtureRoot), parent)
249
258
  }
250
259
 
@@ -57,8 +57,8 @@ function readOfflineOutput (outputRoot) {
57
57
  if (!exporterInitialized) return emptyOutput()
58
58
 
59
59
  const state = {
60
- bytes: 0,
61
- completionBytes: 0,
60
+ bytes: 0n,
61
+ completionBytes: 0n,
62
62
  decodedEntries: 0,
63
63
  files: 0,
64
64
  }
@@ -150,7 +150,7 @@ function readPayloadFiles (payloadsRoot, kind, state, consume) {
150
150
  }
151
151
 
152
152
  function readRegularFile (filename, individualLimit, state, completion) {
153
- const stat = fs.lstatSync(filename)
153
+ const stat = fs.lstatSync(filename, { bigint: true })
154
154
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink > 1) {
155
155
  throw new Error('Offline validation artifact must be a regular, unlinked file.')
156
156
  }
@@ -163,16 +163,16 @@ function readRegularFile (filename, individualLimit, state, completion) {
163
163
 
164
164
  const file = fs.openSync(filename, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0))
165
165
  try {
166
- const opened = fs.fstatSync(file)
166
+ const opened = fs.fstatSync(file, { bigint: true })
167
167
  if (!opened.isFile() || opened.nlink > 1 || opened.dev !== stat.dev || opened.ino !== stat.ino) {
168
168
  throw new Error('Offline validation artifact changed while it was opened.')
169
169
  }
170
170
  const buffer = fs.readFileSync(file)
171
- const completed = fs.fstatSync(file)
171
+ const completed = fs.fstatSync(file, { bigint: true })
172
172
  if (completed.size !== opened.size || completed.mtimeMs !== opened.mtimeMs) {
173
173
  throw new Error('Offline validation artifact changed while it was read.')
174
174
  }
175
- state[totalKey] += buffer.length
175
+ state[totalKey] += BigInt(buffer.length)
176
176
  return buffer
177
177
  } finally {
178
178
  fs.closeSync(file)
@@ -0,0 +1,94 @@
1
+ 'use strict'
2
+
3
+ const { spawnSync } = require('node:child_process')
4
+ const path = require('node:path')
5
+
6
+ const { BLOCKER_CATEGORIES } = require('./blocker-category')
7
+ const { getBaseEnv } = require('./command-runner')
8
+ const { sanitizeString } = require('./redaction')
9
+
10
+ const DEFAULT_PACKAGE_ROOT = path.resolve(__dirname, '..', '..')
11
+ const LOAD_MARKER = 'dd-trace-test-optimization-package-check-ok'
12
+ const MAX_OUTPUT_BYTES = 64 * 1024
13
+ const TIMEOUT_MS = 10_000
14
+
15
+ // Loads the initialization entrypoint in an isolated child without initializing tracing.
16
+ function checkInstalledPackage ({ packageRoot = DEFAULT_PACKAGE_ROOT } = {}) {
17
+ const root = path.resolve(packageRoot)
18
+ const initPath = path.join(root, 'ci', 'init.js')
19
+ const script = [
20
+ `require(${JSON.stringify(initPath)})`,
21
+ `process.stdout.write(${JSON.stringify(LOAD_MARKER)})`,
22
+ 'process.exit(0)',
23
+ ].join(';')
24
+ const result = spawnSync(process.execPath, ['-e', script], {
25
+ cwd: root,
26
+ encoding: 'utf8',
27
+ env: {
28
+ ...getBaseEnv('clean'),
29
+ DD_CIVISIBILITY_ENABLED: 'false',
30
+ DD_INSTRUMENTATION_TELEMETRY_ENABLED: 'false',
31
+ DD_PROFILING_ENABLED: 'false',
32
+ DD_REMOTE_CONFIGURATION_ENABLED: 'false',
33
+ DD_TRACE_ENABLED: 'false',
34
+ NODE_OPTIONS: '',
35
+ },
36
+ maxBuffer: MAX_OUTPUT_BYTES,
37
+ shell: false,
38
+ timeout: TIMEOUT_MS,
39
+ windowsHide: true,
40
+ })
41
+
42
+ if (!result.error && result.status === 0 && result.stdout.includes(LOAD_MARKER)) {
43
+ return {
44
+ ok: true,
45
+ diagnosis: 'The installed dd-trace package loaded its Test Optimization initialization entrypoint.',
46
+ }
47
+ }
48
+
49
+ const detail = getFailureDetail(result)
50
+ return {
51
+ ok: false,
52
+ diagnosis: 'The installed dd-trace package could not load its Test Optimization initialization entrypoint' +
53
+ (detail ? `: ${detail}` : '.'),
54
+ recommendation: 'Reinstall dd-trace through the project\'s normal dependency workflow, then create a fresh ' +
55
+ 'validation plan. If the load check still fails, report it as an installed-package problem without running ' +
56
+ 'tests.',
57
+ }
58
+ }
59
+
60
+ function getInstalledPackageFailure (framework, packageCheck) {
61
+ return {
62
+ frameworkId: framework.id,
63
+ scenario: 'basic-reporting',
64
+ status: 'blocked',
65
+ diagnosis: `${packageCheck.diagnosis} No project test was run and no library-compatibility conclusion was reached.`,
66
+ evidence: {
67
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
68
+ blockedByProjectSetup: true,
69
+ installedPackageIncomplete: true,
70
+ recommendation: packageCheck.recommendation,
71
+ validationIncomplete: true,
72
+ },
73
+ artifacts: [],
74
+ }
75
+ }
76
+
77
+ function getFailureDetail (result) {
78
+ if (result.error?.code === 'ETIMEDOUT') return `the isolated load check exceeded ${TIMEOUT_MS} ms.`
79
+ if (result.error) return `${sanitizeLine(result.error.message)}.`
80
+
81
+ const output = `${result.stderr || ''}\n${result.stdout || ''}`
82
+ const line = output.split(/\r?\n/).map(value => value.trim()).find(value => {
83
+ return /^(?:Error:|.*(?:MODULE_NOT_FOUND|Cannot find (?:module|package)|ERR_[A-Z_]+))/.test(value)
84
+ })
85
+ if (line) return `${sanitizeLine(line)}.`
86
+ if (result.signal) return `the isolated load check exited after signal ${sanitizeLine(result.signal)}.`
87
+ return `the isolated load check exited ${result.status ?? 'without a status'}.`
88
+ }
89
+
90
+ function sanitizeLine (value) {
91
+ return sanitizeString(String(value)).replaceAll(/\p{Cc}+/gu, ' ').trim().slice(0, 1000).replace(/[.]+$/, '')
92
+ }
93
+
94
+ module.exports = { checkInstalledPackage, getInstalledPackageFailure }
@@ -4,6 +4,7 @@ const crypto = require('node:crypto')
4
4
  const path = require('node:path')
5
5
 
6
6
  const { VERSION } = require('../../version')
7
+ const { getRequiredCapabilities } = require('./approval')
7
8
  const { writeApprovalArtifacts } = require('./approval-artifacts')
8
9
  const { serializeApprovalCommand } = require('./command-runner')
9
10
  const { getUnavailableExecutable } = require('./executable')
@@ -41,6 +42,9 @@ function formatExecutionPlan (input) {
41
42
  * @param {string[]} [input.selectedFrameworkIds] selected framework ids
42
43
  * @param {string|null} [input.requestedScenario] selected scenario
43
44
  * @param {boolean} [input.keepTempFiles] retain temporary files
45
+ * @param {object} [input.packageCheck] installed package-load result
46
+ * @param {Map<string, object>} [input.ciPreflightResults] static CI results by framework
47
+ * @param {Array<{path: string, sha256: string}>} [input.expectedProjectFiles] preflight project snapshot
44
48
  * @param {boolean} [input.verbose] print progress
45
49
  * @returns {{plan: string}} written plan
46
50
  */
@@ -50,6 +54,9 @@ function formatExecutionPlanArtifacts ({
50
54
  selectedFrameworkIds = [],
51
55
  requestedScenario = null,
52
56
  keepTempFiles = false,
57
+ packageCheck,
58
+ ciPreflightResults = new Map(),
59
+ expectedProjectFiles,
53
60
  verbose = false,
54
61
  }) {
55
62
  const offlineFixtureNonce = crypto.randomBytes(16).toString('hex')
@@ -61,6 +68,7 @@ function formatExecutionPlanArtifacts ({
61
68
  offlineFixtureNonce,
62
69
  keepTempFiles,
63
70
  verbose,
71
+ expectedProjectFiles,
64
72
  })
65
73
  const validatorArgv = [
66
74
  process.execPath,
@@ -72,8 +80,11 @@ function formatExecutionPlanArtifacts ({
72
80
  ]
73
81
  const plan = formatApprovalPlan({
74
82
  approvalArtifacts,
83
+ ciPreflightResults,
75
84
  manifest,
76
85
  out,
86
+ keepTempFiles,
87
+ packageCheck,
77
88
  requestedScenario,
78
89
  validatorArgv,
79
90
  })
@@ -86,20 +97,67 @@ function formatExecutionPlanArtifacts ({
86
97
  *
87
98
  * @param {object} input formatting inputs
88
99
  * @param {object} input.approvalArtifacts approval artifact paths and digest
100
+ * @param {Map<string, object>} input.ciPreflightResults static CI results by framework
89
101
  * @param {object} input.manifest normalized manifest
90
102
  * @param {string} input.out output directory
103
+ * @param {boolean} input.keepTempFiles retain temporary files
104
+ * @param {object|undefined} input.packageCheck installed package-load result
91
105
  * @param {string|null} input.requestedScenario selected scenario
92
106
  * @param {string[]} input.validatorArgv exact validator command
93
107
  * @returns {string} Markdown plan
94
108
  */
95
- function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenario, validatorArgv }) {
109
+ function formatApprovalPlan ({
110
+ approvalArtifacts,
111
+ ciPreflightResults,
112
+ manifest,
113
+ out,
114
+ keepTempFiles,
115
+ packageCheck,
116
+ requestedScenario,
117
+ validatorArgv,
118
+ }) {
96
119
  const root = manifest.repository.root
120
+ const runnableFrameworks = manifest.frameworks.filter(framework => framework.status === 'runnable')
121
+ const requiredCapabilities = getRequiredCapabilities({
122
+ manifest,
123
+ requestedScenario,
124
+ })
125
+ const buildPrerequisites = runnableFrameworks.filter(framework => {
126
+ return framework.buildArtifactRequired === true ||
127
+ (framework.validation?.fallbackTests || []).some(fallback => fallback.buildArtifactRequired === true)
128
+ }).length
97
129
  const lines = [
98
130
  '# Test Optimization Validation Plan',
99
131
  '',
100
132
  `Repository: ${inline(root)}`,
101
133
  `Validator: ${inline(`dd-trace ${VERSION}`)}`,
102
134
  `Results: ${inline(relative(root, out))}`,
135
+ ...(packageCheck?.ok
136
+ ? [`Installed package check: ${plain(packageCheck.diagnosis)}`]
137
+ : []),
138
+ '',
139
+ '## Approval Summary',
140
+ '',
141
+ requestedScenario === 'ci-wiring'
142
+ ? 'Local execution: none; this plan performs static CI analysis only.'
143
+ : `Local execution: ${runnableFrameworks.length} framework target${
144
+ runnableFrameworks.length === 1 ? '' : 's'
145
+ } eligible; ${manifest.frameworks.length - runnableFrameworks.length} setup or static-only.`,
146
+ `Required host capabilities: ${
147
+ requiredCapabilities.length > 0 ? requiredCapabilities.map(formatCapability).join(', ') : 'none declared'
148
+ }.`,
149
+ `Build prerequisites: ${
150
+ buildPrerequisites > 0
151
+ ? `${buildPrerequisites} eligible framework target${
152
+ buildPrerequisites === 1 ? '' : 's'
153
+ } may require the project's normal build`
154
+ : 'none detected statically'
155
+ }.`,
156
+ `Mutable paths: ${inline(relative(root, out))} and the temporary files disclosed below; temporary files ${
157
+ keepTempFiles ? 'will be retained by request' : 'will be removed after validation'
158
+ }.`,
159
+ 'CI replay: none. CI configuration is inspected statically and CI commands are never executed.',
160
+ 'Approval: one checksum-bound validator command, shown once at the end of this plan.',
103
161
  '',
104
162
  'The validator will run only the displayed `node <repository-contained-runner> <one-test-file>` commands. The ' +
105
163
  'runner may be an exact repository-owned Node test wrapper. The validator itself will not directly invoke ' +
@@ -112,8 +170,15 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
112
170
  for (const framework of manifest.frameworks) {
113
171
  lines.push(`### ${plain(getFrameworkLabel(framework, root))}`, '')
114
172
  if (framework.status !== 'runnable') {
115
- lines.push(`Status: ${plain(formatStatus(framework.status))}.`)
116
- for (const note of framework.notes || []) lines.push(`- ${plain(note)}`)
173
+ lines.push(
174
+ `Status: ${plain(formatStatus(framework.status))}.`,
175
+ ...(framework.blockerCategory
176
+ ? [`Blocker: ${plain(framework.blockerCategory.replaceAll('_', ' '))}.`]
177
+ : [])
178
+ )
179
+ if (framework.notes) {
180
+ for (const note of framework.notes) lines.push(`- ${plain(note)}`)
181
+ }
117
182
  lines.push('')
118
183
  continue
119
184
  }
@@ -122,13 +187,25 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
122
187
  lines.push('Status: static CI audit only; no project test command is selected.', '')
123
188
  } else {
124
189
  const basic = getBasicCommand(framework)
190
+ const fallbackTests = framework.validation.fallbackTests || []
125
191
  const unavailable = getUnavailableExecutable(basic)
126
192
  const status = unavailable
127
193
  ? `local validation will be incomplete because ${inline(unavailable)} is unavailable`
128
194
  : 'eligible for approved clean preflight; runtime prerequisites are unverified'
129
195
  lines.push(
130
196
  `Status: ${status}.`,
131
- `Representative test: ${inline(relative(root, framework.validation.testFile))}`,
197
+ `Representative test: ${inline(relative(root, framework.validation.testFile))}${formatCandidateRequirements({
198
+ buildArtifactRequired: framework.buildArtifactRequired,
199
+ localSocketRequired: framework.localSocketRequired,
200
+ })}`,
201
+ ...(fallbackTests.length > 0
202
+ ? [
203
+ 'Fallback tests, tried in order only if the representative does not pass cleanly:',
204
+ ...fallbackTests.map(fallback => {
205
+ return `- ${inline(relative(root, fallback.testFile))}${formatCandidateRequirements(fallback)}`
206
+ }),
207
+ ]
208
+ : []),
132
209
  `Working directory: ${inline(relative(root, basic.cwd))}`,
133
210
  `Timeout: ${basic.timeoutMs} ms`,
134
211
  ...(Object.keys(basic.env || {}).length > 0
@@ -136,16 +213,71 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
136
213
  : []),
137
214
  ...formatOmittedRunnerOptions(framework.validation.omittedRunnerOptions),
138
215
  '',
216
+ ...(framework.browserRequired
217
+ ? [
218
+ 'Browser permission: this approved validation command launches the project browser through its ' +
219
+ 'selected test runner. If the agent platform blocks browser processes, request its narrow permission ' +
220
+ 'for the exact checksum-bound validator command below; do not change or broaden the command. ' +
221
+ 'Complete ' +
222
+ 'the project\'s normal browser installation, application startup, and build first when the selected ' +
223
+ 'test depends on them.',
224
+ '',
225
+ ]
226
+ : []),
227
+ ...(framework.localSocketRequired
228
+ ? [
229
+ `Localhost prerequisite: ${
230
+ framework.allCandidatesRequireLocalSocket
231
+ ? 'every approved candidate appears'
232
+ : 'the selected test appears'
233
+ } to open or contact a local listener. A restricted execution environment may require narrow ` +
234
+ 'permission for the exact validator command.',
235
+ '',
236
+ ]
237
+ : []),
238
+ ...(framework.buildArtifactRequired
239
+ ? [
240
+ 'Build prerequisite: the selected test appears to load a build or dist artifact. Complete the ' +
241
+ 'project\'s normal build before validation if that artifact is not already present.',
242
+ '',
243
+ ]
244
+ : []),
139
245
  'Basic Reporting command:',
140
246
  '',
141
247
  codeBlock(serializeApprovalCommand(basic)),
142
248
  '',
143
249
  'The command runs once without Datadog and once with validator-owned offline initialization. A debug rerun ' +
144
250
  'and one clean confirmation may run only when needed to diagnose a mismatch.',
251
+ ...(fallbackTests.length > 0
252
+ ? [
253
+ 'If this test does not pass cleanly, the validator may try the disclosed fallback tests in order. It ' +
254
+ 'will initialize only the first candidate that passes, and will stop early when the failure is a ' +
255
+ 'confirmed shared runtime prerequisite rather than a candidate-specific test failure.',
256
+ ]
257
+ : []),
145
258
  ''
146
259
  )
147
260
 
261
+ for (const [index, fallback] of fallbackTests.entries()) {
262
+ const fallbackCommand = getBasicCommand(framework, fallback.testFile)
263
+ lines.push(
264
+ `Fallback Basic Reporting command ${index + 1}:`,
265
+ '',
266
+ codeBlock(serializeApprovalCommand(fallbackCommand)),
267
+ ''
268
+ )
269
+ }
270
+
148
271
  const selectedScenarios = getSelectedGeneratedScenarios(framework, requestedScenario)
272
+ if (selectedScenarios.length > 0) {
273
+ lines.push(
274
+ `Advanced checks use working directory: ${inline(relative(
275
+ root,
276
+ getGeneratedCommand(framework, selectedScenarios[0]).cwd
277
+ ))}`,
278
+ ''
279
+ )
280
+ }
149
281
  for (const scenario of selectedScenarios) {
150
282
  const command = getGeneratedCommand(framework, scenario)
151
283
  const source = getGeneratedSource(framework, scenario)
@@ -157,9 +289,6 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
157
289
  `Temporary file: ${inline(relative(root, scenario.testIdentities[0].file))}`,
158
290
  '',
159
291
  codeBlock(source),
160
- '',
161
- 'Execution count: one clean generated-test verification, one instrumented identity-discovery run, and ' +
162
- 'one feature-validation run. One debug rerun may run only after a failure.',
163
292
  ''
164
293
  )
165
294
  }
@@ -173,12 +302,23 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
173
302
  ''
174
303
  )
175
304
  }
305
+ if (selectedScenarios.length > 0) {
306
+ lines.push(
307
+ 'Advanced execution policy: each generated scenario has one clean verification, one instrumented ' +
308
+ 'identity-discovery run, and one feature-validation run. One debug rerun may run only after a failure.',
309
+ ''
310
+ )
311
+ }
176
312
  }
177
313
 
178
314
  const ci = framework.ciWiring
315
+ const ciPreflight = ciPreflightResults.get(framework.id)
179
316
  lines.push(
180
317
  'CI audit: static only; no CI or package command will execute.',
181
318
  `CI review: ${plain(ci?.reviewComplete ? 'complete' : 'incomplete')}.`,
319
+ ...(ciPreflight
320
+ ? [`CI pre-approval result: ${plain(formatCiPreflightResult(ciPreflight))}`]
321
+ : []),
182
322
  ...(ci?.configFile ? [`CI file: ${inline(relative(root, ci.configFile))}`] : []),
183
323
  ''
184
324
  )
@@ -188,6 +328,8 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
188
328
  '## Writes and Cleanup',
189
329
  '',
190
330
  `- Validation artifacts: ${inline(relative(root, out))}`,
331
+ '- Single-flight lock: the approved validator creates one fixed lock in the result directory while it runs. An ' +
332
+ 'existing lock blocks execution and is never reclaimed automatically.',
191
333
  ...(requestedScenario === 'ci-wiring'
192
334
  ? []
193
335
  : [
@@ -233,10 +375,41 @@ function formatApprovalPlan ({ approvalArtifacts, manifest, out, requestedScenar
233
375
  return lines.join('\n')
234
376
  }
235
377
 
378
+ function formatCapability (capability) {
379
+ const labels = {
380
+ browser_process: 'browser process',
381
+ localhost_socket: 'localhost socket',
382
+ }
383
+ return labels[capability] || capability
384
+ }
385
+
386
+ function formatCandidateRequirements (candidate) {
387
+ const requirements = [
388
+ ...(candidate.localSocketRequired ? ['localhost'] : []),
389
+ ...(candidate.buildArtifactRequired ? ['build output'] : []),
390
+ ]
391
+ return requirements.length > 0 ? ` (${requirements.join(', ')} required)` : ''
392
+ }
393
+
394
+ function formatCiPreflightResult (result) {
395
+ const status = result.status === 'fail'
396
+ ? 'confirmed actionable problem'
397
+ : result.status === 'pass'
398
+ ? 'no confirmed problem'
399
+ : 'incomplete'
400
+ return `${status}: ${result.diagnosis}`
401
+ }
402
+
236
403
  function formatOmittedRunnerOptions (options = []) {
237
- return options.map(option => option === '--run'
238
- ? 'Normalized runner option: `--run` is omitted because the validator supplies Vitest `run` itself.'
239
- : 'Omitted runner option: `--typecheck` is excluded because validation executes runtime tests only.')
404
+ const descriptions = {
405
+ '-R': 'Omitted runner option: `-R` selects only Mocha report presentation; the validator uses its bounded ' +
406
+ 'reporter.',
407
+ '--reporter': 'Omitted runner option: `--reporter` selects only Mocha report presentation; the validator uses ' +
408
+ 'its bounded reporter.',
409
+ '--run': 'Normalized runner option: `--run` is omitted because the validator supplies Vitest `run` itself.',
410
+ '--typecheck': 'Omitted runner option: `--typecheck` is excluded because validation executes runtime tests only.',
411
+ }
412
+ return options.map(option => descriptions[option])
240
413
  }
241
414
 
242
415
  /**