dd-trace 6.4.0 → 6.6.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 (185) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +11 -8
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +31 -19
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  65. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  66. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  68. package/packages/datadog-instrumentations/src/jest.js +44 -17
  69. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  70. package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
  71. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  72. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  73. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  74. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  75. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  76. package/packages/datadog-instrumentations/src/next.js +24 -0
  77. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  78. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  79. package/packages/datadog-instrumentations/src/pg.js +2 -2
  80. package/packages/datadog-instrumentations/src/playwright.js +162 -46
  81. package/packages/datadog-instrumentations/src/process.js +3 -0
  82. package/packages/datadog-instrumentations/src/router.js +18 -8
  83. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  84. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +3 -3
  85. package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
  86. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  87. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  88. package/packages/datadog-instrumentations/src/ws.js +2 -1
  89. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  90. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  91. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  92. package/packages/datadog-plugin-cucumber/src/index.js +3 -2
  93. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  94. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  95. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  96. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  97. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  98. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  99. package/packages/datadog-plugin-http2/src/client.js +1 -1
  100. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  101. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  102. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  103. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  104. package/packages/datadog-plugin-playwright/src/index.js +181 -65
  105. package/packages/datadog-plugin-router/src/index.js +11 -2
  106. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  107. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  108. package/packages/datadog-plugin-ws/src/close.js +2 -1
  109. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  110. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  111. package/packages/datadog-plugin-ws/src/server.js +2 -1
  112. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  113. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  114. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  115. package/packages/dd-trace/src/azure_metadata.js +2 -1
  116. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  117. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  122. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  124. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  125. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  127. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  128. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  129. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  131. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  132. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  133. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  134. package/packages/dd-trace/src/config/helper.js +1 -0
  135. package/packages/dd-trace/src/config/index.js +10 -0
  136. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  137. package/packages/dd-trace/src/exporter.js +2 -0
  138. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  139. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  140. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  141. package/packages/dd-trace/src/feature-registry.js +10 -3
  142. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  143. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  144. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  145. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  146. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  147. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  148. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  149. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  150. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  151. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  152. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  153. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  154. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  155. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  156. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  157. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  158. package/packages/dd-trace/src/llmobs/util.js +32 -0
  159. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  160. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  161. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  162. package/packages/dd-trace/src/openfeature/index.js +0 -2
  163. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  164. package/packages/dd-trace/src/openfeature/register.js +16 -27
  165. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  166. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  167. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  168. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  169. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  170. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  171. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  172. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  173. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  174. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  175. package/packages/dd-trace/src/proxy.js +84 -7
  176. package/packages/dd-trace/src/span_processor.js +5 -0
  177. package/packages/dd-trace/src/standalone/index.js +0 -14
  178. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  179. package/packages/dd-trace/src/util.js +26 -0
  180. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  181. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  182. package/vendor/dist/protobufjs/index.js +1 -1
  183. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  184. package/vendor/dist/shell-quote/index.js +1 -1
  185. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,1120 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('node:crypto')
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+
7
+ const { getArtifactId } = require('./artifact-id')
8
+ const { writeApprovalArtifacts } = require('./approval-artifacts')
9
+ const { getCommandOutputPaths } = require('./command-output-policy')
10
+ const { getCommandSuitabilityError } = require('./command-suitability')
11
+ const { serializeApprovalCommand } = require('./command-runner')
12
+ const {
13
+ getApprovedExecutable,
14
+ getUnavailableExecutable,
15
+ } = require('./executable')
16
+ const { getCiWiringCommand, getDatadogCleanCommand, getLocalValidationCommand } = require('./local-command')
17
+ const {
18
+ getOfflineFixturePaths,
19
+ getOfflineScenarioNames,
20
+ } = require('./offline-fixtures')
21
+ const { sanitizeEnv, sanitizeString } = require('./redaction')
22
+ const { getBasicReportingCommand } = require('./scenarios/basic-reporting')
23
+ const { writeFileSafely } = require('./safe-files')
24
+
25
+ const VALIDATOR_PATH = path.resolve(__dirname, '..', 'validate-test-optimization.js')
26
+ const APPROVAL_SUMMARY_FILENAME = 'approval-summary.md'
27
+ const EXECUTION_PLAN_FILENAME = 'execution-plan.md'
28
+ const GENERATED_SCENARIO_DETAILS = {
29
+ 'basic-pass': {
30
+ heading: 'Advanced Check: Early Flake Detection',
31
+ description: 'Creates a temporary passing test, records it with Early Flake Detection disabled, then enables ' +
32
+ 'the feature and checks that Datadog recognizes the test as new and retries it.',
33
+ },
34
+ 'atr-fail-once': {
35
+ heading: 'Advanced Check: Auto Test Retries',
36
+ description: 'Creates a temporary test that fails on its first attempt, then checks that Datadog retries it ' +
37
+ 'and observes the passing attempt.',
38
+ },
39
+ 'test-management-target': {
40
+ heading: 'Advanced Check: Test Management',
41
+ description: 'Creates a temporary target test, supplies a quarantine setting for that test, then checks that ' +
42
+ 'Datadog applies the setting.',
43
+ },
44
+ }
45
+ const FRAMEWORK_NAMES = {
46
+ jest: 'Jest',
47
+ karma: 'Karma',
48
+ mocha: 'Mocha',
49
+ 'node:test': 'Node.js test runner',
50
+ playwright: 'Playwright',
51
+ vitest: 'Vitest',
52
+ }
53
+ // eslint-disable-next-line prefer-regex-literals
54
+ const CONTROL_CHARACTERS_PATTERN = new RegExp(String.raw`[\u0000-\u001F\u007F]+`, 'g')
55
+
56
+ /**
57
+ * Produces the deterministic execution plan shown before live validation.
58
+ *
59
+ * @param {object} input plan inputs
60
+ * @param {object} input.manifest normalized validation manifest
61
+ * @param {string} input.out validation output directory
62
+ * @param {string[]} [input.selectedFrameworkIds] explicitly selected framework entries
63
+ * @param {string|null} [input.requestedScenario] explicitly selected scenario
64
+ * @param {boolean} [input.keepTempFiles] whether generated files should be retained
65
+ * @param {boolean} [input.verbose] whether command progress should be printed
66
+ * @returns {string} Markdown execution plan
67
+ */
68
+ function formatExecutionPlan ({
69
+ manifest,
70
+ out,
71
+ selectedFrameworkIds = [],
72
+ requestedScenario,
73
+ keepTempFiles = false,
74
+ verbose = false,
75
+ }) {
76
+ assertPlannedExecutablesAvailable(manifest, requestedScenario)
77
+ const offlineFixtureNonce = crypto.randomBytes(16).toString('hex')
78
+ const approvalArtifacts = writeApprovalArtifacts({
79
+ manifest,
80
+ out,
81
+ selectedFrameworkIds,
82
+ requestedScenario,
83
+ offlineFixtureNonce,
84
+ keepTempFiles,
85
+ verbose,
86
+ })
87
+ const approvalDigest = approvalArtifacts.digest
88
+ const validatorArgv = getValidatorArgv({
89
+ approvedPlanSha256: approvalDigest,
90
+ approvalJsonPath: approvalArtifacts.approvalJsonPath,
91
+ repositoryRoot: manifest.repository.root,
92
+ })
93
+ const coveredFileVerification = process.platform === 'win32'
94
+ ? []
95
+ : [
96
+ 'Optional: verify every listed dd-trace package and command executable file against its recorded SHA-256:',
97
+ '',
98
+ codeBlock(sanitizeString(serializeApprovalCommand({
99
+ argv: ['shasum', '-a', '256', '--quiet', '-c', approvalArtifacts.coveredFilesPath],
100
+ cwd: manifest.repository.root,
101
+ usesShell: false,
102
+ }))),
103
+ '',
104
+ ]
105
+
106
+ const lines = [
107
+ '# Test Optimization Validation Execution Plan',
108
+ '',
109
+ `Repository: ${inlineCode(manifest.repository.root)}`,
110
+ `Manifest: ${inlineCode(manifest.__path)}`,
111
+ `Results: ${inlineCode(out)}`,
112
+ '',
113
+ '## What Will Be Validated',
114
+ '',
115
+ 'The validator runs selected project tests without Datadog to confirm they work normally, then runs the same ' +
116
+ 'tests with Datadog initialized to check that test data is reported. When a CI test command can be replayed, ' +
117
+ 'it also checks whether the configuration from that CI job reaches the test process. Temporary tests are ' +
118
+ 'used for the advanced feature checks.',
119
+ '',
120
+ ]
121
+
122
+ for (const framework of manifest.frameworks) {
123
+ const label = formatFrameworkLabel(framework, manifest.repository.root)
124
+ lines.push(`- **${plainText(label)}**: ${formatFrameworkStatus(framework.status)}`)
125
+ if (framework.status !== 'runnable') {
126
+ for (const note of framework.notes || []) lines.push(` - ${plainText(note)}`)
127
+ }
128
+ }
129
+
130
+ lines.push(
131
+ '',
132
+ '## Test Commands',
133
+ '',
134
+ 'Environment values that affect a project command are shown inline after secret-like values are replaced with ' +
135
+ '`<redacted>`. Repository files in a command are shown relative to its stated working directory. Datadog ' +
136
+ 'preloads are shown as package names; the validator resolves them from the installed `dd-trace` package.',
137
+ ''
138
+ )
139
+ for (const framework of manifest.frameworks) {
140
+ if (framework.status !== 'runnable') continue
141
+
142
+ appendFrameworkExecutions(
143
+ lines,
144
+ framework,
145
+ requestedScenario,
146
+ manifest.repository.root,
147
+ out,
148
+ offlineFixtureNonce
149
+ )
150
+ }
151
+ appendCommandIntegrity(lines, manifest, requestedScenario)
152
+
153
+ lines.push(
154
+ '',
155
+ '## Start the Validation',
156
+ '',
157
+ '`validate-test-optimization.js` is the local validator included with the installed `dd-trace` package. ' +
158
+ 'After approval, it creates bounded filesystem cache fixtures, performs every check listed above, writes ' +
159
+ 'events to local artifacts, and removes temporary fixtures and tests afterward. It does not open a listener ' +
160
+ 'or use a network endpoint.',
161
+ '',
162
+ 'The validator wrote the exact approval material to these local files without running project code:',
163
+ '',
164
+ `- Approval details: ${inlineCode(getRepositoryRelativePath(
165
+ manifest.repository.root,
166
+ approvalArtifacts.approvalJsonPath
167
+ ))}`,
168
+ `- Covered file checksums: ${inlineCode(getRepositoryRelativePath(
169
+ manifest.repository.root,
170
+ approvalArtifacts.coveredFilesPath
171
+ ))}`,
172
+ '',
173
+ 'The JSON contains the sanitized command shapes, generated test source, selected options, file fingerprints, ' +
174
+ 'and executable identities covered by approval. It is an internal diagnostic artifact and may contain ' +
175
+ 'repository paths or CI metadata.',
176
+ '',
177
+ 'Optional: independently hash the approval JSON with a standard system tool:',
178
+ '',
179
+ codeBlock(formatIndependentHashCommand(approvalArtifacts.approvalJsonPath)),
180
+ '',
181
+ `Expected SHA-256: ${inlineCode(approvalDigest)}`,
182
+ '',
183
+ ...coveredFileVerification,
184
+ 'Immediately before project code runs, the validator verifies the saved approval JSON against the SHA-256 in ' +
185
+ 'the command below, then reconstructs the approval material from the current manifest, validator package, ' +
186
+ 'generated tests, and executables. Both checks must match. This detects changes after review; it does not ' +
187
+ 'verify where the installed `dd-trace` package came from.',
188
+ '',
189
+ 'Run the approved validation command:',
190
+ '',
191
+ codeBlock(sanitizeString(serializeApprovalCommand({
192
+ argv: validatorArgv,
193
+ cwd: manifest.repository.root,
194
+ usesShell: false,
195
+ }))),
196
+ '',
197
+ `Working directory: ${inlineCode(manifest.repository.root)}`,
198
+ '',
199
+ 'The validator supplies diagnostic Datadog settings from cache files only while these local checks run; those ' +
200
+ 'settings are not customer CI recommendations. dd-trace makes no network requests in this validation mode. ' +
201
+ 'A setup or test command may still use the network unless the execution sandbox blocks it.',
202
+ '',
203
+ 'These checks run the project commands listed above. The validator does not require real Datadog ' +
204
+ 'credentials, inspect credential stores, or upload validation results. Project tests are arbitrary code and ' +
205
+ 'can forge diagnostic cache or event data, so this result is diagnostic evidence, not a security attestation. ' +
206
+ 'Review the exact commands before approving them for this environment.'
207
+ )
208
+
209
+ const plan = lines.join('\n')
210
+ const approvalSummary = formatApprovalSummary({
211
+ approvalArtifacts,
212
+ approvalDigest,
213
+ manifest,
214
+ out,
215
+ requestedScenario,
216
+ validatorArgv,
217
+ })
218
+ writeFileSafely(out, getExecutionPlanPath(out), `${plan}\n`, 'validation execution plan')
219
+ writeFileSafely(out, getApprovalSummaryPath(out), `${approvalSummary}\n`, 'validation approval summary')
220
+ return plan
221
+ }
222
+
223
+ /**
224
+ * Returns the bounded customer-facing summary an agent presents before approval.
225
+ *
226
+ * @param {object} input summary inputs
227
+ * @param {object} input.approvalArtifacts written approval artifact paths
228
+ * @param {string} input.approvalDigest approval material digest
229
+ * @param {object} input.manifest normalized validation manifest
230
+ * @param {string} input.out validation output directory
231
+ * @param {string|null|undefined} input.requestedScenario selected scenario
232
+ * @param {string[]} input.validatorArgv approved validator command
233
+ * @returns {string} Markdown approval summary
234
+ */
235
+ function formatApprovalSummary ({
236
+ approvalArtifacts,
237
+ approvalDigest,
238
+ manifest,
239
+ out,
240
+ requestedScenario,
241
+ validatorArgv,
242
+ }) {
243
+ const repositoryRoot = manifest.repository.root
244
+ const coveredFileVerification = process.platform === 'win32'
245
+ ? []
246
+ : [
247
+ 'Optional: verify every covered manifest, validator, and executable file:',
248
+ '',
249
+ codeBlock(sanitizeString(serializeApprovalCommand({
250
+ argv: ['shasum', '-a', '256', '--quiet', '-c', approvalArtifacts.coveredFilesPath],
251
+ cwd: repositoryRoot,
252
+ usesShell: false,
253
+ }))),
254
+ '',
255
+ ]
256
+ const lines = [
257
+ '# Test Optimization Validation Approval Summary',
258
+ '',
259
+ `Repository: ${inlineCode(repositoryRoot)}`,
260
+ `Detailed execution plan: ${inlineCode(getRepositoryRelativePath(repositoryRoot, getExecutionPlanPath(out)))}`,
261
+ '',
262
+ 'This summary shows every project command and the exact temporary test source. The detailed plan contains ' +
263
+ 'the offline-fixture, artifact, executable-integrity, and checksum details covered by the same approval hash.',
264
+ '',
265
+ '## Scope',
266
+ '',
267
+ ]
268
+
269
+ for (const framework of manifest.frameworks) {
270
+ const label = formatFrameworkLabel(framework, repositoryRoot)
271
+ lines.push(`- **${plainText(label)}**: ${formatFrameworkStatus(framework.status)}`)
272
+ if (framework.status !== 'runnable' && framework.notes?.[0]) {
273
+ lines.push(` - ${plainText(framework.notes[0])}`)
274
+ }
275
+ }
276
+
277
+ lines.push('', '## Commands', '')
278
+ for (const framework of manifest.frameworks) {
279
+ if (framework.status !== 'runnable') continue
280
+
281
+ appendApprovalSummaryFramework(lines, framework, requestedScenario, repositoryRoot)
282
+ }
283
+
284
+ lines.push(
285
+ '## Safety and Outputs',
286
+ '',
287
+ `- Local results: ${inlineCode(getRepositoryRelativePath(repositoryRoot, out))}`,
288
+ '- The validator creates private offline Datadog response files outside the repository and removes them ' +
289
+ 'afterward.',
290
+ '- The dd-trace validation path opens no listener, contacts no Datadog endpoint, requires no real Datadog ' +
291
+ 'credentials, and uploads nothing.',
292
+ '- Project commands are repository code and may use the network or access local resources unless the ' +
293
+ 'execution environment prevents it.',
294
+ '',
295
+ '## Approval Command',
296
+ '',
297
+ `Approval details: ${inlineCode(getRepositoryRelativePath(
298
+ repositoryRoot,
299
+ approvalArtifacts.approvalJsonPath
300
+ ))}`,
301
+ '',
302
+ 'Optional: independently reproduce the approval hash without running project code:',
303
+ '',
304
+ codeBlock(formatIndependentHashCommand(approvalArtifacts.approvalJsonPath)),
305
+ '',
306
+ `Expected SHA-256: ${inlineCode(approvalDigest)}`,
307
+ '',
308
+ ...coveredFileVerification,
309
+ 'These checks confirm that the reviewed inputs have not changed since plan generation. They do not verify ' +
310
+ 'where the installed `dd-trace` package came from; establish package origin separately through trusted ' +
311
+ 'lockfile/integrity metadata or a verified package tarball.',
312
+ '',
313
+ 'Run the approved validation command:',
314
+ '',
315
+ codeBlock(sanitizeString(serializeApprovalCommand({
316
+ argv: validatorArgv,
317
+ cwd: repositoryRoot,
318
+ usesShell: false,
319
+ }))),
320
+ '',
321
+ `Working directory: ${inlineCode(repositoryRoot)}`,
322
+ '',
323
+ 'Approve executing the commands and temporary file operations shown in this summary?'
324
+ )
325
+ return lines.join('\n')
326
+ }
327
+
328
+ /**
329
+ * Appends one runnable framework to the bounded approval summary.
330
+ *
331
+ * @param {string[]} lines rendered summary lines
332
+ * @param {object} framework manifest framework entry
333
+ * @param {string|null|undefined} requestedScenario selected scenario
334
+ * @param {string} repositoryRoot repository root
335
+ * @returns {void}
336
+ */
337
+ function appendApprovalSummaryFramework (lines, framework, requestedScenario, repositoryRoot) {
338
+ const basicCommand = getBasicReportingCommand(framework)
339
+ const directInitialization = getDirectInitialization(framework)
340
+ const maxTestCount = framework.preflight?.maxTestCount ?? 50
341
+ lines.push(`### ${plainText(formatFrameworkLabel(framework, repositoryRoot))}`, '')
342
+
343
+ for (const setupCommand of framework.setup?.commands || []) {
344
+ appendApprovalSummaryCommand(lines, {
345
+ command: setupCommand,
346
+ label: `Project setup: ${setupCommand.id || setupCommand.description || 'setup'}`,
347
+ repositoryRoot,
348
+ runs: '1',
349
+ })
350
+ }
351
+ appendApprovalSummaryCommand(lines, {
352
+ command: getDatadogCleanCommand(basicCommand),
353
+ label: 'Test execution without Datadog',
354
+ note: 'Inherited NODE_OPTIONS and DD_* variables are removed. The command must report between 1 and ' +
355
+ `${maxTestCount} tests.`,
356
+ repositoryRoot,
357
+ runs: '1, plus 1 clean confirmation only if the Datadog run exits differently',
358
+ })
359
+ appendApprovalSummaryCommand(lines, {
360
+ command: basicCommand,
361
+ environmentOverrides: { NODE_OPTIONS: directInitialization },
362
+ label: 'Test execution with Datadog',
363
+ note: 'A second Datadog debug run occurs only when diagnosis needs debug output.',
364
+ repositoryRoot,
365
+ runs: '1, plus at most 1 Datadog debug run when needed',
366
+ })
367
+
368
+ const ciWiringSelected = !requestedScenario || requestedScenario === 'ci-wiring'
369
+ if (ciWiringSelected && framework.ciWiringCommand) {
370
+ appendApprovalSummaryCommand(lines, {
371
+ command: getCiWiringCommand(framework),
372
+ label: 'CI test execution',
373
+ note: 'A short preload probe may run when initialization reachability needs confirmation.',
374
+ repositoryRoot,
375
+ runs: '1, plus at most 1 preload probe',
376
+ })
377
+ } else if (ciWiringSelected) {
378
+ lines.push(
379
+ '**CI test execution:** not run.',
380
+ '',
381
+ `Reason: ${plainText(
382
+ framework.ciWiring?.reason || framework.ciWiring?.diagnosis || 'No replayable CI test command was selected.'
383
+ )}`,
384
+ ''
385
+ )
386
+ }
387
+
388
+ const selectedGeneratedScenario = getSelectedGeneratedScenario(requestedScenario)
389
+ const advancedSelected = !requestedScenario || selectedGeneratedScenario
390
+ const strategy = framework.generatedTestStrategy
391
+ if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
392
+ const scenarios = selectedGeneratedScenario
393
+ ? (strategy.scenarios || []).filter(scenario => scenario.id === selectedGeneratedScenario)
394
+ : strategy.scenarios || []
395
+ for (const scenario of scenarios) {
396
+ appendApprovalSummaryCommand(lines, {
397
+ command: getLocalValidationCommand(framework, scenario.runCommand),
398
+ environmentOverrides: { NODE_OPTIONS: directInitialization },
399
+ label: GENERATED_SCENARIO_DETAILS[scenario.id]?.heading || `Advanced check: ${scenario.id}`,
400
+ note: 'Runs verification, identity discovery, and feature validation; a debug run occurs only on failure.',
401
+ repositoryRoot,
402
+ runs: '3, or 4 when the debug run is needed',
403
+ })
404
+ }
405
+
406
+ lines.push('**Temporary test source:**', '')
407
+ for (const file of strategy.files || []) {
408
+ lines.push(
409
+ `${inlineCode(getRepositoryRelativePath(repositoryRoot, file.path))}`,
410
+ '',
411
+ codeBlock(file.contentLines.join('\n')),
412
+ ''
413
+ )
414
+ }
415
+ lines.push('**Files removed after validation:**', '')
416
+ for (const cleanupPath of strategy.cleanupPaths || []) {
417
+ lines.push(`- ${inlineCode(getRepositoryRelativePath(repositoryRoot, cleanupPath))}`)
418
+ }
419
+ lines.push('')
420
+ } else if (advancedSelected && strategy) {
421
+ lines.push(`**Advanced feature checks:** not run. ${plainText(strategy.reason || strategy.status)}`, '')
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Appends one exact command and its execution-relevant context to the approval summary.
427
+ *
428
+ * @param {string[]} lines rendered summary lines
429
+ * @param {object} input command summary
430
+ * @param {object} input.command structured command
431
+ * @param {Record<string, string>} [input.environmentOverrides] validator-provided readable environment
432
+ * @param {string} input.label customer-facing command label
433
+ * @param {string} [input.note] additional execution behavior
434
+ * @param {string} input.repositoryRoot repository root
435
+ * @param {string} input.runs maximum execution count
436
+ * @returns {void}
437
+ */
438
+ function appendApprovalSummaryCommand (lines, {
439
+ command,
440
+ environmentOverrides = {},
441
+ label,
442
+ note,
443
+ repositoryRoot,
444
+ runs,
445
+ }) {
446
+ lines.push(
447
+ `**${plainText(label)}**`,
448
+ '',
449
+ codeBlock(formatCommandForPlan(command, repositoryRoot, environmentOverrides)),
450
+ '',
451
+ `- Working directory: ${inlineCode(getRepositoryRelativePath(repositoryRoot, command.cwd))}`,
452
+ `- Runs: ${plainText(runs)}`,
453
+ `- Timeout: ${command.timeoutMs || 300_000} ms`
454
+ )
455
+ if (command.usesShell) lines.push(`- Shell executable: ${inlineCode(command.shell || 'platform default shell')}`)
456
+ const outputPaths = getCommandOutputPaths(command)
457
+ if (outputPaths.length > 0) {
458
+ lines.push('- Command-created outputs removed afterward: ' + outputPaths.map(outputPath => {
459
+ return inlineCode(getRepositoryRelativePath(repositoryRoot, outputPath))
460
+ }).join(', '))
461
+ }
462
+ for (const adjustment of command.localAdjustments || []) {
463
+ lines.push(`- Local adjustment: ${plainText(adjustment)}`)
464
+ }
465
+ if (note) lines.push(`- ${plainText(note)}`)
466
+ lines.push('')
467
+ }
468
+
469
+ /**
470
+ * Returns the durable customer-facing plan path written by --print-plan.
471
+ *
472
+ * @param {string} out validation output directory
473
+ * @returns {string} absolute execution plan path
474
+ */
475
+ function getExecutionPlanPath (out) {
476
+ return path.join(out, EXECUTION_PLAN_FILENAME)
477
+ }
478
+
479
+ /**
480
+ * Returns the bounded approval summary path written by --print-plan.
481
+ *
482
+ * @param {string} out validation output directory
483
+ * @returns {string} absolute approval summary path
484
+ */
485
+ function getApprovalSummaryPath (out) {
486
+ return path.join(out, APPROVAL_SUMMARY_FILENAME)
487
+ }
488
+
489
+ /**
490
+ * Refuses to render an approvable plan with a command that cannot start before setup runs.
491
+ *
492
+ * @param {object} manifest normalized validation manifest
493
+ * @param {string|null|undefined} requestedScenario selected scenario
494
+ * @returns {void}
495
+ */
496
+ function assertPlannedExecutablesAvailable (manifest, requestedScenario) {
497
+ for (const framework of manifest.frameworks) {
498
+ if (framework.status !== 'runnable') continue
499
+
500
+ const plannedCommands = getPlannedCommands(framework, requestedScenario)
501
+ for (const plannedCommand of plannedCommands) {
502
+ const executable = getUnavailableExecutable(plannedCommand.command)
503
+ if (!executable) continue
504
+
505
+ throw new Error(
506
+ `Cannot render an approvable plan because ${plannedCommand.label} for ${framework.id} uses ` +
507
+ `executable "${executable}", which is not available from ${plannedCommand.command.cwd}. ` +
508
+ 'Choose a locally available command or mark this check with its concrete setup blocker before asking ' +
509
+ 'for approval.'
510
+ )
511
+ }
512
+ for (const plannedCommand of plannedCommands) {
513
+ const suitabilityError = getCommandSuitabilityError({
514
+ command: plannedCommand.command,
515
+ framework,
516
+ label: plannedCommand.label,
517
+ repositoryRoot: manifest.repository.root,
518
+ })
519
+ if (!suitabilityError) continue
520
+ throw new Error(
521
+ `Cannot render an approvable plan because ${plannedCommand.label} for ${framework.id} ${suitabilityError}`
522
+ )
523
+ }
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Collects structured commands selected by the current plan options.
529
+ *
530
+ * @param {object} framework manifest framework entry
531
+ * @param {string|null|undefined} requestedScenario selected scenario
532
+ * @returns {{label: string, command: object}[]} planned commands
533
+ */
534
+ function getPlannedCommands (framework, requestedScenario) {
535
+ const commands = []
536
+ for (const command of framework.setup?.commands || []) {
537
+ commands.push({ label: `project setup command ${command.id || command.description || ''}`.trim(), command })
538
+ }
539
+
540
+ commands.push({ label: 'the selected test command', command: getBasicReportingCommand(framework) })
541
+
542
+ const ciWiringSelected = !requestedScenario || requestedScenario === 'ci-wiring'
543
+ if (ciWiringSelected && framework.ciWiringCommand) {
544
+ commands.push({ label: 'the CI test command', command: getCiWiringCommand(framework) })
545
+ }
546
+
547
+ const selectedGeneratedScenario = getSelectedGeneratedScenario(requestedScenario)
548
+ const advancedSelected = !requestedScenario || selectedGeneratedScenario
549
+ const strategy = framework.generatedTestStrategy
550
+ if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
551
+ const scenarios = selectedGeneratedScenario
552
+ ? (strategy.scenarios || []).filter(scenario => scenario.id === selectedGeneratedScenario)
553
+ : strategy.scenarios || []
554
+ for (const scenario of scenarios) {
555
+ commands.push({
556
+ label: `the ${scenario.id} advanced-feature command`,
557
+ command: getLocalValidationCommand(framework, scenario.runCommand),
558
+ })
559
+ }
560
+ }
561
+
562
+ return commands
563
+ }
564
+
565
+ /**
566
+ * Builds the exact validator command covered by the approval checkpoint.
567
+ *
568
+ * @param {object} input command options
569
+ * @param {string} input.approvedPlanSha256 digest of the approved manifest and options
570
+ * @param {string} input.approvalJsonPath reviewed approval JSON path
571
+ * @param {string} input.repositoryRoot repository root
572
+ * @returns {string[]} validator argv
573
+ */
574
+ function getValidatorArgv ({
575
+ approvedPlanSha256,
576
+ approvalJsonPath,
577
+ repositoryRoot,
578
+ }) {
579
+ const validatorPath = getPreferredValidatorPath(repositoryRoot)
580
+ return [
581
+ validatorPath === VALIDATOR_PATH ? process.execPath : 'node',
582
+ validatorPath,
583
+ '--run-approved-plan', getRepositoryRelativePath(repositoryRoot, approvalJsonPath),
584
+ '--sha256', approvedPlanSha256,
585
+ ]
586
+ }
587
+
588
+ /**
589
+ * Uses the stable package path when it resolves to this installed validator.
590
+ *
591
+ * @param {string} repositoryRoot repository root
592
+ * @returns {string} relative package path or exact validator path
593
+ */
594
+ function getPreferredValidatorPath (repositoryRoot) {
595
+ const directPath = path.join(repositoryRoot, 'node_modules', 'dd-trace', 'ci', 'validate-test-optimization.js')
596
+ try {
597
+ if (fs.realpathSync(directPath) === fs.realpathSync(VALIDATOR_PATH)) {
598
+ return path.relative(repositoryRoot, directPath).split(path.sep).join('/')
599
+ }
600
+ } catch {}
601
+ return VALIDATOR_PATH
602
+ }
603
+
604
+ /**
605
+ * Returns a platform-standard command for hashing the saved approval JSON independently of the validator.
606
+ *
607
+ * @param {string} approvalJsonPath absolute approval JSON path
608
+ * @returns {string} printable checksum command
609
+ */
610
+ function formatIndependentHashCommand (approvalJsonPath) {
611
+ const command = process.platform === 'win32'
612
+ ? { argv: ['certutil', '-hashfile', approvalJsonPath, 'SHA256'], cwd: path.dirname(approvalJsonPath) }
613
+ : { argv: ['shasum', '-a', '256', approvalJsonPath], cwd: path.dirname(approvalJsonPath) }
614
+ return sanitizeString(serializeApprovalCommand({ ...command, usesShell: false }))
615
+ }
616
+
617
+ function appendFrameworkExecutions (
618
+ lines,
619
+ framework,
620
+ requestedScenario,
621
+ repositoryRoot,
622
+ out,
623
+ offlineFixtureNonce
624
+ ) {
625
+ const basicCommand = getBasicReportingCommand(framework)
626
+ const frameworkLabel = formatFrameworkLabel(framework, repositoryRoot)
627
+ const directInitialization = getDirectInitialization(framework)
628
+ const maxTestCount = framework.preflight?.maxTestCount ?? 50
629
+ lines.push(`### ${plainText(frameworkLabel)}`, '')
630
+
631
+ for (const setupCommand of framework.setup?.commands || []) {
632
+ appendExecutionSection(lines, {
633
+ heading: `Project Setup: ${setupCommand.id || setupCommand.description || 'Project Setup'}`,
634
+ description: 'Prepares the project for the selected test command.',
635
+ command: setupCommand,
636
+ executions: '1',
637
+ environment: 'Use the command-specific variables shown inline. No Datadog variables are added.',
638
+ repositoryRoot,
639
+ })
640
+ }
641
+ const cleanCommand = getDatadogCleanCommand(basicCommand)
642
+ appendExecutionSection(lines, {
643
+ heading: 'Test Execution Without Datadog',
644
+ description: 'Runs the selected test command without Datadog to confirm that the tests can run normally and ' +
645
+ `that it reports between 1 and ${maxTestCount} tests.`,
646
+ command: cleanCommand,
647
+ executions: '1, plus 1 clean confirmation only if the Datadog run exits differently',
648
+ environment: `Remove inherited NODE_OPTIONS and DD_*; ${formatCommandVariableContext(cleanCommand)}`,
649
+ repositoryRoot,
650
+ })
651
+ appendExecutionSection(lines, {
652
+ heading: 'Test Execution With Datadog',
653
+ description: 'Runs the same test command with Datadog initialized and checks that test data is reported.',
654
+ command: basicCommand,
655
+ executions: '1, plus at most 1 debug rerun when needed',
656
+ environment: 'Datadog initialization is shown inline. The validator also supplies private offline response ' +
657
+ 'paths and noise-suppression settings only while this check runs.',
658
+ environmentOverrides: { NODE_OPTIONS: directInitialization },
659
+ repositoryRoot,
660
+ })
661
+
662
+ const ciWiringSelected = !requestedScenario || requestedScenario === 'ci-wiring'
663
+ if (ciWiringSelected && framework.ciWiringCommand) {
664
+ const ciCommand = getCiWiringCommand(framework)
665
+ appendExecutionSection(lines, {
666
+ heading: 'CI Test Execution',
667
+ description: 'Runs the test command with the environment recorded from the identified CI job and checks ' +
668
+ 'whether that CI configuration initializes Datadog in the final test process.',
669
+ command: ciCommand,
670
+ executions: '1, plus 1 short preload probe when needed',
671
+ environment: formatCiEnvironmentSummary(ciCommand),
672
+ repositoryRoot,
673
+ })
674
+ } else if (ciWiringSelected) {
675
+ lines.push(
676
+ '#### CI Test Execution',
677
+ '',
678
+ 'Not run.',
679
+ '',
680
+ `- Reason: ${plainText(
681
+ framework.ciWiring?.reason || framework.ciWiring?.diagnosis || 'No replayable CI test command was selected.'
682
+ )}`,
683
+ ''
684
+ )
685
+ }
686
+
687
+ const strategy = framework.generatedTestStrategy
688
+ const selectedGeneratedScenario = getSelectedGeneratedScenario(requestedScenario)
689
+ const advancedSelected = !requestedScenario || selectedGeneratedScenario
690
+ if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
691
+ const selectedScenarios = selectedGeneratedScenario
692
+ ? (strategy.scenarios || []).filter(scenario => scenario.id === selectedGeneratedScenario)
693
+ : strategy.scenarios || []
694
+ for (const scenario of selectedScenarios) {
695
+ const command = getLocalValidationCommand(framework, scenario.runCommand)
696
+ const details = GENERATED_SCENARIO_DETAILS[scenario.id] || {
697
+ heading: `Advanced Check: ${scenario.id}`,
698
+ description: 'Runs a temporary test to verify this advanced feature.',
699
+ }
700
+ appendExecutionSection(lines, {
701
+ heading: details.heading,
702
+ description: `${details.description} The framework runner is invoked directly so only this temporary test ` +
703
+ 'runs instead of the broader project test suite.',
704
+ command,
705
+ executions: '3: verify the test alone, discover its identity, then validate the feature; ' +
706
+ 'plus 1 debug rerun only on failure',
707
+ environment: 'Datadog initialization is shown inline. The validator supplies the feature setting from a ' +
708
+ 'private offline response only while this check runs.',
709
+ environmentOverrides: { NODE_OPTIONS: directInitialization },
710
+ repositoryRoot,
711
+ })
712
+ }
713
+ }
714
+
715
+ appendOfflineArtifacts(lines, {
716
+ offlineFixtureNonce,
717
+ framework,
718
+ out,
719
+ repositoryRoot,
720
+ requestedScenario,
721
+ })
722
+
723
+ if (advancedSelected && strategy && ['planned', 'verified'].includes(strategy.status)) {
724
+ lines.push(
725
+ '#### Temporary Tests Created for Advanced Checks',
726
+ '',
727
+ 'The validator creates these tests temporarily and removes them after validation.',
728
+ ''
729
+ )
730
+ for (const file of strategy.files || []) {
731
+ lines.push(
732
+ `##### ${inlineCode(getRepositoryRelativePath(repositoryRoot, file.path))}`,
733
+ '',
734
+ codeBlock(file.contentLines.join('\n')),
735
+ ''
736
+ )
737
+ }
738
+ lines.push(
739
+ '#### Temporary Test Cleanup',
740
+ '',
741
+ 'The validator removes these temporary test and state files after validation. Paths are relative to the ' +
742
+ 'repository root:',
743
+ ''
744
+ )
745
+ for (const cleanupPath of strategy.cleanupPaths || []) {
746
+ lines.push(`- ${inlineCode(getRepositoryRelativePath(repositoryRoot, cleanupPath))}`)
747
+ }
748
+ lines.push('', 'Directories created for these files are also removed when they are empty.', '')
749
+ } else if (advancedSelected && strategy) {
750
+ lines.push(
751
+ '#### Advanced Feature Checks',
752
+ '',
753
+ 'Not run.',
754
+ '',
755
+ `- Reason: ${plainText(strategy.reason || strategy.status)}`,
756
+ ''
757
+ )
758
+ }
759
+ lines.push('')
760
+ }
761
+
762
+ /**
763
+ * Describes the offline Datadog responses and event outputs used by instrumented executions.
764
+ *
765
+ * @param {string[]} lines rendered plan lines
766
+ * @param {object} input plan inputs
767
+ * @param {string} input.offlineFixtureNonce random fixture-root nonce
768
+ * @param {object} input.framework framework manifest entry
769
+ * @param {string} input.out validation result directory
770
+ * @param {string} input.repositoryRoot repository root
771
+ * @param {string|null|undefined} input.requestedScenario selected scenario
772
+ */
773
+ function appendOfflineArtifacts (lines, {
774
+ offlineFixtureNonce,
775
+ framework,
776
+ out,
777
+ repositoryRoot,
778
+ requestedScenario,
779
+ }) {
780
+ lines.push(
781
+ '#### Offline Datadog Responses',
782
+ '',
783
+ 'During normal operation, `dd-trace` downloads Test Optimization settings and test lists from Datadog. ' +
784
+ 'For this offline validation, the validator writes equivalent bounded responses to a private temporary ' +
785
+ 'directory and `dd-trace` reads them from the filesystem. No Datadog backend, Agent, or network endpoint ' +
786
+ 'is used.',
787
+ ''
788
+ )
789
+
790
+ const scenarioNames = getOfflineScenarioNames(requestedScenario)
791
+ const firstFixture = getOfflineFixturePaths({
792
+ offlineFixtureNonce,
793
+ framework,
794
+ scenarioName: scenarioNames[0],
795
+ })
796
+ const frameworkFixtureRoot = path.dirname(firstFixture.root)
797
+ lines.push(
798
+ `Private response directory: ${inlineCode(frameworkFixtureRoot)}`,
799
+ '',
800
+ 'Each check gets an isolated subdirectory containing bounded Test Optimization settings and test lists that ' +
801
+ '`dd-trace` normally receives from Datadog. Isolation prevents a baseline, feature check, or conditional ' +
802
+ 'debug rerun from overwriting another check. A debug rerun uses the same response data and adds ' +
803
+ '`DD_TRACE_DEBUG=1`.',
804
+ '',
805
+ `Captured event artifacts: ${inlineCode(getRepositoryRelativePath(
806
+ repositoryRoot,
807
+ path.join(out, 'runs', getArtifactId(framework.id))
808
+ ))}`,
809
+ '',
810
+ 'Each execution writes bounded temporary JSON payload files under `.offline-payloads/payloads/tests/`, using ' +
811
+ 'the Test Optimization payload-file layout. The temporary payload directory is removed after parsing, and a ' +
812
+ 'sanitized `events.ndjson` file remains for diagnosis. Exact fixture recipes and paths are included in the ' +
813
+ 'approval digest even though this plan summarizes their shared layout.',
814
+ ''
815
+ )
816
+ }
817
+
818
+ /**
819
+ * Renders one customer-facing validation step with its exact command and execution context.
820
+ *
821
+ * @param {string[]} lines rendered plan lines
822
+ * @param {object} input execution details
823
+ * @param {string} input.heading customer-facing check name
824
+ * @param {string} input.description reason for the execution
825
+ * @param {object} input.command manifest command
826
+ * @param {string} input.executions maximum execution count
827
+ * @param {string} input.environment environment changes made for this check
828
+ * @param {Record<string, string>} [input.environmentOverrides] readable validator-provided environment values
829
+ * @param {string} input.repositoryRoot repository root
830
+ * @returns {void}
831
+ */
832
+ function appendExecutionSection (lines, {
833
+ heading,
834
+ description,
835
+ command,
836
+ executions,
837
+ environment,
838
+ environmentOverrides = {},
839
+ repositoryRoot,
840
+ }) {
841
+ lines.push(
842
+ `#### ${plainText(heading)}`,
843
+ '',
844
+ plainText(description),
845
+ '',
846
+ 'Command:',
847
+ '',
848
+ codeBlock(formatCommandForPlan(command, repositoryRoot, environmentOverrides)),
849
+ '',
850
+ `- Working directory: ${inlineCode(getRepositoryRelativePath(repositoryRoot, command.cwd))}`,
851
+ `- Runs: ${plainText(executions)}`,
852
+ `- Environment changes: ${plainText(environment)}`,
853
+ `- Timeout: ${command.timeoutMs || 300_000} ms`
854
+ )
855
+ if (command.usesShell) lines.push(`- Shell executable: ${inlineCode(command.shell || 'platform default shell')}`)
856
+ const outputPaths = getCommandOutputPaths(command)
857
+ if (outputPaths.length > 0) {
858
+ lines.push('- Command-created outputs: ' + outputPaths.map(outputPath => {
859
+ return inlineCode(getRepositoryRelativePath(repositoryRoot, outputPath))
860
+ }).join(', ') + ' (must not exist before validation; newly created paths are removed)')
861
+ }
862
+ for (const adjustment of command.localAdjustments || []) {
863
+ lines.push(`- Local adjustment: ${plainText(adjustment)}`)
864
+ }
865
+ lines.push('')
866
+ }
867
+
868
+ /**
869
+ * Shortens a validated repository path for customer-facing plans.
870
+ *
871
+ * @param {string} repositoryRoot repository root shown at the start of the plan
872
+ * @param {string} filename absolute validated path
873
+ * @returns {string} repository-relative path when possible
874
+ */
875
+ function getRepositoryRelativePath (repositoryRoot, filename) {
876
+ const relative = path.relative(repositoryRoot, filename)
877
+ if (!relative) return '.'
878
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return filename
879
+ return relative.split(path.sep).join('/')
880
+ }
881
+
882
+ function getSelectedGeneratedScenario (requestedScenario) {
883
+ return {
884
+ efd: 'basic-pass',
885
+ atr: 'atr-fail-once',
886
+ 'test-management': 'test-management-target',
887
+ }[requestedScenario]
888
+ }
889
+
890
+ /**
891
+ * Describes command-specific variables without displaying their values.
892
+ *
893
+ * @param {object} command manifest command
894
+ * @returns {string} variable context
895
+ */
896
+ function formatCommandVariableContext (command) {
897
+ const names = Object.keys(command.env || {})
898
+ return names.length > 0
899
+ ? `keep command variables: ${names.join(', ')}`
900
+ : 'the command sets no other environment variables'
901
+ }
902
+
903
+ /**
904
+ * Renders the command and its command-specific environment in one readable block.
905
+ *
906
+ * @param {object} command manifest command
907
+ * @param {string} repositoryRoot absolute repository root
908
+ * @param {Record<string, string>} environmentOverrides readable validator-provided values
909
+ * @returns {string} readable command
910
+ */
911
+ function formatCommandForPlan (command, repositoryRoot, environmentOverrides) {
912
+ const displayCommand = command.usesShell
913
+ ? command
914
+ : {
915
+ ...command,
916
+ argv: command.argv.map((argument, index) => {
917
+ return formatCommandArgument(argument, index, command, repositoryRoot)
918
+ }),
919
+ }
920
+ const environment = { ...command.env, ...environmentOverrides }
921
+ if (command.env?.NODE_OPTIONS && environmentOverrides.NODE_OPTIONS) {
922
+ environment.NODE_OPTIONS = `${environmentOverrides.NODE_OPTIONS} ${command.env.NODE_OPTIONS}`
923
+ }
924
+ const sanitizedEnvironment = sanitizeEnv(environment) || {}
925
+ const prefix = Object.entries(sanitizedEnvironment).map(([name, value]) => {
926
+ return `${name}=${formatEnvironmentValue(value)}`
927
+ }).join(' ')
928
+ const serialized = sanitizeString(serializeApprovalCommand(displayCommand))
929
+ return prefix ? `${prefix} ${serialized}` : serialized
930
+ }
931
+
932
+ /**
933
+ * Shortens repository-contained command paths without changing their meaning from the stated working directory.
934
+ *
935
+ * @param {string} argument command argument
936
+ * @param {number} index argument index
937
+ * @param {object} command manifest command
938
+ * @param {string} repositoryRoot absolute repository root
939
+ * @returns {string} readable argument
940
+ */
941
+ function formatCommandArgument (argument, index, command, repositoryRoot) {
942
+ if (index === 0 && path.resolve(argument) === path.resolve(process.execPath)) return 'node'
943
+ if (!path.isAbsolute(argument) || !isPathInside(repositoryRoot, argument)) return argument
944
+ return path.relative(command.cwd, argument) || '.'
945
+ }
946
+
947
+ /**
948
+ * Quotes one environment value only when its characters require it.
949
+ *
950
+ * @param {string} value environment value
951
+ * @returns {string} readable assignment value
952
+ */
953
+ function formatEnvironmentValue (value) {
954
+ return serializeApprovalCommand({ argv: [String(value)], usesShell: false })
955
+ }
956
+
957
+ /**
958
+ * Checks whether a path remains within a parent directory.
959
+ *
960
+ * @param {string} parent parent directory
961
+ * @param {string} child candidate child path
962
+ * @returns {boolean} whether child is within parent
963
+ */
964
+ function isPathInside (parent, child) {
965
+ const relative = path.relative(parent, child)
966
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
967
+ }
968
+
969
+ /**
970
+ * Returns the customer-facing Datadog preload required by one framework.
971
+ *
972
+ * @param {object} framework manifest framework entry
973
+ * @returns {string} NODE_OPTIONS value
974
+ */
975
+ function getDirectInitialization (framework) {
976
+ return framework.framework === 'vitest'
977
+ ? '--import dd-trace/register.js -r dd-trace/ci/init'
978
+ : '-r dd-trace/ci/init'
979
+ }
980
+
981
+ /**
982
+ * Describes the variables captured from the selected CI job.
983
+ *
984
+ * @param {object} command CI test command
985
+ * @returns {string} customer-facing environment summary
986
+ */
987
+ function formatCiEnvironmentSummary (command) {
988
+ const names = Object.keys(command.env || {})
989
+ const datadogNames = names.filter(name => name.startsWith('DD_') || name === 'NODE_OPTIONS')
990
+ if (datadogNames.length === 0) {
991
+ return 'The selected CI job supplies no Datadog variables. Other recorded CI variables, if any, are shown ' +
992
+ 'inline.'
993
+ }
994
+ return 'Variables recorded from the selected CI job are shown inline. Secret-like values are redacted.'
995
+ }
996
+
997
+ /**
998
+ * Names a framework using the package or project that contributors recognize.
999
+ *
1000
+ * @param {object} framework manifest framework entry
1001
+ * @param {string} repositoryRoot absolute repository root
1002
+ * @returns {string} customer-facing framework label
1003
+ */
1004
+ function formatFrameworkLabel (framework, repositoryRoot) {
1005
+ const frameworkName = FRAMEWORK_NAMES[framework.framework] || framework.framework || 'Test'
1006
+ const projectName = framework.project?.name
1007
+ if (projectName && projectName !== 'root') return `${frameworkName} tests for ${projectName}`
1008
+ const projectRoot = framework.project?.root
1009
+ const relativeRoot = projectRoot && getRepositoryRelativePath(repositoryRoot, projectRoot)
1010
+ return relativeRoot && relativeRoot !== '.'
1011
+ ? `${frameworkName} tests in ${relativeRoot}`
1012
+ : `${frameworkName} tests`
1013
+ }
1014
+
1015
+ /**
1016
+ * Lists each executable identity once instead of repeating it under every command.
1017
+ *
1018
+ * @param {string[]} lines rendered plan lines
1019
+ * @param {object} manifest normalized validation manifest
1020
+ * @param {string|null|undefined} requestedScenario selected scenario
1021
+ * @returns {void}
1022
+ */
1023
+ function appendCommandIntegrity (lines, manifest, requestedScenario) {
1024
+ const executables = new Map()
1025
+ for (const framework of manifest.frameworks) {
1026
+ if (framework.status !== 'runnable') continue
1027
+
1028
+ for (const { command } of getPlannedCommands(framework, requestedScenario)) {
1029
+ const executable = getApprovedExecutable(command)
1030
+ if (executable) {
1031
+ for (const identity of [executable, ...(executable.delegated || [])]) {
1032
+ const key = `${identity.invocationPath}:${identity.path}:${identity.sha256}`
1033
+ const entry = executables.get(key) || { executable: identity, labels: new Set() }
1034
+ entry.labels.add(getExecutableLabel(command, identity.invocationPath))
1035
+ executables.set(key, entry)
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+ if (executables.size === 0) return
1041
+
1042
+ lines.push(
1043
+ '## Executables Used',
1044
+ '',
1045
+ 'These programs start the commands shown above. The validator records their fingerprints internally and ' +
1046
+ 'stops if an executable or PATH selection changes after approval. This confirms that the approved programs ' +
1047
+ 'did not change; it does not establish that project scripts, packages, or subprocesses are safe.',
1048
+ ''
1049
+ )
1050
+ for (const { executable, labels } of executables.values()) {
1051
+ const canonicalTarget = executable.invocationPath === executable.path
1052
+ ? ''
1053
+ : ` (verified target: ${inlineCode(executable.path)})`
1054
+ lines.push(`- ${[...labels].sort().join(', ')}: ${inlineCode(executable.invocationPath)}${canonicalTarget}`)
1055
+ }
1056
+ lines.push('')
1057
+ }
1058
+
1059
+ function getExecutableLabel (command, invocationPath) {
1060
+ const name = path.basename(invocationPath).replace(/\.(?:bat|cmd|exe)$/i, '').toLowerCase()
1061
+ if (command.usesShell) {
1062
+ return {
1063
+ bash: 'Bash shell',
1064
+ sh: 'POSIX shell',
1065
+ zsh: 'Zsh shell',
1066
+ }[name] || `${name} shell`
1067
+ }
1068
+ return {
1069
+ bash: 'Bash shell',
1070
+ node: 'Node.js',
1071
+ npm: 'npm',
1072
+ npx: 'npx',
1073
+ pnpm: 'pnpm',
1074
+ sh: 'POSIX shell',
1075
+ yarn: 'Yarn',
1076
+ zsh: 'Zsh shell',
1077
+ }[name] || name
1078
+ }
1079
+
1080
+ /**
1081
+ * Converts manifest framework statuses into customer-facing plan text.
1082
+ *
1083
+ * @param {string} status manifest framework status
1084
+ * @returns {string} customer-facing status
1085
+ */
1086
+ function formatFrameworkStatus (status) {
1087
+ return {
1088
+ runnable: 'will be validated',
1089
+ detected_not_runnable: 'detected, but no runnable command was found',
1090
+ requires_external_service: 'requires an external service',
1091
+ requires_manual_setup: 'requires additional setup',
1092
+ unsupported_by_validator: 'not supported by this validator',
1093
+ unknown: 'could not be determined',
1094
+ }[status] || plainText(status)
1095
+ }
1096
+
1097
+ function codeBlock (value) {
1098
+ return `\`\`\`text\n${visibleMultilineText(value).replaceAll('```', String.raw`\u0060\u0060\u0060`)}\n\`\`\``
1099
+ }
1100
+
1101
+ function inlineCode (value) {
1102
+ return `\`${plainText(value).replaceAll('`', String.raw`\u0060`)}\``
1103
+ }
1104
+
1105
+ function plainText (value) {
1106
+ return sanitizeString(String(value ?? '')).replaceAll(CONTROL_CHARACTERS_PATTERN, ' ').trim()
1107
+ }
1108
+
1109
+ function visibleMultilineText (value) {
1110
+ return String(value ?? '')
1111
+ .replaceAll('\r\n', '\n')
1112
+ .replaceAll('\r', String.raw`\r`)
1113
+ // eslint-disable-next-line prefer-regex-literals
1114
+ .replaceAll(new RegExp(String.raw`[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]`, 'g'), character => {
1115
+ return String.raw`\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
1116
+ })
1117
+ .trim()
1118
+ }
1119
+
1120
+ module.exports = { formatExecutionPlan, getApprovalSummaryPath, getExecutionPlanPath }