dd-trace 6.8.0 → 6.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/ci/diagnose.js +27 -25
  2. package/ci/init.js +6 -6
  3. package/ci/runbook.md +114 -20
  4. package/ci/test-optimization-validation/approval-artifacts.js +36 -3
  5. package/ci/test-optimization-validation/approval.js +78 -22
  6. package/ci/test-optimization-validation/blocker-category.js +24 -0
  7. package/ci/test-optimization-validation/ci-discovery.js +23 -17
  8. package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
  9. package/ci/test-optimization-validation/ci-remediation.js +1 -1
  10. package/ci/test-optimization-validation/cli.js +280 -52
  11. package/ci/test-optimization-validation/command-blocker.js +159 -20
  12. package/ci/test-optimization-validation/command-output-policy.js +9 -3
  13. package/ci/test-optimization-validation/command-runner.js +9 -7
  14. package/ci/test-optimization-validation/environment.js +4 -2
  15. package/ci/test-optimization-validation/executable.js +15 -13
  16. package/ci/test-optimization-validation/execution-lock.js +70 -0
  17. package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
  18. package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
  19. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  20. package/ci/test-optimization-validation/generated-files.js +38 -28
  21. package/ci/test-optimization-validation/generated-test-contract.js +8 -4
  22. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  23. package/ci/test-optimization-validation/literal-glob.js +52 -0
  24. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  25. package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
  26. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  27. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  28. package/ci/test-optimization-validation/package-check.js +94 -0
  29. package/ci/test-optimization-validation/plan-writer.js +183 -10
  30. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  31. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  32. package/ci/test-optimization-validation/redaction.js +18 -27
  33. package/ci/test-optimization-validation/report-writer.js +182 -48
  34. package/ci/test-optimization-validation/result-semantics.js +20 -3
  35. package/ci/test-optimization-validation/runner-command.js +40 -20
  36. package/ci/test-optimization-validation/runner-contract.js +79 -43
  37. package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
  38. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  39. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  40. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  41. package/ci/test-optimization-validation/test-output.js +2 -1
  42. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  43. package/ci/vitest-no-worker-init-setup.mjs +30 -34
  44. package/index.d.ts +98 -1
  45. package/init.js +1 -17
  46. package/initialize.mjs +11 -0
  47. package/loader-hook.mjs +6 -4
  48. package/package.json +5 -3
  49. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  50. package/packages/datadog-esbuild/src/utils.js +16 -1
  51. package/packages/datadog-instrumentations/src/ai.js +61 -34
  52. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  53. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
  54. package/packages/datadog-instrumentations/src/cucumber.js +124 -47
  55. package/packages/datadog-instrumentations/src/dns.js +21 -2
  56. package/packages/datadog-instrumentations/src/electron.js +1 -0
  57. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  58. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  59. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  60. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  61. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +33 -9
  62. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +118 -0
  63. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  64. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  65. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  66. package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
  67. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  68. package/packages/datadog-instrumentations/src/jest.js +979 -233
  69. package/packages/datadog-instrumentations/src/mocha/main.js +107 -9
  70. package/packages/datadog-instrumentations/src/mocha/utils.js +64 -42
  71. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
  72. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
  73. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  74. package/packages/datadog-instrumentations/src/next.js +155 -23
  75. package/packages/datadog-instrumentations/src/openai.js +14 -0
  76. package/packages/datadog-instrumentations/src/pg.js +180 -6
  77. package/packages/datadog-instrumentations/src/playwright.js +173 -79
  78. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  79. package/packages/datadog-instrumentations/src/promise.js +3 -3
  80. package/packages/datadog-instrumentations/src/router.js +195 -19
  81. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -10
  82. package/packages/datadog-instrumentations/src/vitest-main.js +360 -56
  83. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  84. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  85. package/packages/datadog-instrumentations/src/webdriverio.js +194 -18
  86. package/packages/datadog-instrumentations/src/when.js +3 -3
  87. package/packages/datadog-instrumentations/src/ws.js +5 -1
  88. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
  90. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  92. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  93. package/packages/datadog-plugin-electron/src/net.js +10 -4
  94. package/packages/datadog-plugin-http/src/client.js +1 -1
  95. package/packages/datadog-plugin-http2/src/client.js +1 -1
  96. package/packages/datadog-plugin-jest/src/index.js +1 -18
  97. package/packages/datadog-plugin-jest/src/util.js +2 -0
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  99. package/packages/datadog-plugin-mocha/src/index.js +318 -1
  100. package/packages/datadog-plugin-next/src/index.js +57 -9
  101. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  102. package/packages/datadog-plugin-pg/src/index.js +65 -1
  103. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  104. package/packages/datadog-plugin-vitest/src/index.js +95 -7
  105. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  106. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  107. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  108. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  109. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  110. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  111. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  112. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  113. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  114. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  115. package/packages/dd-trace/src/appsec/index.js +48 -476
  116. package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
  117. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  118. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
  120. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
  122. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  123. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  124. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  125. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  127. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  128. package/packages/dd-trace/src/config/helper.js +2 -0
  129. package/packages/dd-trace/src/config/index.js +16 -5
  130. package/packages/dd-trace/src/config/remote_config.js +1 -1
  131. package/packages/dd-trace/src/config/supported-configurations.json +8 -0
  132. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  133. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  134. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  135. package/packages/dd-trace/src/guardrails/log.js +1 -10
  136. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  137. package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
  138. package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
  139. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  140. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  141. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  142. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  143. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  144. package/packages/dd-trace/src/llmobs/sdk.js +149 -68
  145. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  146. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  147. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  148. package/packages/dd-trace/src/log/channels.js +1 -9
  149. package/packages/dd-trace/src/log/levels.js +12 -0
  150. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  151. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  152. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  153. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  154. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  155. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  156. package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
  157. package/packages/dd-trace/src/plugins/index.js +1 -0
  158. package/packages/dd-trace/src/plugins/util/ci.js +6 -0
  159. package/packages/dd-trace/src/plugins/util/git.js +6 -4
  160. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
  161. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  162. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  163. package/packages/dd-trace/src/plugins/util/test.js +319 -75
  164. package/packages/dd-trace/src/plugins/util/url.js +1 -1
  165. package/packages/dd-trace/src/plugins/util/web.js +23 -1
  166. package/packages/dd-trace/src/profiler.js +1 -1
  167. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  168. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  169. package/packages/dd-trace/src/proxy.js +1 -0
  170. package/packages/dd-trace/src/ritm.js +5 -0
  171. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  172. package/packages/dd-trace/src/sampling_rule.js +4 -2
  173. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  174. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  175. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  176. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  177. package/vendor/dist/pprof-format/index.js +1 -1
  178. package/version.js +10 -8
  179. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -0,0 +1,517 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+
