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,862 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+
5
+ const { getArtifactId } = require('./artifact-id')
6
+ const {
7
+ MAX_GENERATED_FILES,
8
+ getGeneratedFileContentError,
9
+ } = require('./generated-file-policy')
10
+ const { getInlineDatadogInitialization } = require('./local-command')
11
+ const {
12
+ hasUnsafeExecutionCharacter,
13
+ hasUnsafeInvisibleCharacter,
14
+ isSensitiveName,
15
+ sanitizeForReport,
16
+ sanitizeString,
17
+ } = require('./redaction')
18
+
19
+ const FRAMEWORKS = new Set([
20
+ 'jest',
21
+ 'vitest',
22
+ 'mocha',
23
+ 'cucumber',
24
+ 'cypress',
25
+ 'playwright',
26
+ 'node:test',
27
+ 'ava',
28
+ 'tap',
29
+ 'jasmine',
30
+ 'karma',
31
+ 'uvu',
32
+ 'testcafe',
33
+ 'custom',
34
+ 'unknown',
35
+ ])
36
+
37
+ const STATUSES = new Set([
38
+ 'runnable',
39
+ 'detected_not_runnable',
40
+ 'requires_external_service',
41
+ 'requires_manual_setup',
42
+ 'unsupported_by_validator',
43
+ 'unknown',
44
+ ])
45
+ const CI_WIRING_STATUSES = new Set([
46
+ 'pass',
47
+ 'fail',
48
+ 'skip',
49
+ 'unknown',
50
+ ])
51
+ const CI_WIRING_REPLAYABILITIES = new Set(['replayable', 'not_replayable'])
52
+ const CI_INITIALIZATION_STATUSES = new Set(['configured', 'not_configured', 'unknown'])
53
+ const UNRESOLVED_PLACEHOLDER_PATTERN = /\$\{[^}]+\}/
54
+ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
55
+ const MAX_COMMAND_TIMEOUT_MS = 30 * 60 * 1000
56
+ const MAX_FRAMEWORKS = 100
57
+ const MAX_MANIFEST_ARRAY_ENTRIES = 1000
58
+ const MAX_SETUP_COMMANDS = 100
59
+ const MAX_VALIDATION_ERRORS = 50
60
+ const MAX_REPRESENTATIVE_TESTS = 1000
61
+ const SECRET_PLACEHOLDER = 'dd-validation-placeholder'
62
+ const SAFE_SECRET_FIELD_VALUES = new Set(['', '0', '1', 'false', 'true', 'none', 'disabled'])
63
+ const COMMAND_FIELDS = new Set([
64
+ 'argv',
65
+ 'cwd',
66
+ 'description',
67
+ 'env',
68
+ 'required',
69
+ 'requiredEnvVars',
70
+ 'shell',
71
+ 'shellCommand',
72
+ 'shellReason',
73
+ 'timeoutMs',
74
+ 'usesShell',
75
+ 'id',
76
+ 'outputPaths',
77
+ ])
78
+
79
+ const GENERATED_SCENARIO_IDS = new Set([
80
+ 'basic-pass',
81
+ 'atr-fail-once',
82
+ 'test-management-target',
83
+ ])
84
+ const GENERATED_SCENARIO_EXIT_CODES = {
85
+ 'basic-pass': 0,
86
+ 'atr-fail-once': 1,
87
+ 'test-management-target': 0,
88
+ }
89
+
90
+ function validateManifest (manifest) {
91
+ const errors = createErrorCollector()
92
+
93
+ if (!manifest || typeof manifest !== 'object') {
94
+ return ['Manifest must be a JSON object.']
95
+ }
96
+
97
+ requiredString(manifest, 'schemaVersion', errors)
98
+ requiredObject(manifest, 'repository', errors)
99
+ requiredObject(manifest, 'environment', errors)
100
+ requiredArray(manifest, 'frameworks', errors)
101
+ if (Array.isArray(manifest.frameworks) && manifest.frameworks.length === 0) {
102
+ errors.push('frameworks must include at least one framework entry.')
103
+ }
104
+ validateArrayLimit(manifest, 'frameworks', MAX_FRAMEWORKS, errors)
105
+ validateArrayLimit(manifest, 'omitted', MAX_MANIFEST_ARRAY_ENTRIES, errors)
106
+ validateArrayLimit(manifest, 'omittedTestCommands', MAX_MANIFEST_ARRAY_ENTRIES, errors)
107
+
108
+ if (manifest.repository) {
109
+ requiredAbsolutePath(manifest.repository, 'root', errors)
110
+ }
111
+
112
+ if (manifest.ciDiscovery) {
113
+ validateCiDiscovery(manifest.ciDiscovery, 'ciDiscovery', errors)
114
+ }
115
+
116
+ if (Array.isArray(manifest.frameworks)) {
117
+ const frameworks = manifest.frameworks.slice(0, MAX_FRAMEWORKS)
118
+ validateUniqueFrameworkIds(frameworks, errors)
119
+ validateUniqueArtifactIds(frameworks, errors)
120
+ validateDuplicateRunnableCoverage(frameworks, errors)
121
+ validateGeneratedPathCollisions(frameworks, errors)
122
+ for (const [index, framework] of frameworks.entries()) {
123
+ validateFramework(framework, index, errors)
124
+ }
125
+ }
126
+
127
+ validateRepositoryContainedPaths(manifest, errors)
128
+
129
+ return errors.finalize()
130
+ }
131
+
132
+ /**
133
+ * Rejects generated files or cleanup targets shared by multiple framework entries.
134
+ *
135
+ * @param {object[]} frameworks manifest framework entries
136
+ * @param {{push: function(string): void}} errors bounded validation error collector
137
+ * @returns {void}
138
+ */
139
+ function validateGeneratedPathCollisions (frameworks, errors) {
140
+ const seen = new Map()
141
+ for (const [index, framework] of frameworks.entries()) {
142
+ const strategy = framework?.generatedTestStrategy
143
+ const frameworkPaths = new Map([
144
+ ...limitedArray(strategy?.files, MAX_GENERATED_FILES).map(file => file?.path),
145
+ ...limitedArray(strategy?.cleanupPaths, MAX_MANIFEST_ARRAY_ENTRIES),
146
+ ].filter(filename => typeof filename === 'string' && path.isAbsolute(filename))
147
+ .map(filename => [path.normalize(filename), filename]))
148
+
149
+ for (const [key, filename] of frameworkPaths) {
150
+ const previous = seen.get(key)
151
+ if (previous === undefined) {
152
+ seen.set(key, index)
153
+ continue
154
+ }
155
+ errors.push(
156
+ `frameworks[${index}].generatedTestStrategy path ${JSON.stringify(filename)} conflicts with ` +
157
+ `frameworks[${previous}].generatedTestStrategy. Generated files and cleanup paths must be unique across ` +
158
+ 'framework entries.'
159
+ )
160
+ }
161
+ }
162
+ }
163
+
164
+ function validateDuplicateRunnableCoverage (frameworks, errors) {
165
+ const seen = new Map()
166
+ for (const [index, framework] of frameworks.entries()) {
167
+ if (framework?.status !== 'runnable' || !framework.ciWiringCommand) continue
168
+ const key = JSON.stringify({
169
+ framework: framework.framework,
170
+ projectRoot: framework.project?.root,
171
+ existingTestCommand: commandExecutionShape(framework.existingTestCommand),
172
+ ciWiringCommand: commandExecutionShape(framework.ciWiringCommand),
173
+ ciInitializesDatadog: commandInitializesDatadog(framework.ciWiringCommand),
174
+ })
175
+ const previous = seen.get(key)
176
+ if (previous === undefined) {
177
+ seen.set(key, index)
178
+ continue
179
+ }
180
+ errors.push(
181
+ `frameworks[${index}] duplicates runnable framework and CI command coverage from frameworks[${previous}]. ` +
182
+ 'Keep one representative framework entry and record the other CI job as an omitted or duplicate candidate.'
183
+ )
184
+ }
185
+ }
186
+
187
+ function validateUniqueArtifactIds (frameworks, errors) {
188
+ const seen = new Map()
189
+ for (const [index, framework] of frameworks.entries()) {
190
+ if (typeof framework?.id !== 'string') continue
191
+ const artifactId = getArtifactId(framework.id)
192
+ const previous = seen.get(artifactId)
193
+ if (previous !== undefined && previous !== framework.id) {
194
+ errors.push(
195
+ `frameworks[${index}].id collides with another framework artifact identifier after normalization.`
196
+ )
197
+ } else {
198
+ seen.set(artifactId, framework.id)
199
+ }
200
+ }
201
+ }
202
+
203
+ function commandExecutionShape (command) {
204
+ if (!command) return null
205
+ return {
206
+ argv: command.argv,
207
+ cwd: command.cwd,
208
+ shell: command.shell,
209
+ shellCommand: command.shellCommand,
210
+ usesShell: Boolean(command.usesShell),
211
+ }
212
+ }
213
+
214
+ function commandInitializesDatadog (command) {
215
+ const values = [
216
+ ...(command?.argv || []),
217
+ command?.shellCommand,
218
+ command?.env?.NODE_OPTIONS,
219
+ ].filter(Boolean).join(' ')
220
+ return /dd-trace\/ci\/init/.test(values)
221
+ }
222
+
223
+ function validateRepositoryContainedPaths (manifest, errors) {
224
+ const repositoryRoot = manifest.repository?.root
225
+ if (typeof repositoryRoot !== 'string' || !path.isAbsolute(repositoryRoot)) return
226
+
227
+ const frameworks = Array.isArray(manifest.frameworks) ? manifest.frameworks.slice(0, MAX_FRAMEWORKS) : []
228
+ for (const [index, framework] of frameworks.entries()) {
229
+ if (!framework || typeof framework !== 'object' || Array.isArray(framework)) continue
230
+ const prefix = `frameworks[${index}]`
231
+ containedPath(repositoryRoot, framework.project?.root, `${prefix}.project.root`, errors)
232
+ containedPath(repositoryRoot, framework.project?.packageJson, `${prefix}.project.packageJson`, errors)
233
+ for (const [configIndex, configFile] of
234
+ limitedArray(framework.project?.configFiles, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
235
+ containedPath(repositoryRoot, configFile, `${prefix}.project.configFiles[${configIndex}]`, errors)
236
+ }
237
+
238
+ for (const [name, command] of getFrameworkCommands(framework)) {
239
+ containedPath(repositoryRoot, command?.cwd, `${prefix}.${name}.cwd`, errors)
240
+ }
241
+
242
+ containedPath(repositoryRoot, framework.ciWiring?.configFile, `${prefix}.ciWiring.configFile`, errors)
243
+ containedPath(repositoryRoot, framework.ciWiring?.workingDirectory, `${prefix}.ciWiring.workingDirectory`, errors)
244
+
245
+ const strategy = framework.generatedTestStrategy
246
+ containedPath(repositoryRoot, strategy?.testDirectory, `${prefix}.generatedTestStrategy.testDirectory`, errors)
247
+ for (const [fileIndex, file] of limitedArray(strategy?.files, MAX_GENERATED_FILES).entries()) {
248
+ containedPath(repositoryRoot, file?.path, `${prefix}.generatedTestStrategy.files[${fileIndex}].path`, errors)
249
+ }
250
+ for (const [cleanupIndex, cleanupPath] of
251
+ limitedArray(strategy?.cleanupPaths, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
252
+ containedPath(
253
+ repositoryRoot,
254
+ cleanupPath,
255
+ `${prefix}.generatedTestStrategy.cleanupPaths[${cleanupIndex}]`,
256
+ errors
257
+ )
258
+ }
259
+ for (const [scenarioIndex, scenario] of
260
+ limitedArray(strategy?.scenarios, GENERATED_SCENARIO_IDS.size).entries()) {
261
+ for (const [identityIndex, identity] of
262
+ limitedArray(scenario?.testIdentities, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
263
+ containedPath(
264
+ repositoryRoot,
265
+ identity?.file,
266
+ `${prefix}.generatedTestStrategy.scenarios[${scenarioIndex}].testIdentities[${identityIndex}].file`,
267
+ errors
268
+ )
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ function getFrameworkCommands (framework) {
275
+ const commands = []
276
+ for (const name of ['existingTestCommand', 'ciWiringCommand']) {
277
+ if (framework[name]) commands.push([name, framework[name]])
278
+ }
279
+ for (const [index, command] of limitedArray(framework.setup?.commands, MAX_SETUP_COMMANDS).entries()) {
280
+ commands.push([`setup.commands[${index}]`, command])
281
+ }
282
+ for (const [index, scenario] of
283
+ limitedArray(framework.generatedTestStrategy?.scenarios, GENERATED_SCENARIO_IDS.size).entries()) {
284
+ if (scenario?.runCommand) {
285
+ commands.push([`generatedTestStrategy.scenarios[${index}].runCommand`, scenario.runCommand])
286
+ }
287
+ }
288
+ return commands
289
+ }
290
+
291
+ function containedPath (root, candidate, key, errors) {
292
+ if (typeof candidate !== 'string' || !path.isAbsolute(candidate)) return
293
+ const relative = path.relative(path.resolve(root), path.resolve(candidate))
294
+ if (relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))) return
295
+ errors.push(`${key} must be inside repository.root.`)
296
+ }
297
+
298
+ function validateCiDiscovery (ciDiscovery, prefix, errors) {
299
+ if (!ciDiscovery || typeof ciDiscovery !== 'object' || Array.isArray(ciDiscovery)) {
300
+ errors.push(`${prefix} must be an object when present.`)
301
+ return
302
+ }
303
+
304
+ for (const field of ['searched', 'found', 'staticFound', 'warnings', 'notes', 'contradictions']) {
305
+ if (ciDiscovery[field] !== undefined) {
306
+ if (Array.isArray(ciDiscovery[field])) {
307
+ validateStringArray(ciDiscovery, field, errors, prefix)
308
+ } else {
309
+ errors.push(`${prefix}.${field} must be an array when present.`)
310
+ }
311
+ }
312
+ }
313
+
314
+ if (ciDiscovery.method !== undefined && typeof ciDiscovery.method !== 'string') {
315
+ errors.push(`${prefix}.method must be a string when present.`)
316
+ }
317
+ }
318
+
319
+ function validateFramework (framework, index, errors) {
320
+ const prefix = `frameworks[${index}]`
321
+ if (!framework || typeof framework !== 'object' || Array.isArray(framework)) {
322
+ errors.push(`${prefix} must be an object.`)
323
+ return
324
+ }
325
+ requiredString(framework, 'id', errors, prefix)
326
+ enumString(framework, 'framework', FRAMEWORKS, errors, prefix)
327
+ enumString(framework, 'status', STATUSES, errors, prefix)
328
+ requiredObject(framework, 'project', errors, prefix)
329
+
330
+ if (framework.project) {
331
+ requiredAbsolutePath(framework.project, 'root', errors, `${prefix}.project`)
332
+ optionalAbsolutePath(framework.project, 'packageJson', errors, `${prefix}.project`)
333
+ optionalAbsolutePathArray(framework.project, 'configFiles', errors, `${prefix}.project`)
334
+ validateArrayLimit(framework.project, 'configFiles', MAX_MANIFEST_ARRAY_ENTRIES, errors, `${prefix}.project`)
335
+ }
336
+
337
+ if (framework.status === 'runnable') {
338
+ requiredCommand(framework, 'existingTestCommand', errors, prefix, { datadogClean: true })
339
+ validateDatadogCleanCommand(framework.existingTestCommand, `${prefix}.existingTestCommand`, errors)
340
+ requiredObject(framework, 'preflight', errors, prefix)
341
+ validatePreflight(framework.preflight, `${prefix}.preflight`, errors)
342
+ requiredObject(framework, 'ciWiring', errors, prefix)
343
+ } else {
344
+ requireNonEmptyNotes(framework, errors, prefix)
345
+ }
346
+
347
+ if (framework.ciWiringCommand) {
348
+ requiredCommand(framework, 'ciWiringCommand', errors, prefix)
349
+ }
350
+
351
+ if (framework.ciWiring) {
352
+ validateCiWiring(framework, prefix, errors)
353
+ }
354
+
355
+ if (framework.forcedLocalCommand !== undefined) {
356
+ errors.push(
357
+ `${prefix}.forcedLocalCommand is not supported. Use the focused existingTestCommand for Basic Reporting ` +
358
+ 'and ciWiringCommand for the CI-shaped replay.'
359
+ )
360
+ }
361
+
362
+ if (framework.setup?.commands) {
363
+ if (Array.isArray(framework.setup.commands)) {
364
+ validateArrayLimit(framework.setup, 'commands', MAX_SETUP_COMMANDS, errors, `${prefix}.setup`)
365
+ for (const [commandIndex, command] of framework.setup.commands.slice(0, MAX_SETUP_COMMANDS).entries()) {
366
+ requiredCommand({ command }, 'command', errors, `${prefix}.setup.commands[${commandIndex}]`)
367
+ }
368
+ } else {
369
+ errors.push(`${prefix}.setup.commands must be an array.`)
370
+ }
371
+ }
372
+
373
+ if (framework.generatedTestStrategy) {
374
+ validateGeneratedTestStrategy(framework.generatedTestStrategy, `${prefix}.generatedTestStrategy`, errors)
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Validates the approved upper bound for a representative test command.
380
+ *
381
+ * @param {object} preflight preflight declaration
382
+ * @param {string} prefix manifest field path
383
+ * @param {{push: function(string): void}} errors bounded validation error collector
384
+ * @returns {void}
385
+ */
386
+ function validatePreflight (preflight, prefix, errors) {
387
+ if (!preflight || typeof preflight !== 'object' || Array.isArray(preflight)) return
388
+
389
+ if (!Number.isInteger(preflight.maxTestCount) || preflight.maxTestCount < 1) {
390
+ errors.push(`${prefix}.maxTestCount must be a positive integer.`)
391
+ } else if (preflight.maxTestCount > MAX_REPRESENTATIVE_TESTS) {
392
+ errors.push(`${prefix}.maxTestCount must not exceed ${MAX_REPRESENTATIVE_TESTS}.`)
393
+ }
394
+ }
395
+
396
+ function validateGeneratedTestStrategy (strategy, prefix, errors) {
397
+ if (!['planned', 'verified', 'proposed', 'not_possible'].includes(strategy.status)) {
398
+ errors.push(`${prefix}.status must be planned, verified, proposed, or not_possible.`)
399
+ }
400
+
401
+ const completeStrategy = strategy.status === 'planned' || strategy.status === 'verified'
402
+ if (completeStrategy) {
403
+ requiredArray(strategy, 'files', errors, prefix)
404
+ requiredArray(strategy, 'scenarios', errors, prefix)
405
+ requiredArray(strategy, 'cleanupPaths', errors, prefix)
406
+ validateCompleteGeneratedScenarioSet(strategy, prefix, errors)
407
+ } else if ((strategy.status === 'proposed' || strategy.status === 'not_possible') &&
408
+ (typeof strategy.reason !== 'string' || strategy.reason.trim() === '')) {
409
+ errors.push(`${prefix}.reason must explain why the generated test strategy is ${strategy.status}.`)
410
+ }
411
+
412
+ if (Array.isArray(strategy.files)) {
413
+ if (strategy.files.length > MAX_GENERATED_FILES) {
414
+ errors.push(`${prefix}.files must contain at most ${MAX_GENERATED_FILES} generated files.`)
415
+ }
416
+ for (const [index, file] of strategy.files.slice(0, MAX_GENERATED_FILES).entries()) {
417
+ requiredAbsolutePath(file, 'path', errors, `${prefix}.files[${index}]`)
418
+ requiredArray(file, 'contentLines', errors, `${prefix}.files[${index}]`)
419
+ validateStringArray(file, 'contentLines', errors, `${prefix}.files[${index}]`)
420
+ const policyError = getGeneratedFileContentError(file.contentLines)
421
+ if (policyError) errors.push(`${prefix}.files[${index}].contentLines ${policyError}.`)
422
+ }
423
+ }
424
+
425
+ if (Array.isArray(strategy.scenarios)) {
426
+ validateArrayLimit(strategy, 'scenarios', GENERATED_SCENARIO_IDS.size, errors, prefix)
427
+ for (const [index, scenario] of strategy.scenarios.slice(0, GENERATED_SCENARIO_IDS.size).entries()) {
428
+ requiredString(scenario, 'id', errors, `${prefix}.scenarios[${index}]`)
429
+ enumString(scenario, 'id', GENERATED_SCENARIO_IDS, errors, `${prefix}.scenarios[${index}]`)
430
+ requiredCommand(scenario, 'runCommand', errors, `${prefix}.scenarios[${index}]`, { datadogClean: true })
431
+ validateDatadogCleanCommand(scenario.runCommand, `${prefix}.scenarios[${index}].runCommand`, errors)
432
+ validateScenarioIdentities(
433
+ scenario,
434
+ `${prefix}.scenarios[${index}]`,
435
+ errors,
436
+ completeStrategy
437
+ )
438
+ if (completeStrategy) {
439
+ validateGeneratedScenarioOutcome(scenario, `${prefix}.scenarios[${index}]`, errors)
440
+ }
441
+ }
442
+ }
443
+
444
+ optionalAbsolutePath(strategy, 'testDirectory', errors, prefix)
445
+ optionalAbsolutePathArray(strategy, 'cleanupPaths', errors, prefix)
446
+ }
447
+
448
+ function validateCiWiring (framework, prefix, errors) {
449
+ const ciWiring = framework.ciWiring
450
+ if (!ciWiring || typeof ciWiring !== 'object' || Array.isArray(ciWiring)) {
451
+ errors.push(`${prefix}.ciWiring must be an object when present.`)
452
+ return
453
+ }
454
+
455
+ if (!CI_WIRING_STATUSES.has(ciWiring.status)) {
456
+ errors.push(`${prefix}.ciWiring.status must be pass, fail, skip, or unknown.`)
457
+ }
458
+
459
+ if (!CI_WIRING_REPLAYABILITIES.has(ciWiring.replayability)) {
460
+ errors.push(`${prefix}.ciWiring.replayability must be replayable or not_replayable.`)
461
+ }
462
+ if (Object.hasOwn(ciWiring, 'ciWiringCommand')) {
463
+ errors.push(`${prefix}.ciWiring.ciWiringCommand is misplaced; use ${prefix}.ciWiringCommand.`)
464
+ }
465
+ if (ciWiring.replayability === 'replayable' && !framework.ciWiringCommand) {
466
+ errors.push(`${prefix}.ciWiringCommand is required when ${prefix}.ciWiring.replayability is replayable.`)
467
+ }
468
+ if (ciWiring.replayability === 'not_replayable') {
469
+ if (ciWiring.status === 'pass' || ciWiring.status === 'fail') {
470
+ errors.push(`${prefix}.ciWiring.status must be skip or unknown when replayability is not_replayable.`)
471
+ }
472
+ if (framework.ciWiringCommand) {
473
+ errors.push(`${prefix}.ciWiringCommand must be omitted when ${prefix}.ciWiring.replayability is not_replayable.`)
474
+ }
475
+ if (!hasNonEmptyString(ciWiring.replayBlocker)) {
476
+ errors.push(`${prefix}.ciWiring.replayBlocker must explain why CI replay is not_replayable.`)
477
+ }
478
+ }
479
+
480
+ if (ciWiring.initialization !== undefined) {
481
+ validateCiInitialization(ciWiring.initialization, `${prefix}.ciWiring.initialization`, errors)
482
+ }
483
+
484
+ if (ciWiring.initialization?.status === 'not_configured' &&
485
+ commandInitializesDatadog(framework.ciWiringCommand)) {
486
+ errors.push(
487
+ `${prefix}.ciWiring.initialization.status is not_configured, but ${prefix}.ciWiringCommand adds dd-trace ` +
488
+ 'initialization. The replay command must preserve the discovered CI configuration; remove the added ' +
489
+ 'initialization or correct the initialization status and evidence.'
490
+ )
491
+ }
492
+
493
+ if (framework.ciWiringCommand) {
494
+ for (const field of ['provider', 'configFile', 'job', 'step', 'whySelected']) {
495
+ requiredString(ciWiring, field, errors, `${prefix}.ciWiring`)
496
+ }
497
+ requiredAbsolutePath(ciWiring, 'configFile', errors, `${prefix}.ciWiring`)
498
+ requiredAbsolutePath(ciWiring, 'workingDirectory', errors, `${prefix}.ciWiring`)
499
+ if (path.resolve(ciWiring.workingDirectory || '') !== path.resolve(framework.ciWiringCommand.cwd || '')) {
500
+ errors.push(`${prefix}.ciWiringCommand.cwd must match ${prefix}.ciWiring.workingDirectory.`)
501
+ }
502
+ if (ciWiring.shell !== undefined && ciWiring.shell !== null) {
503
+ if (typeof ciWiring.shell !== 'string' || ciWiring.shell.trim() === '') {
504
+ errors.push(`${prefix}.ciWiring.shell must be a non-empty string when present.`)
505
+ } else if (hasUnsafeExecutionCharacter(ciWiring.shell)) {
506
+ errors.push(`${prefix}.ciWiring.shell must not contain invisible or control characters.`)
507
+ }
508
+ }
509
+ }
510
+
511
+ if ((ciWiring.status === 'skip' || ciWiring.status === 'unknown') &&
512
+ !hasNonEmptyString(ciWiring.diagnosis) && !hasNonEmptyString(ciWiring.reason)) {
513
+ errors.push(`${prefix}.ciWiring must explain why CI wiring is ${ciWiring.status}.`)
514
+ }
515
+ }
516
+
517
+ function validateCiInitialization (initialization, prefix, errors) {
518
+ if (!initialization || typeof initialization !== 'object' || Array.isArray(initialization)) {
519
+ errors.push(`${prefix} must be an object when present.`)
520
+ return
521
+ }
522
+
523
+ if (!CI_INITIALIZATION_STATUSES.has(initialization.status)) {
524
+ errors.push(`${prefix}.status must be configured, not_configured, or unknown.`)
525
+ }
526
+ if (Array.isArray(initialization.evidence)) {
527
+ validateStringArray(initialization, 'evidence', errors, prefix)
528
+ if (initialization.status !== 'unknown' && initialization.evidence.length === 0) {
529
+ errors.push(`${prefix}.evidence must explain the ${initialization.status} conclusion.`)
530
+ }
531
+ } else {
532
+ errors.push(`${prefix}.evidence must be an array.`)
533
+ }
534
+ }
535
+
536
+ function hasNonEmptyString (value) {
537
+ return typeof value === 'string' && value.trim() !== ''
538
+ }
539
+
540
+ function validateDatadogCleanCommand (command, prefix, errors) {
541
+ for (const [name, value] of Object.entries(command?.env || {})) {
542
+ if (name.startsWith('DD_') || (name === 'NODE_OPTIONS' && /dd-trace/.test(String(value)))) {
543
+ errors.push(`${prefix}.env.${name} must not configure Datadog initialization for local validation.`)
544
+ }
545
+ }
546
+
547
+ const inlineInitialization = getInlineDatadogInitialization(command)
548
+ if (inlineInitialization) {
549
+ errors.push(
550
+ `${prefix} ${inlineInitialization} and must be Datadog-clean for local validation. ` +
551
+ 'Remove the inline initialization; preserve exact CI initialization only in ciWiringCommand.'
552
+ )
553
+ }
554
+ }
555
+
556
+ function validateGeneratedScenarioOutcome (scenario, prefix, errors) {
557
+ const expected = scenario.expectedWithoutDatadog
558
+ if (!expected || typeof expected !== 'object' || Array.isArray(expected)) {
559
+ errors.push(`${prefix}.expectedWithoutDatadog must be an object when generatedTestStrategy is planned or verified.`)
560
+ return
561
+ }
562
+
563
+ const expectedExitCode = GENERATED_SCENARIO_EXIT_CODES[scenario.id]
564
+ if (expectedExitCode !== undefined && expected.exitCode !== expectedExitCode) {
565
+ errors.push(`${prefix}.expectedWithoutDatadog.exitCode must be ${expectedExitCode} for ${scenario.id}.`)
566
+ }
567
+ if (expected.observedTestCount !== 1) {
568
+ errors.push(`${prefix}.expectedWithoutDatadog.observedTestCount must be 1 so the command isolates this scenario.`)
569
+ }
570
+ }
571
+
572
+ function validateCompleteGeneratedScenarioSet (strategy, prefix, errors) {
573
+ if (!Array.isArray(strategy.scenarios)) return
574
+
575
+ const seen = new Set()
576
+ for (const scenario of strategy.scenarios.slice(0, GENERATED_SCENARIO_IDS.size)) {
577
+ if (typeof scenario?.id === 'string') seen.add(scenario.id)
578
+ }
579
+
580
+ for (const scenarioId of GENERATED_SCENARIO_IDS) {
581
+ if (!seen.has(scenarioId)) {
582
+ errors.push(`${prefix}.scenarios must include generated scenario "${scenarioId}" when status is planned or ` +
583
+ 'verified.')
584
+ }
585
+ }
586
+ }
587
+
588
+ function validateUniqueFrameworkIds (frameworks, errors) {
589
+ const seen = new Set()
590
+ for (const [index, framework] of frameworks.entries()) {
591
+ if (typeof framework?.id !== 'string') continue
592
+ if (seen.has(framework.id)) {
593
+ errors.push(`frameworks[${index}].id must be unique; duplicate "${framework.id}".`)
594
+ }
595
+ seen.add(framework.id)
596
+ }
597
+ }
598
+
599
+ function requireNonEmptyNotes (framework, errors, prefix) {
600
+ if (!Array.isArray(framework.notes) || framework.notes.length === 0) {
601
+ errors.push(`${prefix}.notes must include a reason when status is ${framework.status}.`)
602
+ return
603
+ }
604
+ validateStringArray(framework, 'notes', errors, prefix)
605
+ }
606
+
607
+ function validateScenarioIdentities (scenario, prefix, errors, required = false) {
608
+ if (!Array.isArray(scenario.testIdentities)) {
609
+ if (required) {
610
+ errors.push(`${prefix}.testIdentities must be a non-empty array when generatedTestStrategy is planned or ` +
611
+ 'verified.')
612
+ }
613
+ return
614
+ }
615
+
616
+ if (required && scenario.testIdentities.length === 0) {
617
+ errors.push(`${prefix}.testIdentities must be a non-empty array when generatedTestStrategy is planned or verified.`)
618
+ }
619
+ validateArrayLimit(
620
+ { testIdentities: scenario.testIdentities },
621
+ 'testIdentities',
622
+ MAX_MANIFEST_ARRAY_ENTRIES,
623
+ errors,
624
+ prefix
625
+ )
626
+
627
+ for (const [index, identity] of scenario.testIdentities.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
628
+ const identityPrefix = `${prefix}.testIdentities[${index}]`
629
+ if (!identity || typeof identity !== 'object' || Array.isArray(identity)) {
630
+ errors.push(`${identityPrefix} must be an object.`)
631
+ continue
632
+ }
633
+ requiredString(identity, 'name', errors, identityPrefix)
634
+ if (identity.suite !== undefined && identity.suite !== null && typeof identity.suite !== 'string') {
635
+ errors.push(`${identityPrefix}.suite must be a string or null when present.`)
636
+ }
637
+ optionalAbsolutePath(identity, 'file', errors, identityPrefix)
638
+ }
639
+ }
640
+
641
+ function requiredCommand (target, field, errors, prefix = '', options = {}) {
642
+ const value = target && target[field]
643
+ const key = join(prefix, field)
644
+ if (!value || typeof value !== 'object') {
645
+ errors.push(`${key} must be an object.`)
646
+ return
647
+ }
648
+ for (const name of Object.keys(value)) {
649
+ if (!COMMAND_FIELDS.has(name)) errors.push(`${key}.${name} is not an allowed command field.`)
650
+ }
651
+ if (value.usesShell !== undefined && typeof value.usesShell !== 'boolean') {
652
+ errors.push(`${key}.usesShell must be a boolean when present.`)
653
+ }
654
+ if (value.required !== undefined && typeof value.required !== 'boolean') {
655
+ errors.push(`${key}.required must be a boolean when present.`)
656
+ }
657
+ requiredAbsolutePath(value, 'cwd', errors, key)
658
+ rejectUnresolvedPlaceholder(value.cwd, `${key}.cwd`, errors)
659
+ if (value.shell !== undefined) {
660
+ requiredString(value, 'shell', errors, key)
661
+ rejectUnresolvedPlaceholder(value.shell, `${key}.shell`, errors)
662
+ if (!value.usesShell) errors.push(`${key}.shell requires usesShell to be true.`)
663
+ if (typeof value.shell === 'string' && hasUnsafeExecutionCharacter(value.shell)) {
664
+ errors.push(`${key}.shell must not contain invisible or control characters.`)
665
+ }
666
+ }
667
+ if (value.usesShell) {
668
+ requiredString(value, 'shellCommand', errors, key)
669
+ rejectUnresolvedPlaceholder(value.shellCommand, `${key}.shellCommand`, errors)
670
+ if (typeof value.shellCommand === 'string' && hasUnsafeInvisibleCharacter(value.shellCommand)) {
671
+ errors.push(`${key}.shellCommand must not contain invisible or control characters.`)
672
+ } else if (typeof value.shellCommand === 'string' && sanitizeString(value.shellCommand) !== value.shellCommand) {
673
+ errors.push(`${key}.shellCommand must not contain inline secret-like values. Put safe placeholders in env.`)
674
+ }
675
+ } else if (!Array.isArray(value.argv) || value.argv.length === 0) {
676
+ errors.push(`${key}.argv must be a non-empty array unless usesShell is true.`)
677
+ } else {
678
+ validateStringArray(value, 'argv', errors, key)
679
+ for (const [index, arg] of value.argv.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
680
+ rejectUnresolvedPlaceholder(arg, `${key}.argv[${index}]`, errors)
681
+ }
682
+ if (value.argv.some(hasUnsafeExecutionCharacter)) {
683
+ errors.push(`${key}.argv must not contain invisible or control characters.`)
684
+ } else if (JSON.stringify(sanitizeForReport(value.argv)) !== JSON.stringify(value.argv)) {
685
+ errors.push(`${key}.argv must not contain inline secret-like values. Put safe placeholders in env.`)
686
+ }
687
+ }
688
+ if (value.env !== undefined) {
689
+ if (!value.env || typeof value.env !== 'object' || Array.isArray(value.env)) {
690
+ errors.push(`${key}.env must be an object when present.`)
691
+ } else {
692
+ const environmentEntries = Object.entries(value.env)
693
+ if (environmentEntries.length > MAX_MANIFEST_ARRAY_ENTRIES) {
694
+ errors.push(`${key}.env must contain at most ${MAX_MANIFEST_ARRAY_ENTRIES} entries.`)
695
+ }
696
+ for (const [name, envValue] of environmentEntries.slice(0, MAX_MANIFEST_ARRAY_ENTRIES)) {
697
+ if (!ENV_NAME_PATTERN.test(name)) {
698
+ errors.push(`${key}.env contains invalid variable name ${JSON.stringify(name)}.`)
699
+ }
700
+ if (typeof envValue !== 'string') errors.push(`${key}.env.${name} must be a string.`)
701
+ const validatePlaceholder = !(options.datadogClean && name.startsWith('DD_'))
702
+ if (typeof envValue === 'string' && hasUnsafeExecutionCharacter(envValue)) {
703
+ errors.push(`${key}.env.${name} must not contain invisible or control characters.`)
704
+ } else if (validatePlaceholder && typeof envValue === 'string' && containsSecretValue(name, envValue) &&
705
+ envValue !== SECRET_PLACEHOLDER) {
706
+ errors.push(`${key}.env.${name} must use the safe placeholder ${JSON.stringify(SECRET_PLACEHOLDER)}.`)
707
+ }
708
+ rejectUnresolvedPlaceholder(envValue, `${key}.env.${name}`, errors)
709
+ }
710
+ }
711
+ }
712
+ if (value.requiredEnvVars !== undefined) {
713
+ if (Array.isArray(value.requiredEnvVars)) {
714
+ validateArrayLimit(value, 'requiredEnvVars', MAX_MANIFEST_ARRAY_ENTRIES, errors, key)
715
+ for (const [index] of value.requiredEnvVars.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
716
+ requiredString(value.requiredEnvVars, index, errors, `${key}.requiredEnvVars`)
717
+ }
718
+ } else {
719
+ errors.push(`${key}.requiredEnvVars must be an array when present.`)
720
+ }
721
+ }
722
+ optionalAbsolutePathArray(value, 'outputPaths', errors, key)
723
+ if (value.timeoutMs !== undefined && (!Number.isFinite(value.timeoutMs) || value.timeoutMs <= 0)) {
724
+ errors.push(`${key}.timeoutMs must be a positive number when present.`)
725
+ } else if (value.timeoutMs > MAX_COMMAND_TIMEOUT_MS) {
726
+ errors.push(`${key}.timeoutMs must not exceed ${MAX_COMMAND_TIMEOUT_MS} ms.`)
727
+ }
728
+ }
729
+
730
+ function containsSecretValue (name, value) {
731
+ if (SAFE_SECRET_FIELD_VALUES.has(value.toLowerCase())) return false
732
+ return isSensitiveName(name) || sanitizeString(value) !== value
733
+ }
734
+
735
+ function rejectUnresolvedPlaceholder (value, key, errors) {
736
+ if (typeof value !== 'string' || !UNRESOLVED_PLACEHOLDER_PATTERN.test(value)) return
737
+ errors.push(`${key} contains an unresolved placeholder. Resolve it before live validation.`)
738
+ }
739
+
740
+ function requiredObject (target, field, errors, prefix = '') {
741
+ const value = target && target[field]
742
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
743
+ errors.push(`${join(prefix, field)} must be an object.`)
744
+ }
745
+ }
746
+
747
+ function requiredArray (target, field, errors, prefix = '') {
748
+ if (!Array.isArray(target && target[field])) {
749
+ errors.push(`${join(prefix, field)} must be an array.`)
750
+ }
751
+ }
752
+
753
+ function requiredString (target, field, errors, prefix = '') {
754
+ if (typeof (target && target[field]) !== 'string' || target[field].length === 0) {
755
+ errors.push(`${join(prefix, field)} must be a non-empty string.`)
756
+ }
757
+ }
758
+
759
+ function enumString (target, field, values, errors, prefix = '') {
760
+ if (!values.has(target && target[field])) {
761
+ errors.push(`${join(prefix, field)} must be one of: ${[...values].join(', ')}.`)
762
+ }
763
+ }
764
+
765
+ function requiredAbsolutePath (target, field, errors, prefix = '') {
766
+ const value = target && target[field]
767
+ if (typeof value !== 'string' || !path.isAbsolute(value)) {
768
+ errors.push(`${join(prefix, field)} must be an absolute path.`)
769
+ } else if (hasUnsafeExecutionCharacter(value)) {
770
+ errors.push(`${join(prefix, field)} must not contain invisible or control characters.`)
771
+ }
772
+ }
773
+
774
+ function optionalAbsolutePath (target, field, errors, prefix = '') {
775
+ const value = target && target[field]
776
+ if (value === undefined || value === null) return
777
+ if (typeof value !== 'string' || !path.isAbsolute(value)) {
778
+ errors.push(`${join(prefix, field)} must be an absolute path when present.`)
779
+ } else if (hasUnsafeExecutionCharacter(value)) {
780
+ errors.push(`${join(prefix, field)} must not contain invisible or control characters.`)
781
+ }
782
+ }
783
+
784
+ function optionalAbsolutePathArray (target, field, errors, prefix = '') {
785
+ const value = target && target[field]
786
+ if (value === undefined) return
787
+ if (!Array.isArray(value)) {
788
+ errors.push(`${join(prefix, field)} must be an array when present.`)
789
+ return
790
+ }
791
+
792
+ validateArrayLimit(target, field, MAX_MANIFEST_ARRAY_ENTRIES, errors, prefix)
793
+ for (const [index, item] of value.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
794
+ if (typeof item !== 'string' || !path.isAbsolute(item)) {
795
+ errors.push(`${join(prefix, field)}[${index}] must be an absolute path.`)
796
+ } else if (hasUnsafeExecutionCharacter(item)) {
797
+ errors.push(`${join(prefix, field)}[${index}] must not contain invisible or control characters.`)
798
+ }
799
+ }
800
+ }
801
+
802
+ function validateStringArray (target, field, errors, prefix = '') {
803
+ const value = target && target[field]
804
+ if (!Array.isArray(value)) return
805
+
806
+ validateArrayLimit(target, field, MAX_MANIFEST_ARRAY_ENTRIES, errors, prefix)
807
+ for (const [index, item] of value.slice(0, MAX_MANIFEST_ARRAY_ENTRIES).entries()) {
808
+ if (typeof item !== 'string') {
809
+ errors.push(`${join(prefix, field)}[${index}] must be a string.`)
810
+ }
811
+ }
812
+ }
813
+
814
+ function validateArrayLimit (target, field, limit, errors, prefix = '') {
815
+ const value = target && target[field]
816
+ if (Array.isArray(value) && value.length > limit) {
817
+ errors.push(`${join(prefix, field)} must contain at most ${limit} entries.`)
818
+ }
819
+ }
820
+
821
+ function limitedArray (value, limit) {
822
+ return Array.isArray(value) ? value.slice(0, limit) : []
823
+ }
824
+
825
+ function createErrorCollector () {
826
+ const errors = []
827
+ let omitted = 0
828
+ Object.defineProperties(errors, {
829
+ push: {
830
+ value (...messages) {
831
+ for (const message of messages) {
832
+ if (this.length < MAX_VALIDATION_ERRORS - 1) {
833
+ Array.prototype.push.call(this, message)
834
+ } else {
835
+ omitted++
836
+ }
837
+ }
838
+ return this.length
839
+ },
840
+ },
841
+ finalize: {
842
+ value () {
843
+ if (omitted > 0) {
844
+ Array.prototype.push.call(this, `${omitted} additional validation error(s) omitted.`)
845
+ }
846
+ return this
847
+ },
848
+ },
849
+ })
850
+ return errors
851
+ }
852
+
853
+ function join (prefix, field) {
854
+ return prefix ? `${prefix}.${field}` : field
855
+ }
856
+
857
+ module.exports = {
858
+ MAX_FRAMEWORKS,
859
+ MAX_REPRESENTATIVE_TESTS,
860
+ MAX_VALIDATION_ERRORS,
861
+ validateManifest,
862
+ }