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,351 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ const {
7
+ MAX_GENERATED_FILES,
8
+ getGeneratedFileContentError,
9
+ } = require('./generated-file-policy')
10
+ const { createFileSafely, ensureSafeDirectory } = require('./safe-files')
11
+
12
+ const RUNTIME_FILE_NAMESPACE = 'dd-test-optimization-validation'
13
+ const createdGeneratedDirectories = new Map()
14
+ const initializedCleanupStrategies = new WeakSet()
15
+ const authorizedRuntimeCleanupFiles = new Map()
16
+ const writtenGeneratedFiles = new Map()
17
+
18
+ function writeGeneratedFiles (framework) {
19
+ const strategy = framework.generatedTestStrategy
20
+ if (!strategy || !['planned', 'verified'].includes(strategy.status)) {
21
+ return []
22
+ }
23
+ if ((strategy.files || []).length > MAX_GENERATED_FILES) {
24
+ throw new Error(`Generated validation strategy must contain at most ${MAX_GENERATED_FILES} files.`)
25
+ }
26
+ initializeRuntimeCleanupFiles(framework, strategy)
27
+
28
+ const written = []
29
+ try {
30
+ for (const file of strategy.files || []) {
31
+ const filename = validateGeneratedFilePath(framework, file.path)
32
+ validateContentLines(file.contentLines, filename)
33
+ const content = `${file.contentLines.join('\n')}\n`
34
+ if (fs.existsSync(filename)) {
35
+ if (fs.readFileSync(filename, 'utf8') === content) continue
36
+ throw new Error(`Refusing to overwrite existing generated validation file with different content: ${filename}`)
37
+ }
38
+
39
+ const directory = path.dirname(filename)
40
+ const missingDirectories = getMissingDirectories(framework.project.root, directory)
41
+ ensureSafeDirectory(framework.project.root, directory, 'generated validation file directory', {
42
+ allowRootSymlink: true,
43
+ })
44
+ for (const createdDirectory of missingDirectories) {
45
+ createdGeneratedDirectories.set(
46
+ createdDirectory,
47
+ authorizePathForCleanup(framework.project.root, createdDirectory)
48
+ )
49
+ }
50
+ validateGeneratedFilePath(framework, filename)
51
+ createFileSafely(framework.project.root, filename, content, 'generated validation file')
52
+ writtenGeneratedFiles.set(filename, authorizePathForCleanup(framework.project.root, filename))
53
+ written.push(filename)
54
+ }
55
+ pinRuntimeCleanupParents(strategy)
56
+ } catch (err) {
57
+ cleanupPaths(written)
58
+ cleanupCreatedDirectories(framework.project.root)
59
+ forgetWrittenGeneratedFiles(written)
60
+ throw err
61
+ }
62
+ return written
63
+ }
64
+
65
+ function cleanupGeneratedFiles (manifest, { keep = false } = {}) {
66
+ if (keep) return
67
+
68
+ for (const framework of manifest.frameworks || []) {
69
+ const strategy = framework.generatedTestStrategy
70
+ cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true }))
71
+ cleanupCreatedDirectories(framework.project.root)
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Finds missing directories that the validator will create for one generated file.
77
+ *
78
+ * @param {string} root project root
79
+ * @param {string} directory generated file directory
80
+ * @returns {string[]} missing directories, from deepest to shallowest
81
+ */
82
+ function getMissingDirectories (root, directory) {
83
+ const missing = []
84
+ let current = path.resolve(directory)
85
+ const resolvedRoot = path.resolve(root)
86
+ while (current !== resolvedRoot && isPathInside(resolvedRoot, current) && !fs.existsSync(current)) {
87
+ missing.push(current)
88
+ current = path.dirname(current)
89
+ }
90
+ return missing
91
+ }
92
+
93
+ /**
94
+ * Removes empty generated directories created by this validator process.
95
+ *
96
+ * @param {string} root project root
97
+ */
98
+ function cleanupCreatedDirectories (root) {
99
+ const resolvedRoot = path.resolve(root)
100
+ const directories = [...createdGeneratedDirectories.entries()]
101
+ .filter(([directory, authorization]) => {
102
+ return isPathInside(resolvedRoot, directory) && isCleanupAuthorizationValid(directory, authorization)
103
+ })
104
+ .sort(([left], [right]) => right.length - left.length)
105
+
106
+ for (const [directory] of directories) {
107
+ try {
108
+ fs.rmdirSync(directory)
109
+ createdGeneratedDirectories.delete(directory)
110
+ } catch (error) {
111
+ if (error.code === 'ENOENT') createdGeneratedDirectories.delete(directory)
112
+ }
113
+ }
114
+ }
115
+
116
+ function cleanupGeneratedRuntimeFiles (framework) {
117
+ const strategy = framework.generatedTestStrategy
118
+ if (!strategy) return
119
+
120
+ initializeRuntimeCleanupFiles(framework, strategy)
121
+ cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: false }))
122
+ }
123
+
124
+ function initializeRuntimeCleanupFiles (framework, strategy) {
125
+ if (initializedCleanupStrategies.has(strategy)) return
126
+
127
+ const generatedFiles = new Set((strategy.files || []).map(file => validateGeneratedFilePath(framework, file.path)))
128
+ for (const cleanupPath of strategy.cleanupPaths || []) {
129
+ const filename = validateCleanupPath(framework, cleanupPath)
130
+ if (generatedFiles.has(filename) || isDirectory(filename) || !isNamespacedRuntimeFile(filename)) continue
131
+ if (fs.existsSync(filename)) {
132
+ throw new Error(`Refusing to delete pre-existing generated validation runtime file: ${filename}`)
133
+ }
134
+ authorizedRuntimeCleanupFiles.set(filename, authorizePathForCleanup(framework.project.root, filename))
135
+ }
136
+ initializedCleanupStrategies.add(strategy)
137
+ }
138
+
139
+ function getSafeCleanupPaths (framework, strategy, { includeGeneratedFiles }) {
140
+ if (!strategy) return []
141
+
142
+ const generatedFiles = new Set()
143
+ for (const file of strategy.files || []) {
144
+ generatedFiles.add(validateGeneratedFilePath(framework, file.path))
145
+ }
146
+
147
+ const cleanupPaths = []
148
+ for (const cleanupPath of strategy.cleanupPaths || []) {
149
+ const filename = validateCleanupPath(framework, cleanupPath)
150
+ if (generatedFiles.has(filename)) {
151
+ if (includeGeneratedFiles && writtenGeneratedFiles.has(filename)) cleanupPaths.push(filename)
152
+ continue
153
+ }
154
+
155
+ if (authorizedRuntimeCleanupFiles.has(filename)) {
156
+ cleanupPaths.push(filename)
157
+ }
158
+ }
159
+
160
+ if (includeGeneratedFiles) {
161
+ for (const filename of generatedFiles) {
162
+ if (writtenGeneratedFiles.has(filename)) cleanupPaths.push(filename)
163
+ }
164
+ }
165
+ return [...new Set(cleanupPaths)]
166
+ }
167
+
168
+ function cleanupPaths (cleanupPaths) {
169
+ for (const cleanupPath of cleanupPaths) {
170
+ try {
171
+ const authorization = writtenGeneratedFiles.get(cleanupPath) ||
172
+ authorizedRuntimeCleanupFiles.get(cleanupPath)
173
+ if (!authorization || !isCleanupAuthorizationValid(cleanupPath, authorization)) continue
174
+ if (isDirectory(cleanupPath)) continue
175
+ fs.rmSync(cleanupPath, { force: true })
176
+ writtenGeneratedFiles.delete(cleanupPath)
177
+ } catch {
178
+ // Cleanup should be best-effort. The report will contain the command artifacts.
179
+ }
180
+ }
181
+ }
182
+
183
+ function forgetWrittenGeneratedFiles (filenames) {
184
+ for (const filename of filenames) {
185
+ writtenGeneratedFiles.delete(filename)
186
+ }
187
+ }
188
+
189
+ function authorizePathForCleanup (root, filename) {
190
+ const lexicalRoot = path.resolve(root)
191
+ const physicalRoot = fs.realpathSync(lexicalRoot)
192
+ const rootStat = fs.statSync(physicalRoot)
193
+ const authorization = {
194
+ lexicalRoot,
195
+ physicalRoot,
196
+ rootDevice: rootStat.dev,
197
+ rootInode: rootStat.ino,
198
+ }
199
+ pinCleanupParent(authorization, filename)
200
+ pinCleanupTarget(authorization, filename)
201
+ return authorization
202
+ }
203
+
204
+ function pinRuntimeCleanupParents (strategy) {
205
+ for (const cleanupPath of strategy.cleanupPaths || []) {
206
+ const authorization = authorizedRuntimeCleanupFiles.get(path.resolve(cleanupPath))
207
+ if (authorization && authorization.physicalParent === undefined) {
208
+ pinCleanupParent(authorization, cleanupPath)
209
+ }
210
+ }
211
+ }
212
+
213
+ function pinCleanupParent (authorization, filename) {
214
+ try {
215
+ const physicalParent = fs.realpathSync(path.dirname(filename))
216
+ if (!isPathInside(authorization.physicalRoot, physicalParent)) return
217
+ const parentStat = fs.statSync(physicalParent)
218
+ authorization.physicalParent = physicalParent
219
+ authorization.parentDevice = parentStat.dev
220
+ authorization.parentInode = parentStat.ino
221
+ } catch {}
222
+ }
223
+
224
+ function pinCleanupTarget (authorization, filename) {
225
+ try {
226
+ const targetStat = fs.lstatSync(filename)
227
+ authorization.targetDevice = targetStat.dev
228
+ authorization.targetInode = targetStat.ino
229
+ } catch {}
230
+ }
231
+
232
+ function isCleanupAuthorizationValid (filename, authorization) {
233
+ try {
234
+ const currentPhysicalRoot = fs.realpathSync(authorization.lexicalRoot)
235
+ const rootStat = fs.statSync(currentPhysicalRoot)
236
+ if (currentPhysicalRoot !== authorization.physicalRoot ||
237
+ rootStat.dev !== authorization.rootDevice || rootStat.ino !== authorization.rootInode) {
238
+ return false
239
+ }
240
+
241
+ if (authorization.physicalParent === undefined) return false
242
+ const physicalParent = fs.realpathSync(path.dirname(filename))
243
+ const parentStat = fs.statSync(physicalParent)
244
+ if (physicalParent !== authorization.physicalParent ||
245
+ parentStat.dev !== authorization.parentDevice || parentStat.ino !== authorization.parentInode) {
246
+ return false
247
+ }
248
+
249
+ if (authorization.targetDevice !== undefined) {
250
+ const targetStat = fs.lstatSync(filename)
251
+ if (targetStat.dev !== authorization.targetDevice || targetStat.ino !== authorization.targetInode) {
252
+ return false
253
+ }
254
+ }
255
+ return true
256
+ } catch {
257
+ return false
258
+ }
259
+ }
260
+
261
+ function validateGeneratedFilePath (framework, filename) {
262
+ return validatePathUnderProjectRoot(framework, filename, 'generated validation file')
263
+ }
264
+
265
+ function validateCleanupPath (framework, filename) {
266
+ return validatePathUnderProjectRoot(framework, filename, 'generated validation cleanup')
267
+ }
268
+
269
+ function validatePathUnderProjectRoot (framework, filename, label) {
270
+ const root = getProjectRoot(framework)
271
+ const resolved = path.resolve(filename || '')
272
+ if (!root || !isPathInside(root, resolved)) {
273
+ throw new Error(`Refusing ${label} path outside project root: ${filename}`)
274
+ }
275
+ validatePhysicalPath(root, resolved, label)
276
+ return resolved
277
+ }
278
+
279
+ /**
280
+ * Verifies that an existing parent resolves inside the physical project root.
281
+ *
282
+ * @param {string} root project root
283
+ * @param {string} filename candidate filename
284
+ * @param {string} label customer-facing path label
285
+ */
286
+ function validatePhysicalPath (root, filename, label) {
287
+ const parent = path.dirname(filename)
288
+ let physicalRoot
289
+ let physicalParent
290
+ try {
291
+ physicalRoot = fs.realpathSync(root)
292
+ physicalParent = fs.realpathSync(parent)
293
+ } catch (error) {
294
+ if (error.code === 'ENOENT') return
295
+ throw error
296
+ }
297
+
298
+ if (!isPathInside(physicalRoot, physicalParent)) {
299
+ throw new Error(`Refusing ${label} path outside physical project root: ${filename}`)
300
+ }
301
+ try {
302
+ if (fs.lstatSync(filename).isSymbolicLink()) {
303
+ throw new Error(`Refusing ${label} symbolic-link target: ${filename}`)
304
+ }
305
+ } catch (error) {
306
+ if (error.code !== 'ENOENT') throw error
307
+ }
308
+ }
309
+
310
+ function getProjectRoot (framework) {
311
+ const root = framework.project?.root
312
+ return typeof root === 'string' && path.isAbsolute(root) ? path.resolve(root) : null
313
+ }
314
+
315
+ function isPathInside (root, filename) {
316
+ const relative = path.relative(root, filename)
317
+ return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))
318
+ }
319
+
320
+ function validateContentLines (contentLines, filename) {
321
+ if (!Array.isArray(contentLines) || contentLines.some(line => typeof line !== 'string')) {
322
+ throw new Error(`Generated validation file contentLines must be an array of strings: ${filename}`)
323
+ }
324
+ const policyError = getGeneratedFileContentError(contentLines)
325
+ if (policyError) {
326
+ throw new Error(`Generated validation file ${policyError}: ${filename}`)
327
+ }
328
+ }
329
+
330
+ function isNamespacedRuntimeFile (filename) {
331
+ return path.basename(filename).includes(RUNTIME_FILE_NAMESPACE)
332
+ }
333
+
334
+ function isDirectory (filename) {
335
+ try {
336
+ return fs.statSync(filename).isDirectory()
337
+ } catch {
338
+ return false
339
+ }
340
+ }
341
+
342
+ function findGeneratedScenario (framework, scenarioId) {
343
+ return (framework.generatedTestStrategy?.scenarios || []).find(scenario => scenario.id === scenarioId)
344
+ }
345
+
346
+ module.exports = {
347
+ cleanupGeneratedFiles,
348
+ cleanupGeneratedRuntimeFiles,
349
+ findGeneratedScenario,
350
+ writeGeneratedFiles,
351
+ }
@@ -0,0 +1,196 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const { runCommand, serializeDisplayCommand } = require('./command-runner')
7
+ const {
8
+ cleanupGeneratedRuntimeFiles,
9
+ writeGeneratedFiles,
10
+ } = require('./generated-files')
11
+ const {
12
+ getDatadogCleanCommand,
13
+ getLocalValidationCommand,
14
+ } = require('./local-command')
15
+ const { frameworkOutDir } = require('./scenarios/helpers')
16
+ const { getObservedTestCount } = require('./test-output')
17
+
18
+ const GENERATED_SCENARIO_BY_FEATURE = {
19
+ efd: 'basic-pass',
20
+ atr: 'atr-fail-once',
21
+ 'test-management': 'test-management-target',
22
+ }
23
+
24
+ /**
25
+ * Verifies generated scenario commands without Datadog initialization.
26
+ *
27
+ * @param {object} input verification inputs
28
+ * @param {object} input.framework manifest framework entry
29
+ * @param {string} input.out validation output directory
30
+ * @param {object} input.options validator options
31
+ * @returns {Promise<{ok: boolean, failure?: object}>} verification outcome
32
+ */
33
+ async function verifyGeneratedTestStrategy ({ framework, out, options }) {
34
+ const strategy = framework.generatedTestStrategy
35
+ if (!strategy || !['planned', 'verified'].includes(strategy.status)) return { ok: true }
36
+
37
+ const evidence = {
38
+ scenarios: [],
39
+ }
40
+ const artifacts = []
41
+ const startedAt = Date.now()
42
+
43
+ try {
44
+ cleanupGeneratedRuntimeFiles(framework)
45
+ writeGeneratedFiles(framework)
46
+
47
+ for (const scenario of getScenariosToVerify(strategy.scenarios, options.scenarios)) {
48
+ cleanupGeneratedRuntimeFiles(framework)
49
+ const command = getDatadogCleanCommand(getLocalValidationCommand(framework, scenario.runCommand))
50
+ const outDir = frameworkOutDir(out, framework, `generated-verification-${scenario.id}`)
51
+ // Generated commands run serially because fail-once state and cleanup are scenario-local.
52
+ // eslint-disable-next-line no-await-in-loop
53
+ const result = await runCommand(command, {
54
+ artifactRoot: out,
55
+ envMode: 'clean',
56
+ label: `${framework.id}:generated-verification:${scenario.id}`,
57
+ outDir,
58
+ repositoryRoot: options.repositoryRoot,
59
+ requireExecutableApproval: options.requireExecutableApproval,
60
+ verbose: options.verbose,
61
+ })
62
+ const observedTestCount = getObservedTestCount(framework.framework, result.stdout, result.stderr)
63
+ const expected = scenario.expectedWithoutDatadog
64
+ const failOnceStateCreated = scenario.id === 'atr-fail-once'
65
+ ? hasGeneratedRuntimeFile(strategy)
66
+ : undefined
67
+ const scenarioEvidence = {
68
+ id: scenario.id,
69
+ command: serializeDisplayCommand(command),
70
+ exitCode: result.exitCode,
71
+ expectedExitCode: expected.exitCode,
72
+ observedTestCount,
73
+ expectedTestCount: expected.observedTestCount,
74
+ localAdjustments: command.localAdjustments || [],
75
+ }
76
+ if (failOnceStateCreated !== undefined) scenarioEvidence.failOnceStateCreated = failOnceStateCreated
77
+ evidence.scenarios.push(scenarioEvidence)
78
+ artifacts.push(...Object.values(result.artifacts))
79
+
80
+ if (result.timedOut || result.exitCode !== expected.exitCode ||
81
+ observedTestCount !== expected.observedTestCount || failOnceStateCreated === false) {
82
+ cleanupGeneratedRuntimeFiles(framework)
83
+ return getVerificationFailure(framework, evidence, artifacts, scenarioEvidence, result.timedOut)
84
+ }
85
+ }
86
+
87
+ cleanupGeneratedRuntimeFiles(framework)
88
+ strategy.status = 'verified'
89
+ strategy.verification = {
90
+ source: 'validator',
91
+ ran: true,
92
+ durationMs: Date.now() - startedAt,
93
+ observedScenarios: evidence.scenarios,
94
+ cleanupCompleted: true,
95
+ }
96
+ return { ok: true }
97
+ } catch (error) {
98
+ cleanupGeneratedRuntimeFiles(framework)
99
+ return {
100
+ ok: false,
101
+ failure: {
102
+ frameworkId: framework.id,
103
+ scenario: 'generated-test-verification',
104
+ status: 'error',
105
+ diagnosis: 'The validator could not run the temporary validation test as expected. No advanced-feature ' +
106
+ `conclusion was reached: ${error.message || error}`,
107
+ evidence,
108
+ artifacts,
109
+ },
110
+ }
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Selects only generated tests required by the requested advanced checks.
116
+ *
117
+ * @param {object[]} scenarios generated test scenarios
118
+ * @param {Set<string>} [selectedFeatures] validator scenario selection
119
+ * @returns {object[]} generated scenarios to verify
120
+ */
121
+ function getScenariosToVerify (scenarios, selectedFeatures) {
122
+ if (!(selectedFeatures instanceof Set)) return scenarios
123
+
124
+ const selectedScenarioIds = new Set()
125
+ for (const feature of selectedFeatures) {
126
+ const scenarioId = GENERATED_SCENARIO_BY_FEATURE[feature]
127
+ if (scenarioId) selectedScenarioIds.add(scenarioId)
128
+ }
129
+
130
+ if (selectedScenarioIds.size === 0) return scenarios
131
+ return scenarios.filter(scenario => selectedScenarioIds.has(scenario.id))
132
+ }
133
+
134
+ /**
135
+ * Builds a failure when a generated scenario does not behave as declared.
136
+ *
137
+ * @param {object} framework manifest framework entry
138
+ * @param {object} evidence collected verification evidence
139
+ * @param {string[]} artifacts command artifacts
140
+ * @param {object} scenario failed scenario evidence
141
+ * @param {boolean} timedOut whether the scenario timed out
142
+ * @returns {{ok: false, failure: object}} generated verification failure
143
+ */
144
+ function getVerificationFailure (framework, evidence, artifacts, scenario, timedOut) {
145
+ let reason
146
+ if (timedOut) {
147
+ reason = 'timed out'
148
+ } else if (scenario.id === 'atr-fail-once' && scenario.failOnceStateCreated === false) {
149
+ reason = 'failed without creating its declared fail-once state file, so it failed for an unrelated reason'
150
+ } else {
151
+ reason = `exited ${scenario.exitCode} with ${formatObservedCount(scenario.observedTestCount)}; expected exit ` +
152
+ `${scenario.expectedExitCode} and ${scenario.expectedTestCount} test`
153
+ }
154
+ return {
155
+ ok: false,
156
+ failure: {
157
+ frameworkId: framework.id,
158
+ scenario: 'generated-test-verification',
159
+ status: 'error',
160
+ diagnosis: `Temporary validation test "${scenario.id}" ${reason}. No advanced-feature conclusion was reached.`,
161
+ evidence,
162
+ artifacts,
163
+ },
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Checks whether the generated fail-once scenario created a declared runtime state file.
169
+ *
170
+ * @param {object} strategy generated test strategy
171
+ * @returns {boolean} whether a regular declared runtime file exists
172
+ */
173
+ function hasGeneratedRuntimeFile (strategy) {
174
+ const generatedFiles = new Set((strategy.files || []).map(file => path.resolve(file.path)))
175
+ for (const cleanupPath of strategy.cleanupPaths || []) {
176
+ const filename = path.resolve(cleanupPath)
177
+ if (generatedFiles.has(filename)) continue
178
+ try {
179
+ const stat = fs.lstatSync(filename)
180
+ if (!stat.isSymbolicLink() && stat.isFile()) return true
181
+ } catch {}
182
+ }
183
+ return false
184
+ }
185
+
186
+ /**
187
+ * Formats a parsed test count for a customer-facing diagnosis.
188
+ *
189
+ * @param {number|null} count observed test count
190
+ * @returns {string} formatted count
191
+ */
192
+ function formatObservedCount (count) {
193
+ return count === null ? 'an unknown test count' : `${count} observed tests`
194
+ }
195
+
196
+ module.exports = { verifyGeneratedTestStrategy }
@@ -0,0 +1,163 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable eslint-rules/eslint-process-env */
4
+
5
+ const fs = require('node:fs')
6
+ const Module = require('node:module')
7
+ const path = require('node:path')
8
+
9
+ const { sanitizeForReport } = require('./redaction')
10
+
11
+ const PROBE_FILE_ENV = 'DD_TEST_OPTIMIZATION_INIT_PROBE_FILE'
12
+ const probeFile = process.env[PROBE_FILE_ENV]
13
+ const seenModuleLoads = new Set()
14
+
15
+ if (probeFile) {
16
+ writeRecord('process-start', {
17
+ argv: process.argv,
18
+ cwd: process.cwd(),
19
+ detectedTools: detectTools(process.argv),
20
+ execArgv: process.execArgv,
21
+ nodeOptionsPresent: Boolean(process.env.NODE_OPTIONS),
22
+ pid: process.pid,
23
+ ppid: process.ppid,
24
+ })
25
+
26
+ const originalLoad = Module._load
27
+ Module._load = function loadWithProbe (request, parent, isMain) {
28
+ const loaded = originalLoad.apply(this, arguments)
29
+ const tool = detectTool(request) || detectTool(resolveFromParent(request, parent))
30
+
31
+ if (tool) {
32
+ const key = `${process.pid}:${request}:${tool.name}`
33
+ if (!seenModuleLoads.has(key)) {
34
+ seenModuleLoads.add(key)
35
+ writeRecord('module-load', {
36
+ argv: process.argv,
37
+ cwd: process.cwd(),
38
+ isMain: Boolean(isMain),
39
+ parentFilename: parent && parent.filename,
40
+ pid: process.pid,
41
+ ppid: process.ppid,
42
+ request,
43
+ tool,
44
+ })
45
+ }
46
+ }
47
+
48
+ return loaded
49
+ }
50
+ }
51
+
52
+ function resolveFromParent (request, parent) {
53
+ try {
54
+ return Module._resolveFilename(request, parent)
55
+ } catch {
56
+ return ''
57
+ }
58
+ }
59
+
60
+ function detectTools (values) {
61
+ const tools = []
62
+ const seen = new Set()
63
+
64
+ for (const value of values) {
65
+ const tool = detectTool(value)
66
+ if (!tool || seen.has(tool.name)) continue
67
+ seen.add(tool.name)
68
+ tools.push(tool)
69
+ }
70
+
71
+ return tools
72
+ }
73
+
74
+ function detectTool (value) {
75
+ if (typeof value !== 'string' || value.length === 0) return null
76
+
77
+ const normalized = value.split(path.sep).join('/').toLowerCase()
78
+
79
+ if (/(^|\/)(jest|jest\.js)$/.test(normalized) ||
80
+ /\/(?:jest|jest-cli|@jest\/core)\//.test(normalized) ||
81
+ normalized === 'jest' ||
82
+ normalized === 'jest-cli' ||
83
+ normalized === '@jest/core') {
84
+ return { name: 'jest', kind: 'test-runner' }
85
+ }
86
+
87
+ if (/(^|\/)(vitest|vitest\.mjs)$/.test(normalized) ||
88
+ /\/(?:vitest|@vitest\/runner)\//.test(normalized) ||
89
+ normalized === 'vitest' ||
90
+ normalized === '@vitest/runner') {
91
+ return { name: 'vitest', kind: 'test-runner' }
92
+ }
93
+
94
+ if (/(^|\/)(mocha|mocha\.js|_mocha)$/.test(normalized) ||
95
+ /\/mocha\//.test(normalized) ||
96
+ normalized === 'mocha') {
97
+ return { name: 'mocha', kind: 'test-runner' }
98
+ }
99
+
100
+ if (/\/@cucumber\/cucumber\//.test(normalized) ||
101
+ normalized === '@cucumber/cucumber' ||
102
+ normalized === 'cucumber' ||
103
+ normalized === 'cucumber-js') {
104
+ return { name: 'cucumber', kind: 'test-runner' }
105
+ }
106
+
107
+ if (/(^|\/)playwright(?:\.js)?$/.test(normalized) ||
108
+ /\/(?:@playwright\/test|playwright)\//.test(normalized) ||
109
+ normalized === '@playwright/test' ||
110
+ normalized === 'playwright' ||
111
+ normalized === 'playwright/test') {
112
+ return { name: 'playwright', kind: 'test-runner' }
113
+ }
114
+
115
+ if (/(^|\/)cypress(?:\.js)?$/.test(normalized) || /\/cypress\//.test(normalized) || normalized === 'cypress') {
116
+ return { name: 'cypress', kind: 'test-runner' }
117
+ }
118
+
119
+ if (/(^|\/)(nx|nx\.js)$/.test(normalized) || /\/nx\//.test(normalized) || normalized === 'nx') {
120
+ return { name: 'nx', kind: 'wrapper' }
121
+ }
122
+
123
+ if (/(^|\/)(turbo|turbo\.js)$/.test(normalized) || /\/turbo\//.test(normalized) || normalized === 'turbo') {
124
+ return { name: 'turbo', kind: 'wrapper' }
125
+ }
126
+
127
+ if (/(^|\/)(lage|lage\.js)$/.test(normalized) || /\/lage\//.test(normalized) || normalized === 'lage') {
128
+ return { name: 'lage', kind: 'wrapper' }
129
+ }
130
+
131
+ if (/(^|\/)(pnpm|pnpm\.cjs)$/.test(normalized) || /\/pnpm\//.test(normalized) || normalized === 'pnpm') {
132
+ return { name: 'pnpm', kind: 'package-manager' }
133
+ }
134
+
135
+ if (/(^|\/)(npm|npm-cli\.js)$/.test(normalized) || /\/npm\//.test(normalized) || normalized === 'npm') {
136
+ return { name: 'npm', kind: 'package-manager' }
137
+ }
138
+
139
+ if (/(^|\/)(yarn|yarn\.js)$/.test(normalized) || /\/yarn\//.test(normalized) || normalized === 'yarn') {
140
+ return { name: 'yarn', kind: 'package-manager' }
141
+ }
142
+
143
+ return null
144
+ }
145
+
146
+ function writeRecord (type, data) {
147
+ try {
148
+ const record = sanitizeForReport({
149
+ type,
150
+ time: new Date().toISOString(),
151
+ ...data,
152
+ })
153
+ const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | (fs.constants.O_NOFOLLOW || 0)
154
+ const file = fs.openSync(probeFile, flags)
155
+ try {
156
+ fs.writeFileSync(file, `${JSON.stringify(record)}\n`)
157
+ } finally {
158
+ fs.closeSync(file)
159
+ }
160
+ } catch {
161
+ // The probe must never change test behavior.
162
+ }
163
+ }