6
+ const { matchesLiteralGlob } = require('../literal-glob')
7
+ const { maskJavaScriptComments, maskJavaScriptNonCode } = require('../source-text')
8
+
9
+ const CONFIG_PATTERN = /^(?:vite\.config|vitest\.(?:config|workspace))\.[cm]?[jt]s$/
10
+ const WORKSPACE_CONFIG_PATTERN = /^vitest\.workspace\.[cm]?[jt]s$/
11
+ const DIRECT_EXPORT_PATTERN = /(?:export\s+default|module\s*\.\s*exports\s*=)\s*$/
12
+ const CONFIG_CALL_PATTERN =
13
+ /(?:export\s+default|module\s*\.\s*exports\s*=)\s*defineConfig\s*\(\s*$/
14
+ const WORKSPACE_ARRAY_CALL_PATTERN =
15
+ /(?:export\s+default|module\s*\.\s*exports\s*=)\s*defineWorkspace\s*\(\s*$/
16
+ const PROJECT_CALL_PATTERN =
17
+ /(?:export\s+default|module\s*\.\s*exports\s*=)\s*defineProject\s*\(\s*$/
18
+ const LITERAL_PROJECT_PATTERN = /^[A-Za-z0-9_.:@/-]+$/
19
+
20
+ /**
21
+ * Binds one literal `--project` selector to one statically named Vitest project.
22
+ *
23
+ * Customer configuration is read as text only. Dynamic names, roots, includes, and ambiguous matches fail closed.
24
+ *
25
+ * @param {object} input project inputs
26
+ * @param {string[]} input.configFiles approval-bound configuration files
27
+ * @param {string[]} input.projectFiles bounded project files
28
+ * @param {string} input.projectRoot detected project root
29
+ * @param {string[]} input.runnerArgs retained Vitest arguments
30
+ * @returns {{
31
+ * configFile?: string,
32
+ * error?: string,
33
+ * excludePatterns?: string[],
34
+ * files?: string[],
35
+ * includePatterns?: string[],
36
+ * root?: string
37
+ * }|undefined} project binding
38
+ */
39
+ function bindLiteralProject ({ configFiles, projectFiles, projectRoot, runnerArgs }) {
40
+ const projects = getOptionValues(runnerArgs, '--project')
41
+ if (projects.length === 0) return
42
+ if (projects.length !== 1 || !LITERAL_PROJECT_PATTERN.test(projects[0])) {
43
+ return { error: '--project must select exactly one literal project name' }
44
+ }
45
+
46
+ const name = projects[0]
47
+ const roots = getOptionValues(runnerArgs, '--root')
48
+ if (roots.length > 1) {
49
+ return { error: '--project cannot be bound with more than one Vitest root' }
50
+ }
51
+ const effectiveRoot = roots.length === 1
52
+ ? getPhysicalDirectory(path.resolve(projectRoot, roots[0]))
53
+ : getPhysicalDirectory(projectRoot)
54
+ if (!effectiveRoot || !isContainedDirectory(projectRoot, effectiveRoot)) {
55
+ return { error: 'the selected Vitest root is not a project-contained directory' }
56
+ }
57
+
58
+ const selectedConfigs = getSelectedConfigFiles({ configFiles, effectiveRoot, projectRoot, runnerArgs })
59
+ if (selectedConfigs.error) return selectedConfigs
60
+ const bindings = []
61
+ for (const configFile of selectedConfigs.files) {
62
+ const source = readText(configFile)
63
+ if (source === undefined) continue
64
+ for (const property of getLiteralStringProperties(source, 'name')) {
65
+ if (property.value !== name) continue
66
+ const project = getProjectObject(
67
+ source,
68
+ property.index,
69
+ WORKSPACE_CONFIG_PATTERN.test(path.basename(configFile))
70
+ )
71
+ if (!project) continue
72
+ const projectName = getLiteralProperty(project.source, 'name')
73
+ if (projectName.dynamic || projectName.value !== name) continue
74
+ const binding = getBindingFromObject({
75
+ bindingRoot: effectiveRoot,
76
+ configFile,
77
+ projectFiles,
78
+ projectEntry: project.entrySource,
79
+ projectObject: project.source,
80
+ projectRoot,
81
+ standaloneProject: project.standalone,
82
+ })
83
+ if (binding.error) return binding
84
+ bindings.push(binding)
85
+ }
86
+ }
87
+
88
+ if (bindings.length !== 1) {
89
+ return {
90
+ error: bindings.length === 0
91
+ ? `--project ${JSON.stringify(name)} does not map to one statically named Vitest project`
92
+ : `--project ${JSON.stringify(name)} maps to more than one Vitest project`,
93
+ }
94
+ }
95
+ return bindings[0]
96
+ }
97
+
98
+ function getSelectedConfigFiles ({ configFiles, effectiveRoot, projectRoot, runnerArgs }) {
99
+ const explicitConfigs = getOptionValues(runnerArgs, '--config')
100
+ if (explicitConfigs.length > 1) {
101
+ return { error: '--project cannot be bound with more than one explicit Vitest config' }
102
+ }
103
+ if (explicitConfigs.length === 1) {
104
+ const explicitConfig = getPhysicalPath(path.resolve(projectRoot, explicitConfigs[0]))
105
+ const selected = configFiles.find(filename => filename === explicitConfig)
106
+ return selected
107
+ ? { files: [selected] }
108
+ : { error: 'the explicit Vitest config is not an approval-bound regular file' }
109
+ }
110
+
111
+ return {
112
+ files: configFiles.filter(filename => {
113
+ return path.dirname(filename) === effectiveRoot && CONFIG_PATTERN.test(path.basename(filename))
114
+ }),
115
+ }
116
+ }
117
+
118
+ function getBindingFromObject ({
119
+ bindingRoot,
120
+ configFile,
121
+ projectFiles,
122
+ projectEntry,
123
+ projectObject,
124
+ projectRoot,
125
+ standaloneProject,
126
+ }) {
127
+ if (/\.\.\./.test(maskJavaScriptNonCode(projectEntry))) {
128
+ return { error: 'the selected Vitest project uses dynamic spread composition' }
129
+ }
130
+
131
+ const rootProperty = getLiteralProperty(projectObject, 'root')
132
+ if (rootProperty.dynamic) return { error: 'the selected Vitest project has a dynamic root' }
133
+
134
+ const rootBase = standaloneProject ? path.dirname(configFile) : bindingRoot
135
+ const root = rootProperty.value
136
+ ? path.resolve(rootBase, rootProperty.value)
137
+ : standaloneProject
138
+ ? path.dirname(configFile)
139
+ : bindingRoot
140
+ if (!isContainedDirectory(projectRoot, root)) {
141
+ return { error: 'the selected Vitest project root is not a repository-contained directory' }
142
+ }
143
+
144
+ const include = getLiteralStringArray(projectObject, 'include')
145
+ if (include.dynamic) return { error: 'the selected Vitest project has dynamic include patterns' }
146
+ const exclude = getLiteralStringArray(projectObject, 'exclude')
147
+ if (exclude.dynamic) return { error: 'the selected Vitest project has dynamic exclude patterns' }
148
+ const project = {
149
+ excludePatterns: exclude.values,
150
+ includePatterns: include.values,
151
+ root,
152
+ }
153
+ const files = projectFiles.filter(filename => matchesProjectFile(project, filename))
154
+ return { configFile, files, ...project }
155
+ }
156
+
157
+ function supportsGeneratedFiles (project, strategy) {
158
+ return strategy.scenarios.every(scenario => {
159
+ return matchesProjectFile(project, scenario.testIdentities[0].file)
160
+ })
161
+ }
162
+
163
+ function matchesProjectFile (project, filename) {
164
+ const relative = path.relative(project.root, filename)
165
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false
166
+ const normalized = relative.replaceAll('\\', '/')
167
+ const included = project.includePatterns.length === 0 ||
168
+ project.includePatterns.some(pattern => matchesLiteralGlob(normalized, pattern))
169
+ return included && !project.excludePatterns.some(pattern => matchesLiteralGlob(normalized, pattern))
170
+ }
171
+
172
+ /**
173
+ * Returns the literal project object that owns a matching name property.
174
+ *
175
+ * @param {string} source Vitest configuration source
176
+ * @param {number} nameIndex matching name property offset
177
+ * @param {boolean} workspaceConfig whether the source is a Vitest workspace config
178
+ * @returns {{entrySource: string, source: string, standalone: boolean}|undefined} bounded project object
179
+ */
180
+ function getProjectObject (source, nameIndex, workspaceConfig) {
181
+ const objectRanges = getObjectRanges(source)
182
+ const ranges = objectRanges
183
+ .filter(range => range.start < nameIndex && range.end > nameIndex)
184
+ .sort((left, right) => (left.end - left.start) - (right.end - right.start))
185
+ if (ranges.length === 0) return
186
+
187
+ // Vitest accepts both `{ name }` and `{ test: { name } }` project entries.
188
+ const inner = ranges[0]
189
+ const parent = ranges[1]
190
+ const parentPrefix = parent && maskJavaScriptComments(source.slice(parent.start + 1, inner.start))
191
+ const nestedTestObject = /(?:^|,)\s*(?:test|(["'])test\1)\s*:\s*$/.test(parentPrefix || '')
192
+ if (nestedTestObject &&
193
+ getDirectPropertyPositions(source.slice(parent.start + 1, parent.end), 'test').length !== 1) return
194
+ const selected = nestedTestObject ? parent : inner
195
+ const standalone = isStandaloneProjectObject(source, selected)
196
+ if (!standalone &&
197
+ !isTestProjectsEntry(source, selected, objectRanges) &&
198
+ !(workspaceConfig && isWorkspaceProjectEntry(source, selected))) return
199
+ return {
200
+ entrySource: source.slice(selected.start + 1, selected.end),
201
+ source: source.slice(inner.start + 1, inner.end),
202
+ standalone,
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Reports whether an object is the direct literal argument to defineProject.
208
+ *
209
+ * @param {string} source Vitest configuration source
210
+ * @param {{end: number, start: number}} range candidate object range
211
+ * @returns {boolean} whether the object is a standalone project
212
+ */
213
+ function isStandaloneProjectObject (source, range) {
214
+ const before = maskJavaScriptNonCode(source.slice(0, range.start))
215
+ const after = maskJavaScriptNonCode(source.slice(range.end + 1))
216
+ return PROJECT_CALL_PATTERN.test(before) && /^\s*,?\s*\)[\s;]*$/.test(after)
217
+ }
218
+
219
+ /**
220
+ * Reports whether an object is a direct entry in a literal test.projects array.
221
+ *
222
+ * @param {string} source Vitest configuration source
223
+ * @param {{end: number, start: number}} range candidate object range
224
+ * @param {{end: number, start: number}[]} objectRanges source object ranges
225
+ * @returns {boolean} whether the object is a bounded project entry
226
+ */
227
+ function isTestProjectsEntry (source, range, objectRanges) {
228
+ const projectsArray = getDirectContainingArray(source, range)
229
+ if (!projectsArray) return false
230
+
231
+ const containers = objectRanges
232
+ .filter(object => object.start < projectsArray.start && object.end > projectsArray.end)
233
+ .sort((left, right) => (left.end - left.start) - (right.end - right.start))
234
+ const testObject = containers[0]
235
+ const configObject = containers[1]
236
+ if (!testObject || !configObject) return false
237
+
238
+ const projectsPrefix = maskJavaScriptComments(source.slice(testObject.start + 1, projectsArray.start))
239
+ if (!/(?:^|,)\s*(?:projects|"projects"|'projects')\s*:\s*$/.test(projectsPrefix) ||
240
+ getDirectPropertyPositions(
241
+ source.slice(testObject.start + 1, testObject.end),
242
+ 'projects'
243
+ ).length !== 1) return false
244
+
245
+ const testPrefix = maskJavaScriptComments(source.slice(configObject.start + 1, testObject.start))
246
+ return /(?:^|,)\s*(?:test|"test"|'test')\s*:\s*$/.test(testPrefix) &&
247
+ getDirectPropertyPositions(source.slice(configObject.start + 1, configObject.end), 'test').length === 1 &&
248
+ isExportedConfigObject(source, configObject)
249
+ }
250
+
251
+ /**
252
+ * Reports whether an object is the direct exported config or defineConfig argument.
253
+ *
254
+ * @param {string} source Vitest configuration source
255
+ * @param {{end: number, start: number}} range candidate config object range
256
+ * @returns {boolean} whether the object belongs to the exported configuration
257
+ */
258
+ function isExportedConfigObject (source, range) {
259
+ const before = maskJavaScriptNonCode(source.slice(0, range.start))
260
+ const after = maskJavaScriptNonCode(source.slice(range.end + 1))
261
+ return (DIRECT_EXPORT_PATTERN.test(before) && /^[\s;]*$/.test(after)) ||
262
+ (CONFIG_CALL_PATTERN.test(before) && /^\s*,?\s*\)[\s;]*$/.test(after))
263
+ }
264
+
265
+ /**
266
+ * Reports whether an object is a direct entry in the exported array of a Vitest workspace config.
267
+ *
268
+ * @param {string} source Vitest workspace configuration source
269
+ * @param {{end: number, start: number}} range candidate object range
270
+ * @returns {boolean} whether the object is a bounded workspace project entry
271
+ */
272
+ function isWorkspaceProjectEntry (source, range) {
273
+ const workspaceArray = getDirectContainingArray(source, range)
274
+ if (!workspaceArray) return false
275
+
276
+ const before = maskJavaScriptNonCode(source.slice(0, workspaceArray.start))
277
+ const after = maskJavaScriptNonCode(source.slice(workspaceArray.end + 1))
278
+ return (DIRECT_EXPORT_PATTERN.test(before) && /^[\s;]*$/.test(after)) ||
279
+ (WORKSPACE_ARRAY_CALL_PATTERN.test(before) && /^\s*,?\s*\)[\s;]*$/.test(after))
280
+ }
281
+
282
+ /**
283
+ * Returns the smallest array containing an object as a direct entry.
284
+ *
285
+ * @param {string} source JavaScript-like source
286
+ * @param {{end: number, start: number}} range candidate object range
287
+ * @returns {{end: number, start: number}|undefined} containing direct array
288
+ */
289
+ function getDirectContainingArray (source, range) {
290
+ const arrays = getDelimitedRanges(source, '[', ']')
291
+ .filter(array => array.start < range.start && array.end > range.end)
292
+ .sort((left, right) => (left.end - left.start) - (right.end - right.start))
293
+ const directArray = arrays[0]
294
+ if (!directArray) return
295
+
296
+ const entryPrefix = maskJavaScriptComments(source.slice(directArray.start, range.start)).trimEnd()
297
+ const entrySuffix = maskJavaScriptComments(source.slice(range.end + 1, directArray.end + 1)).trimStart()
298
+ if (['[', ','].includes(entryPrefix.at(-1)) && [']', ','].includes(entrySuffix[0])) return directArray
299
+ if (/(?:^|\[|,)\s*defineProject\s*\(\s*$/.test(entryPrefix) && /^,?\s*\)\s*[\],]/.test(entrySuffix)) {
300
+ return directArray
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Finds balanced object ranges while ignoring comments and strings.
306
+ *
307
+ * @param {string} source JavaScript-like source
308
+ * @returns {{end: number, start: number}[]} object ranges
309
+ */
310
+ function getObjectRanges (source) {
311
+ return getDelimitedRanges(source, '{', '}')
312
+ }
313
+
314
+ /**
315
+ * Finds balanced delimiter ranges while ignoring comments and strings.
316
+ *
317
+ * @param {string} source JavaScript-like source
318
+ * @param {string} open opening delimiter
319
+ * @param {string} close closing delimiter
320
+ * @returns {{end: number, start: number}[]} delimiter ranges
321
+ */
322
+ function getDelimitedRanges (source, open, close) {
323
+ const ranges = []
324
+ const stack = []
325
+ let quote
326
+ let lineComment = false
327
+ let blockComment = false
328
+ for (let index = 0; index < source.length; index++) {
329
+ const character = source[index]
330
+ const next = source[index + 1]
331
+ if (lineComment) {
332
+ if (character === '\n') lineComment = false
333
+ continue
334
+ }
335
+ if (blockComment) {
336
+ if (character === '*' && next === '/') {
337
+ blockComment = false
338
+ index++
339
+ }
340
+ continue
341
+ }
342
+ if (quote) {
343
+ if (character === '\\') index++
344
+ else if (character === quote) quote = undefined
345
+ continue
346
+ }
347
+ if (character === '/' && next === '/') {
348
+ lineComment = true
349
+ index++
350
+ } else if (character === '/' && next === '*') {
351
+ blockComment = true
352
+ index++
353
+ } else if (character === '"' || character === "'" || character === '`') {
354
+ quote = character
355
+ } else if (character === open) {
356
+ stack.push(index)
357
+ } else if (character === close) {
358
+ const start = stack.pop()
359
+ if (start !== undefined) ranges.push({ end: index, start })
360
+ }
361
+ }
362
+ return ranges
363
+ }
364
+
365
+ function getLiteralStringProperties (source, property) {
366
+ return getPropertyPositions(source, property).flatMap(({ index, valueStart }) => {
367
+ const literal = /^(["'])([^"'\\]+)\1/.exec(source.slice(valueStart))
368
+ return literal ? [{ index, value: literal[2] }] : []
369
+ })
370
+ }
371
+
372
+ function getPropertyPositions (source, property) {
373
+ const properties = []
374
+ let quote
375
+ let lineComment = false
376
+ let blockComment = false
377
+ let previousCodeCharacter = '{'
378
+ for (let index = 0; index < source.length; index++) {
379
+ const character = source[index]
380
+ const next = source[index + 1]
381
+ if (lineComment) {
382
+ if (character === '\n') lineComment = false
383
+ continue
384
+ }
385
+ if (blockComment) {
386
+ if (character === '*' && next === '/') {
387
+ blockComment = false
388
+ index++
389
+ }
390
+ continue
391
+ }
392
+ if (quote) {
393
+ if (character === '\\') index++
394
+ else if (character === quote) quote = undefined
395
+ continue
396
+ }
397
+ if (character === '/' && next === '/') {
398
+ lineComment = true
399
+ index++
400
+ continue
401
+ }
402
+ if (character === '/' && next === '*') {
403
+ blockComment = true
404
+ index++
405
+ continue
406
+ }
407
+ if (character === '"' || character === "'" || character === '`') {
408
+ const match = new RegExp(String.raw`^(["'])${property}\1\s*:\s*`).exec(source.slice(index))
409
+ if (match && ['{', ','].includes(previousCodeCharacter)) {
410
+ properties.push({ index, valueStart: index + match[0].length })
411
+ previousCodeCharacter = ':'
412
+ index += match[0].length - 1
413
+ continue
414
+ }
415
+ quote = character
416
+ continue
417
+ }
418
+ if (!source.startsWith(property, index) ||
419
+ !['{', ','].includes(previousCodeCharacter) ||
420
+ /[A-Za-z0-9_$]/.test(source[index - 1] || '') ||
421
+ /[A-Za-z0-9_$]/.test(source[index + property.length] || '')) {
422
+ if (!/\s/.test(character)) previousCodeCharacter = character
423
+ continue
424
+ }
425
+
426
+ const match = new RegExp(String.raw`^${property}\s*:\s*`).exec(source.slice(index))
427
+ if (!match) {
428
+ previousCodeCharacter = character
429
+ continue
430
+ }
431
+ properties.push({ index, valueStart: index + match[0].length })
432
+ previousCodeCharacter = ':'
433
+ index += match[0].length - 1
434
+ }
435
+ return properties
436
+ }
437
+
438
+ function getLiteralProperty (source, property) {
439
+ const properties = getDirectPropertyPositions(source, property)
440
+ if (properties.length === 0) return {}
441
+ if (properties.length !== 1) return { dynamic: true }
442
+ const literal = /^(["'])([^"'\\]+)\1/.exec(source.slice(properties[0].valueStart))
443
+ if (literal) return { value: literal[2] }
444
+ return { dynamic: true }
445
+ }
446
+
447
+ function getLiteralStringArray (source, property) {
448
+ const properties = getDirectPropertyPositions(source, property)
449
+ if (properties.length === 0) return { values: [] }
450
+ if (properties.length !== 1) return { dynamic: true, values: [] }
451
+ const arrayMatch = /^\[([\s\S]{0,8192}?)\]/.exec(source.slice(properties[0].valueStart))
452
+ if (!arrayMatch) return { dynamic: true, values: [] }
453
+ const syntax = maskJavaScriptComments(arrayMatch[1])
454
+ const values = [...syntax.matchAll(/(["'])([^"'\\]+)\1/g)].map(match => match[2])
455
+ const residue = syntax.replaceAll(/(["'])([^"'\\]+)\1/g, '').replaceAll(',', '').trim()
456
+ return residue ? { dynamic: true, values: [] } : { values }
457
+ }
458
+
459
+ /**
460
+ * Returns matching properties that are not nested inside another object.
461
+ *
462
+ * @param {string} source JavaScript object contents
463
+ * @param {string} property property name
464
+ * @returns {{index: number, valueStart: number}[]} direct property positions
465
+ */
466
+ function getDirectPropertyPositions (source, property) {
467
+ const nestedObjects = getObjectRanges(source)
468
+ return getPropertyPositions(source, property).filter(position => {
469
+ return !nestedObjects.some(object => object.start < position.index && object.end > position.index)
470
+ })
471
+ }
472
+
473
+ function getOptionValues (args, expected) {
474
+ const values = []
475
+ for (let index = 0; index < args.length; index++) {
476
+ if (args[index].split('=', 1)[0] !== expected) continue
477
+ values.push(args[index].includes('=') ? args[index].slice(args[index].indexOf('=') + 1) : args[++index])
478
+ }
479
+ return values.filter(Boolean)
480
+ }
481
+
482
+ function isContainedDirectory (root, candidate) {
483
+ try {
484
+ const physicalRoot = fs.realpathSync(root)
485
+ const physicalCandidate = fs.realpathSync(candidate)
486
+ const relative = path.relative(physicalRoot, physicalCandidate)
487
+ return fs.statSync(physicalCandidate).isDirectory() &&
488
+ (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)))
489
+ } catch {
490
+ return false
491
+ }
492
+ }
493
+
494
+ function readText (filename) {
495
+ try {
496
+ const stat = fs.statSync(filename)
497
+ if (!stat.isFile() || stat.size > 512 * 1024) return
498
+ return fs.readFileSync(filename, 'utf8')
499
+ } catch {}
500
+ }
501
+
502
+ function getPhysicalPath (filename) {
503
+ try {
504
+ const stat = fs.lstatSync(filename)
505
+ if (!stat.isFile() || stat.isSymbolicLink()) return
506
+ return fs.realpathSync(filename)
507
+ } catch {}
508
+ }
509
+
510
+ function getPhysicalDirectory (directory) {
511
+ try {
512
+ const physical = fs.realpathSync(directory)
513
+ if (fs.statSync(physical).isDirectory()) return physical
514
+ } catch {}
515
+ }
516
+
517
+ module.exports = { bindLiteralProject, supportsGeneratedFiles }
@@ -91,14 +91,16 @@ function cleanupGeneratedFiles (manifest, { keep = false } = {}) {
91
91
  filesRemoved: 0,
92
92
  filesRetained: 0,
93
93
  }
94
- for (const framework of manifest.frameworks || []) {
95
- const strategy = framework.generatedTestStrategy
96
- addCleanupOutcome(
97
- outcome,
98
- cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true })),
99
- 'files'
100
- )
101
- addCleanupOutcome(outcome, cleanupCreatedDirectories(framework.project.root), 'directories')
94
+ if (manifest.frameworks) {
95
+ for (const framework of manifest.frameworks) {
96
+ const strategy = framework.generatedTestStrategy
97
+ addCleanupOutcome(
98
+ outcome,
99
+ cleanupPaths(getSafeCleanupPaths(framework, strategy, { includeGeneratedFiles: true })),
100
+ 'files'
101
+ )
102
+ addCleanupOutcome(outcome, cleanupCreatedDirectories(framework.project.root), 'directories')
103
+ }
102
104
  }
103
105
  outcome.status = outcome.filesRetained > 0 || outcome.directoriesRetained > 0
104
106
  ? 'incomplete'
@@ -168,13 +170,15 @@ function initializeRuntimeCleanupFiles (framework, strategy) {
168
170
  if (initializedCleanupStrategies.has(strategy)) return
169
171
 
170
172
  const generatedFiles = new Set((strategy.files || []).map(file => validateGeneratedFilePath(framework, file.path)))
171
- for (const cleanupPath of strategy.cleanupPaths || []) {
172
- const filename = validateCleanupPath(framework, cleanupPath)
173
- if (generatedFiles.has(filename) || isDirectory(filename) || !isNamespacedRuntimeFile(filename)) continue
174
- if (fs.existsSync(filename)) {
175
- throw new Error(`Refusing to delete pre-existing generated validation runtime file: ${filename}`)
173
+ if (strategy.cleanupPaths) {
174
+ for (const cleanupPath of strategy.cleanupPaths) {
175
+ const filename = validateCleanupPath(framework, cleanupPath)
176
+ if (generatedFiles.has(filename) || isDirectory(filename) || !isNamespacedRuntimeFile(filename)) continue
177
+ if (fs.existsSync(filename)) {
178
+ throw new Error(`Refusing to delete pre-existing generated validation runtime file: ${filename}`)
179
+ }
180
+ authorizedRuntimeCleanupFiles.set(filename, authorizePathForCleanup(framework.project.root, filename))
176
181
  }
177
- authorizedRuntimeCleanupFiles.set(filename, authorizePathForCleanup(framework.project.root, filename))
178
182
  }
179
183
  initializedCleanupStrategies.add(strategy)
180
184
  }
@@ -183,20 +187,24 @@ function getSafeCleanupPaths (framework, strategy, { includeGeneratedFiles }) {
183
187
  if (!strategy) return []
184
188
 
185
189
  const generatedFiles = new Set()
186
- for (const file of strategy.files || []) {
187
- generatedFiles.add(validateGeneratedFilePath(framework, file.path))
190
+ if (strategy.files) {
191
+ for (const file of strategy.files) {
192
+ generatedFiles.add(validateGeneratedFilePath(framework, file.path))
193
+ }
188
194
  }
189
195
 
190
196
  const cleanupPaths = []
191
- for (const cleanupPath of strategy.cleanupPaths || []) {
192
- const filename = validateCleanupPath(framework, cleanupPath)
193
- if (generatedFiles.has(filename)) {
194
- if (includeGeneratedFiles && writtenGeneratedFiles.has(filename)) cleanupPaths.push(filename)
195
- continue
196
- }
197
+ if (strategy.cleanupPaths) {
198
+ for (const cleanupPath of strategy.cleanupPaths) {
199
+ const filename = validateCleanupPath(framework, cleanupPath)
200
+ if (generatedFiles.has(filename)) {
201
+ if (includeGeneratedFiles && writtenGeneratedFiles.has(filename)) cleanupPaths.push(filename)
202
+ continue
203
+ }
197
204
 
198
- if (authorizedRuntimeCleanupFiles.has(filename)) {
199
- cleanupPaths.push(filename)
205
+ if (authorizedRuntimeCleanupFiles.has(filename)) {
206
+ cleanupPaths.push(filename)
207
+ }
200
208
  }
201
209
  }
202
210
 
@@ -264,10 +272,12 @@ function authorizePathForCleanup (root, filename) {
264
272
  }
265
273
 
266
274
  function pinRuntimeCleanupParents (strategy) {
267
- for (const cleanupPath of strategy.cleanupPaths || []) {
268
- const authorization = authorizedRuntimeCleanupFiles.get(path.resolve(cleanupPath))
269
- if (authorization && authorization.physicalParent === undefined) {
270
- pinCleanupParent(authorization, cleanupPath)
275
+ if (strategy.cleanupPaths) {
276
+ for (const cleanupPath of strategy.cleanupPaths) {
277
+ const authorization = authorizedRuntimeCleanupFiles.get(path.resolve(cleanupPath))
278
+ if (authorization && authorization.physicalParent === undefined) {
279
+ pinCleanupParent(authorization, cleanupPath)
280
+ }
271
281
  }
272
282
  }
273
283
  }
@@ -79,7 +79,7 @@ function getGeneratedTestContent ({ framework, moduleSystem, scenarioId, stateFi
79
79
  const requireCall = 'require'
80
80
  imports.push(moduleSystem === 'esm'
81
81
  ? "import { describe, expect, test } from '@jest/globals'"
82
- : `const { describe, expect, test } = ${requireCall}('@jest/globals')`)
82
+ : `const jestGlobals = ${requireCall}('@jest/globals')`)
83
83
  }
84
84
  if (framework === 'vitest' && moduleSystem === 'esm') {
85
85
  imports.push("import { describe, expect, it } from 'vitest'")
@@ -97,8 +97,12 @@ function getGeneratedTestContent ({ framework, moduleSystem, scenarioId, stateFi
97
97
  }
98
98
  }
99
99
 
100
- const assertion = framework === 'mocha' ? 'assert.equal(1, 1)' : 'expect(true).toBe(true)'
101
- const testFunction = framework === 'jest' ? 'test' : 'it'
100
+ const commonJsJest = framework === 'jest' && moduleSystem === 'commonjs'
101
+ const assertion = framework === 'mocha'
102
+ ? 'assert.equal(1, 1)'
103
+ : `${commonJsJest ? 'jestGlobals.' : ''}expect(true).toBe(true)`
104
+ const testFunction = framework === 'jest' ? `${commonJsJest ? 'jestGlobals.' : ''}test` : 'it'
105
+ const describeFunction = commonJsJest ? 'jestGlobals.describe' : 'describe'
102
106
  const body = scenarioId === 'atr-fail-once'
103
107
  ? getAtrBody({ moduleSystem, assertion, stateFile })
104
108
  : ` ${assertion}`
@@ -106,7 +110,7 @@ function getGeneratedTestContent ({ framework, moduleSystem, scenarioId, stateFi
106
110
  return [
107
111
  ...imports,
108
112
  imports.length > 0 ? '' : undefined,
109
- "describe('dd-test-optimization-validation', () => {",
113
+ `${describeFunction}('dd-test-optimization-validation', () => {`,
110
114
  ` ${testFunction}('${scenario.testName}', () => {`,
111
115
  body,
112
116
  ' })',