dd-trace 6.3.0 → 6.5.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 (207) 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/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,738 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable eslint-rules/eslint-process-env */
4
+
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
+
8
+ const { runDiagnosis } = require('../diagnose')
9
+ const { validateManifest } = require('./manifest-schema')
10
+
11
+ const SUPPORTED_SCAFFOLD_FRAMEWORKS = new Set(['jest', 'mocha', 'vitest'])
12
+ const CI_PATHS = [
13
+ '.github/workflows',
14
+ '.gitlab-ci.yml',
15
+ '.circleci/config.yml',
16
+ '.buildkite/pipeline.yml',
17
+ 'bitbucket-pipelines.yml',
18
+ 'azure-pipelines.yml',
19
+ 'Jenkinsfile',
20
+ ]
21
+
22
+ /**
23
+ * Creates a schema-valid starting manifest without executing project code.
24
+ *
25
+ * @param {object} input scaffold inputs
26
+ * @param {string} input.root repository root
27
+ * @param {Set<string>} [input.frameworks] selected framework ids or kinds
28
+ * @returns {object} validation manifest scaffold
29
+ */
30
+ function createManifestScaffold ({ root, frameworks = new Set() }) {
31
+ const repositoryRoot = path.resolve(root)
32
+ const diagnosis = runDiagnosis({ root: repositoryRoot, env: {} })
33
+ const selected = diagnosis.eligibleFrameworks.filter(framework => {
34
+ return frameworks.size === 0 || frameworks.has(framework.id) || frameworks.has(framework.id.split(':')[0])
35
+ })
36
+ const unsupported = diagnosis.unsupportedFrameworks.filter(framework => {
37
+ return frameworks.size === 0 || frameworks.has(framework.id) || frameworks.has(framework.id.split(':')[0])
38
+ })
39
+ if (selected.length === 0 && unsupported.length === 0) {
40
+ throw new Error('No test framework was detected for manifest scaffolding.')
41
+ }
42
+
43
+ const manifest = {
44
+ schemaVersion: '1.0',
45
+ generatedAt: new Date().toISOString(),
46
+ repository: {
47
+ root: repositoryRoot,
48
+ gitRemote: null,
49
+ gitSha: null,
50
+ packageManager: detectPackageManager(repositoryRoot),
51
+ workspaceManager: detectWorkspaceManager(repositoryRoot),
52
+ },
53
+ environment: {
54
+ os: getManifestOs(process.platform),
55
+ shell: process.env.SHELL || null,
56
+ nodeVersion: process.version,
57
+ requiredEnvVars: [],
58
+ safeEnv: {},
59
+ },
60
+ ciDiscovery: discoverCiFiles(repositoryRoot),
61
+ frameworks: [
62
+ ...selected.map(framework => buildFrameworkScaffold(repositoryRoot, framework)),
63
+ ...unsupported.map(framework => buildUnsupportedFrameworkScaffold(repositoryRoot, framework)),
64
+ ],
65
+ omitted: [],
66
+ }
67
+
68
+ const errors = validateManifest(manifest)
69
+ if (errors.length > 0) {
70
+ throw new Error(`Generated manifest scaffold is invalid:\n- ${errors.join('\n- ')}`)
71
+ }
72
+ return manifest
73
+ }
74
+
75
+ /**
76
+ * Builds a diagnostic-only entry for a detected runner the validator cannot execute.
77
+ *
78
+ * @param {string} repositoryRoot repository root
79
+ * @param {object} detection static framework detection
80
+ * @returns {object} non-runnable framework manifest entry
81
+ */
82
+ function buildUnsupportedFrameworkScaffold (repositoryRoot, detection) {
83
+ const packageJsonPath = getDetectionPackageJson(repositoryRoot, detection.locations)
84
+ const projectRoot = path.dirname(packageJsonPath)
85
+ const packageJson = readJson(packageJsonPath) || {}
86
+ const framework = detection.id === 'node-test' ? 'node:test' : detection.id
87
+
88
+ return {
89
+ id: `${detection.id}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
90
+ framework,
91
+ frameworkVersion: getInstalledFrameworkVersion(detection.id, projectRoot, packageJson),
92
+ language: 'unknown',
93
+ status: 'unsupported_by_validator',
94
+ supportLevel: 'detected_only',
95
+ project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
96
+ notes: [
97
+ `${detection.name} was detected at ${detection.locations.join(', ') || 'an unknown location'}, but is not ` +
98
+ 'supported by this Test Optimization validator.',
99
+ ],
100
+ }
101
+ }
102
+
103
+ function buildFrameworkScaffold (repositoryRoot, detection) {
104
+ const packageJsonPath = path.resolve(repositoryRoot, detection.commandLocation || 'package.json')
105
+ const projectRoot = path.dirname(packageJsonPath)
106
+ const packageJson = readJson(packageJsonPath) || {}
107
+ const framework = detection.id
108
+
109
+ if (!SUPPORTED_SCAFFOLD_FRAMEWORKS.has(framework)) {
110
+ return {
111
+ id: `${framework}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
112
+ framework,
113
+ frameworkVersion: detection.version,
114
+ status: 'detected_not_runnable',
115
+ project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
116
+ notes: [`${detection.name} was detected, but automatic generated-test scaffolding is not available.`],
117
+ }
118
+ }
119
+
120
+ const runner = tryResolveRunner(framework, projectRoot)
121
+ if (!runner) {
122
+ return {
123
+ id: `${framework}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
124
+ framework,
125
+ frameworkVersion: detection.version,
126
+ status: 'requires_manual_setup',
127
+ project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
128
+ notes: [
129
+ `${detection.name} was detected, but its executable package could not be resolved from ` +
130
+ `${path.relative(repositoryRoot, projectRoot) || 'the repository root'}.`,
131
+ ],
132
+ }
133
+ }
134
+ const scriptName = getPackageScriptName(packageJson, detection.command)
135
+ const preserveProjectWrapper = Boolean(scriptName) && !usesBareFrameworkRunner(detection.command, framework)
136
+ const baseCommand = buildExistingCommand({
137
+ framework,
138
+ projectRoot,
139
+ repositoryRoot,
140
+ runner,
141
+ scriptName,
142
+ preserveProjectWrapper,
143
+ })
144
+ const representative = findRepresentativeTestFile(projectRoot)
145
+ const command = representative
146
+ ? buildFocusedCommand(baseCommand, framework, representative, Boolean(scriptName), preserveProjectWrapper)
147
+ : baseCommand
148
+
149
+ const generatedTestStrategy = buildGeneratedTestStrategy({
150
+ baseCommand: preserveProjectWrapper ? baseCommand : undefined,
151
+ framework,
152
+ packageJson,
153
+ projectRoot,
154
+ representative,
155
+ runner,
156
+ })
157
+
158
+ return {
159
+ id: `${framework}:${getProjectIdentifier(packageJson, projectRoot, repositoryRoot)}`,
160
+ framework,
161
+ frameworkVersion: detection.version,
162
+ language: /\.tsx?$/.test(generatedTestStrategy.fileExtension) ? 'typescript' : 'javascript',
163
+ status: 'runnable',
164
+ supportLevel: 'validator_supported',
165
+ project: getProject({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }),
166
+ setup: { commands: [], services: [] },
167
+ existingTestCommand: command,
168
+ preflight: { status: 'pending', maxTestCount: 50 },
169
+ ciWiring: {
170
+ status: 'unknown',
171
+ replayability: 'not_replayable',
172
+ replayBlocker: 'CI command selection has not been completed. Inspect the discovered CI configuration and ' +
173
+ 'replace this with a concrete technical blocker only when the selected test command cannot be replayed.',
174
+ diagnosis: 'Select one replayable CI test step and record its exact command and environment before live CI ' +
175
+ 'wiring validation.',
176
+ initialization: {
177
+ status: 'unknown',
178
+ evidence: [],
179
+ },
180
+ },
181
+ generatedTestStrategy,
182
+ notes: [
183
+ representative
184
+ ? `Generated by --init-manifest using representative test ${path.relative(repositoryRoot, representative)}.`
185
+ : 'Generated by --init-manifest. Narrow existingTestCommand if the detected command runs a broad suite.',
186
+ preserveProjectWrapper
187
+ ? `Basic Reporting preserves package script ${scriptName} because it uses a custom test wrapper.`
188
+ : `Basic Reporting invokes the installed ${framework} runner directly; record the CI wrapper separately.`,
189
+ 'CI command selection still requires repository-specific evidence.',
190
+ ],
191
+ }
192
+ }
193
+
194
+ function getProject ({ packageJson, packageJsonPath, projectRoot, repositoryRoot, framework }) {
195
+ return {
196
+ name: packageJson.name || getProjectIdentifier(packageJson, projectRoot, repositoryRoot),
197
+ root: projectRoot,
198
+ packageJson: packageJsonPath,
199
+ configFiles: findConfigFiles(projectRoot, framework),
200
+ evidence: [`Detected ${framework} from ${path.relative(repositoryRoot, packageJsonPath) || 'package.json'}.`],
201
+ }
202
+ }
203
+
204
+ function buildExistingCommand ({
205
+ framework,
206
+ projectRoot,
207
+ repositoryRoot,
208
+ runner,
209
+ scriptName,
210
+ preserveProjectWrapper,
211
+ }) {
212
+ const packageManager = preserveProjectWrapper ? detectPackageManager(repositoryRoot) : undefined
213
+ const argv = preserveProjectWrapper
214
+ ? getPackageScriptArgv(packageManager, scriptName, repositoryRoot)
215
+ : getDirectRunnerArgv(framework, runner)
216
+ return {
217
+ description: preserveProjectWrapper
218
+ ? `Detected custom package script ${scriptName}`
219
+ : `Direct installed ${framework} runner for local capability validation`,
220
+ cwd: projectRoot,
221
+ argv,
222
+ env: {},
223
+ requiredEnvVars: [],
224
+ timeoutMs: 300_000,
225
+ usesShell: false,
226
+ }
227
+ }
228
+
229
+ function buildGeneratedTestStrategy ({ baseCommand, framework, packageJson, projectRoot, representative, runner }) {
230
+ const convention = getGeneratedTestConvention(representative, projectRoot)
231
+ const packageType = getNearestPackageType(convention.testDirectory, projectRoot, packageJson.type)
232
+ const moduleSystem = getGeneratedModuleSystem(framework, convention.fileExtension, packageType)
233
+ const definitions = getGeneratedDefinitions({ framework, convention, moduleSystem })
234
+
235
+ return {
236
+ status: 'planned',
237
+ reason: 'Standard isolated scenarios generated by the validator manifest scaffold.',
238
+ adapter: framework,
239
+ testDirectory: convention.testDirectory,
240
+ moduleSystem,
241
+ fileExtension: convention.fileExtension,
242
+ supportsFocusedSingleFileRun: true,
243
+ usesMultipleFiles: true,
244
+ files: definitions.map(definition => ({
245
+ path: definition.file,
246
+ role: 'test',
247
+ contentLines: definition.content.split('\n'),
248
+ })),
249
+ scenarios: definitions.map(definition => ({
250
+ id: definition.id,
251
+ purpose: definition.purpose,
252
+ runCommand: baseCommand
253
+ ? buildFocusedCommand(baseCommand, framework, definition.file, true, true, moduleSystem)
254
+ : buildGeneratedRunCommand(framework, projectRoot, definition.file, runner, moduleSystem),
255
+ expectedWithoutDatadog: {
256
+ exitCode: definition.id === 'atr-fail-once' ? 1 : 0,
257
+ observedTestCount: 1,
258
+ },
259
+ testIdentities: [{
260
+ suite: null,
261
+ name: definition.testName,
262
+ file: definition.file,
263
+ parameters: null,
264
+ }],
265
+ })),
266
+ cleanupPaths: [
267
+ ...definitions.map(definition => definition.file),
268
+ path.join(path.dirname(definitions.find(definition => definition.id === 'atr-fail-once').file),
269
+ '.dd-test-optimization-validation-atr-state'),
270
+ ],
271
+ }
272
+ }
273
+
274
+ function getGeneratedModuleSystem (framework, fileExtension, packageType) {
275
+ if (/\.(?:cjs|cts)$/.test(fileExtension)) return 'commonjs'
276
+ if (framework === 'vitest' || /\.(?:mjs|mts)$/.test(fileExtension)) return 'esm'
277
+ return packageType === 'module' ? 'esm' : 'commonjs'
278
+ }
279
+
280
+ /**
281
+ * Returns the package module type that applies to a generated test directory.
282
+ *
283
+ * @param {string} testDirectory generated test directory
284
+ * @param {string} projectRoot detected project root
285
+ * @param {string|undefined} fallbackType detected project package type
286
+ * @returns {string|undefined} nearest package module type
287
+ */
288
+ function getNearestPackageType (testDirectory, projectRoot, fallbackType) {
289
+ const root = path.resolve(projectRoot)
290
+ let directory = path.resolve(testDirectory)
291
+
292
+ while (directory === root || isPathInside(root, directory)) {
293
+ const packageJson = readJson(path.join(directory, 'package.json'))
294
+ if (typeof packageJson?.type === 'string') return packageJson.type
295
+ if (directory === root) break
296
+ directory = path.dirname(directory)
297
+ }
298
+
299
+ return fallbackType
300
+ }
301
+
302
+ function getGeneratedTestConvention (representative, projectRoot) {
303
+ if (!representative) {
304
+ return {
305
+ exactFilename: undefined,
306
+ fileExtension: '.test.js',
307
+ testDirectory: path.join(projectRoot, 'test'),
308
+ }
309
+ }
310
+
311
+ const basename = path.basename(representative)
312
+ if (/^test\.[cm]?[jt]s$/.test(basename)) {
313
+ const representativeDirectory = path.dirname(representative)
314
+ return {
315
+ exactFilename: basename,
316
+ fileExtension: path.extname(basename),
317
+ testDirectory: representativeDirectory === projectRoot
318
+ ? projectRoot
319
+ : path.dirname(representativeDirectory),
320
+ }
321
+ }
322
+
323
+ return {
324
+ exactFilename: undefined,
325
+ fileExtension: getTestExtension(representative),
326
+ testDirectory: path.dirname(representative),
327
+ }
328
+ }
329
+
330
+ function getGeneratedDefinitions ({ framework, convention, moduleSystem }) {
331
+ const stateFileExpression = moduleSystem === 'esm'
332
+ ? "join(dirname(fileURLToPath(import.meta.url)), '.dd-test-optimization-validation-atr-state')"
333
+ : "path.join(__dirname, '.dd-test-optimization-validation-atr-state')"
334
+ const definitions = [
335
+ { id: 'basic-pass', purpose: 'basic_reporting|efd_candidate', testName: 'basic-pass' },
336
+ { id: 'atr-fail-once', purpose: 'auto_test_retries_candidate', testName: 'atr-fail-once' },
337
+ { id: 'test-management-target', purpose: 'test_management_candidate', testName: 'test-management-target' },
338
+ ]
339
+
340
+ return definitions.map(definition => {
341
+ const prefix = `dd-test-optimization-validation-${definition.id}`
342
+ const filename = convention.exactFilename
343
+ ? path.join(prefix, convention.exactFilename)
344
+ : `${prefix}${convention.fileExtension}`
345
+ return {
346
+ ...definition,
347
+ file: path.join(convention.testDirectory, filename),
348
+ content: getGeneratedTestContent({ framework, definition, moduleSystem, stateFileExpression }),
349
+ }
350
+ })
351
+ }
352
+
353
+ function getGeneratedTestContent ({ framework, definition, moduleSystem, stateFileExpression }) {
354
+ const imports = []
355
+ if (framework === 'vitest' && moduleSystem === 'esm') {
356
+ imports.push("import { describe, expect, it } from 'vitest'")
357
+ }
358
+ if (framework === 'mocha') {
359
+ imports.push(moduleSystem === 'esm'
360
+ ? "import assert from 'node:assert/strict'"
361
+ : "const assert = require('node:assert/strict')")
362
+ }
363
+ if (definition.id === 'atr-fail-once') {
364
+ if (moduleSystem === 'esm') {
365
+ imports.push(
366
+ "import { existsSync, writeFileSync } from 'node:fs'",
367
+ "import { dirname, join } from 'node:path'",
368
+ "import { fileURLToPath } from 'node:url'"
369
+ )
370
+ } else {
371
+ imports.push("const fs = require('node:fs')", "const path = require('node:path')")
372
+ }
373
+ }
374
+ const assertion = framework === 'mocha' ? 'assert.equal(1, 1)' : 'expect(true).toBe(true)'
375
+ const testFunction = framework === 'jest' ? 'test' : 'it'
376
+ const body = definition.id === 'atr-fail-once'
377
+ ? getAtrBody({ moduleSystem, stateFileExpression, assertion })
378
+ : ` ${assertion}`
379
+
380
+ return [
381
+ ...imports,
382
+ imports.length > 0 ? '' : undefined,
383
+ "describe('dd-test-optimization-validation', () => {",
384
+ ` ${testFunction}('${definition.testName}', () => {`,
385
+ body,
386
+ ' })',
387
+ '})',
388
+ ].filter(line => line !== undefined).join('\n')
389
+ }
390
+
391
+ function getAtrBody ({ moduleSystem, stateFileExpression, assertion }) {
392
+ const exists = moduleSystem === 'esm' ? 'existsSync' : 'fs.existsSync'
393
+ const write = moduleSystem === 'esm' ? 'writeFileSync' : 'fs.writeFileSync'
394
+ return [
395
+ ` const stateFile = ${stateFileExpression}`,
396
+ ` if (!${exists}(stateFile)) {`,
397
+ ` ${write}(stateFile, 'failed-once')`,
398
+ " throw new Error('dd-test-optimization-validation atr first failure')",
399
+ ' }',
400
+ ` ${assertion}`,
401
+ ].join('\n')
402
+ }
403
+
404
+ function buildGeneratedRunCommand (framework, projectRoot, filename, runner, moduleSystem) {
405
+ const args = {
406
+ jest: ['--runTestsByPath', filename, '--runInBand', '--silent', '--no-watchman'],
407
+ mocha: ['--reporter', 'spec', filename],
408
+ vitest: ['run', filename, ...(moduleSystem === 'commonjs' ? ['--globals'] : [])],
409
+ }[framework]
410
+ return {
411
+ cwd: projectRoot,
412
+ argv: [process.execPath, runner, ...args],
413
+ env: {},
414
+ requiredEnvVars: [],
415
+ timeoutMs: 300_000,
416
+ usesShell: false,
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Adds a single test file selection to a detected project command.
422
+ *
423
+ * @param {object} baseCommand detected project command
424
+ * @param {string} framework detected test framework
425
+ * @param {string} filename selected test file
426
+ * @param {boolean} packageScript whether the command invokes a package script
427
+ * @param {boolean} preserveDefaultReporter whether a repository wrapper owns reporter selection
428
+ * @param {string} [moduleSystem] generated test module system
429
+ * @returns {object} focused project command
430
+ */
431
+ function buildFocusedCommand (
432
+ baseCommand,
433
+ framework,
434
+ filename,
435
+ packageScript,
436
+ preserveDefaultReporter,
437
+ moduleSystem
438
+ ) {
439
+ const argv = [...baseCommand.argv]
440
+ if (packageScript && path.basename(argv[0]).toLowerCase() === 'npm') argv.push('--')
441
+ argv.push(...getFocusedTestArgs(framework, filename, preserveDefaultReporter, moduleSystem))
442
+
443
+ return {
444
+ ...baseCommand,
445
+ description: `${baseCommand.description} targeting ${path.basename(filename)}`,
446
+ argv,
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Returns framework arguments that select exactly one test file.
452
+ *
453
+ * @param {string} framework detected test framework
454
+ * @param {string} filename selected test file
455
+ * @param {boolean} preserveDefaultReporter whether a repository wrapper owns reporter selection
456
+ * @param {string} [moduleSystem] generated test module system
457
+ * @returns {string[]} focused test arguments
458
+ */
459
+ function getFocusedTestArgs (framework, filename, preserveDefaultReporter, moduleSystem) {
460
+ if (framework === 'jest') {
461
+ return [
462
+ '--runTestsByPath',
463
+ filename,
464
+ '--runInBand',
465
+ ...(preserveDefaultReporter ? [] : ['--silent']),
466
+ '--no-watchman',
467
+ ]
468
+ }
469
+ return [filename, ...(framework === 'vitest' && moduleSystem === 'commonjs' ? ['--globals'] : [])]
470
+ }
471
+
472
+ /**
473
+ * Resolves an installed framework executable without making the whole scaffold fail when a nested package only
474
+ * declares the dependency.
475
+ *
476
+ * @param {string} framework detected framework
477
+ * @param {string} projectRoot detected project root
478
+ * @returns {string|undefined} resolved executable path
479
+ */
480
+ function tryResolveRunner (framework, projectRoot) {
481
+ try {
482
+ return resolveRunner(framework, projectRoot)
483
+ } catch {}
484
+ }
485
+
486
+ function resolveRunner (framework, projectRoot) {
487
+ const packageName = framework === 'jest' ? 'jest' : framework
488
+ const packageJsonPath = require.resolve(`${packageName}/package.json`, { paths: [projectRoot] })
489
+ const packageJson = readJson(packageJsonPath)
490
+ const bin = typeof packageJson.bin === 'string' ? packageJson.bin : packageJson.bin[packageName]
491
+ return path.resolve(path.dirname(packageJsonPath), bin)
492
+ }
493
+
494
+ function getPackageScriptArgv (packageManager, scriptName, repositoryRoot) {
495
+ if (packageManager === 'yarn') {
496
+ const release = findYarnRelease(repositoryRoot)
497
+ return release ? [process.execPath, release, 'run', scriptName] : ['yarn', 'run', scriptName]
498
+ }
499
+ return [packageManager, 'run', scriptName]
500
+ }
501
+
502
+ /**
503
+ * Finds the package script whose value produced the detected framework command.
504
+ *
505
+ * @param {object} packageJson project package metadata
506
+ * @param {string} command detected framework command
507
+ * @returns {string|undefined} package script name
508
+ */
509
+ function getPackageScriptName (packageJson, command) {
510
+ return Object.entries(packageJson.scripts || {}).find(([, value]) => value === command)?.[0]
511
+ }
512
+
513
+ /**
514
+ * Reports whether a package script invokes the framework binary without a repository wrapper.
515
+ *
516
+ * @param {string} command detected framework command
517
+ * @param {string} framework detected test framework
518
+ * @returns {boolean} whether the command starts with the framework binary
519
+ */
520
+ function usesBareFrameworkRunner (command, framework) {
521
+ const tokens = String(command || '').trim().split(/\s+/)
522
+ const executable = path.basename(tokens[0] || '').replace(/\.cmd$/i, '').toLowerCase()
523
+ if (executable !== framework) return false
524
+ return tokens.length === 1 || framework === 'vitest' && tokens.length === 2 && tokens[1] === 'run'
525
+ }
526
+
527
+ function getDirectRunnerArgv (framework, runner) {
528
+ return [process.execPath, runner, ...(framework === 'vitest' ? ['run'] : [])]
529
+ }
530
+
531
+ function findRepresentativeTestFile (root) {
532
+ const stack = [root]
533
+ const candidates = []
534
+ const packageRanks = new Map()
535
+ let visited = 0
536
+ while (stack.length > 0 && visited < 5000) {
537
+ const directory = stack.pop()
538
+ let entries
539
+ try {
540
+ entries = fs.readdirSync(directory, { withFileTypes: true })
541
+ .sort((left, right) => left.name.localeCompare(right.name))
542
+ } catch {
543
+ continue
544
+ }
545
+ for (const entry of entries) {
546
+ if (['.git', 'node_modules', 'coverage', 'dist', 'build'].includes(entry.name)) continue
547
+ const filename = path.join(directory, entry.name)
548
+ if (entry.isDirectory()) {
549
+ stack.push(filename)
550
+ continue
551
+ }
552
+ visited++
553
+ const inTestsDirectory = path.relative(root, directory).split(path.sep).includes('__tests__')
554
+ if (/^(?:test\.(?:[cm]?[jt]s|[jt]sx)|.+[.-](?:test|spec)\.(?:[cm]?[jt]s|[jt]sx))$/.test(entry.name) ||
555
+ (inTestsDirectory && /\.(?:[cm]?[jt]s|[jt]sx)$/.test(entry.name))) {
556
+ candidates.push(filename)
557
+ }
558
+ }
559
+ }
560
+
561
+ candidates.sort((left, right) => {
562
+ return getTestDirectoryRank(left, root) - getTestDirectoryRank(right, root) ||
563
+ getTestAreaRank(left, root) - getTestAreaRank(right, root) ||
564
+ getIndependentTestProjectRank(left, root, packageRanks) -
565
+ getIndependentTestProjectRank(right, root, packageRanks) ||
566
+ left.localeCompare(right)
567
+ })
568
+ return candidates[0]
569
+ }
570
+
571
+ /**
572
+ * Ranks established test directories ahead of source-adjacent test-looking files.
573
+ *
574
+ * @param {string} filename candidate test file
575
+ * @param {string} root detected project root
576
+ * @returns {number} directory preference rank
577
+ */
578
+ function getTestDirectoryRank (filename, root) {
579
+ const directories = path.relative(root, path.dirname(filename)).split(path.sep)
580
+ if (directories.includes('__tests__')) return 0
581
+ if (directories.some(directory => directory === 'test' || directory === 'tests')) return 1
582
+ return 2
583
+ }
584
+
585
+ /**
586
+ * Ranks conventional project areas ahead of auxiliary repository trees.
587
+ *
588
+ * @param {string} filename candidate test file
589
+ * @param {string} root detected project root
590
+ * @returns {number} project area preference rank
591
+ */
592
+ function getTestAreaRank (filename, root) {
593
+ const [topLevelDirectory] = path.relative(root, filename).split(path.sep)
594
+ return ['packages', 'src', 'test', 'tests'].includes(topLevelDirectory) ? 0 : 1
595
+ }
596
+
597
+ /**
598
+ * Ranks independently tested nested packages behind tests owned by the detected root command.
599
+ *
600
+ * @param {string} filename candidate test file
601
+ * @param {string} root detected project root
602
+ * @param {Map<string, number>} cache package-directory rank cache
603
+ * @returns {number} independent test project rank
604
+ */
605
+ function getIndependentTestProjectRank (filename, root, cache) {
606
+ let directory = path.dirname(filename)
607
+ while (directory !== root && directory.startsWith(`${root}${path.sep}`)) {
608
+ if (cache.has(directory)) return cache.get(directory)
609
+
610
+ const packageJson = readJson(path.join(directory, 'package.json'))
611
+ if (packageJson) {
612
+ const rank = typeof packageJson.scripts?.test === 'string' ? 1 : 0
613
+ cache.set(directory, rank)
614
+ return rank
615
+ }
616
+ directory = path.dirname(directory)
617
+ }
618
+ return 0
619
+ }
620
+
621
+ function getTestExtension (filename) {
622
+ const match = /((?:\.test|\.spec)\.(?:[cm]?[jt]s|[jt]sx))$/.exec(filename)
623
+ return match?.[1] || '.test.js'
624
+ }
625
+
626
+ function findConfigFiles (root, framework) {
627
+ const patterns = {
628
+ jest: /^jest\.config\./,
629
+ mocha: /^\.mocharc\./,
630
+ vitest: /^(?:vite|vitest)\.config\./,
631
+ }[framework]
632
+ if (!patterns) return []
633
+ try {
634
+ return fs.readdirSync(root).filter(filename => patterns.test(filename)).map(filename => path.join(root, filename))
635
+ } catch {
636
+ return []
637
+ }
638
+ }
639
+
640
+ /**
641
+ * Resolves the package.json associated with a framework detection.
642
+ *
643
+ * @param {string} repositoryRoot repository root
644
+ * @param {string[]} locations detected evidence paths
645
+ * @returns {string} absolute package.json path
646
+ */
647
+ function getDetectionPackageJson (repositoryRoot, locations = []) {
648
+ const location = locations.find(value => path.basename(value) === 'package.json')
649
+ return path.resolve(repositoryRoot, location || 'package.json')
650
+ }
651
+
652
+ /**
653
+ * Resolves a detected runner version without executing repository code.
654
+ *
655
+ * @param {string} framework detected framework package name
656
+ * @param {string} projectRoot detected project root
657
+ * @param {object} packageJson detected project package.json
658
+ * @returns {string|null} installed or declared framework version
659
+ */
660
+ function getInstalledFrameworkVersion (framework, projectRoot, packageJson) {
661
+ if (framework === 'node-test') return process.version
662
+ try {
663
+ const filename = require.resolve(`${framework}/package.json`, { paths: [projectRoot] })
664
+ return readJson(filename)?.version || null
665
+ } catch {}
666
+
667
+ for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
668
+ if (typeof packageJson[field]?.[framework] === 'string') return packageJson[field][framework]
669
+ }
670
+ return null
671
+ }
672
+
673
+ function discoverCiFiles (root) {
674
+ const found = []
675
+ for (const relativePath of CI_PATHS) {
676
+ const filename = path.join(root, relativePath)
677
+ if (!fs.existsSync(filename)) continue
678
+ const stat = fs.statSync(filename)
679
+ if (stat.isDirectory()) {
680
+ for (const entry of fs.readdirSync(filename).sort()) {
681
+ if (/\.ya?ml$/.test(entry)) found.push(path.posix.join(relativePath, entry))
682
+ }
683
+ } else {
684
+ found.push(relativePath)
685
+ }
686
+ }
687
+ return {
688
+ searched: [...CI_PATHS],
689
+ found,
690
+ method: 'deterministic-known-ci-paths',
691
+ warnings: [],
692
+ notes: ['Generated by --init-manifest; select and record one replayable CI test step before live validation.'],
693
+ }
694
+ }
695
+
696
+ function detectPackageManager (root) {
697
+ if (fs.existsSync(path.join(root, 'pnpm-lock.yaml'))) return 'pnpm'
698
+ if (fs.existsSync(path.join(root, 'yarn.lock'))) return 'yarn'
699
+ return 'npm'
700
+ }
701
+
702
+ function detectWorkspaceManager (root) {
703
+ if (fs.existsSync(path.join(root, 'pnpm-workspace.yaml'))) return 'pnpm'
704
+ const packageJson = readJson(path.join(root, 'package.json'))
705
+ return packageJson?.workspaces ? detectPackageManager(root) : 'none'
706
+ }
707
+
708
+ function getManifestOs (platform) {
709
+ if (platform === 'win32') return 'windows'
710
+ if (platform === 'darwin' || platform === 'linux') return platform
711
+ return 'unknown'
712
+ }
713
+
714
+ function findYarnRelease (root) {
715
+ const directory = path.join(root, '.yarn', 'releases')
716
+ try {
717
+ const release = fs.readdirSync(directory).find(filename => /^yarn-.+\.cjs$/.test(filename))
718
+ return release && path.join(directory, release)
719
+ } catch {}
720
+ }
721
+
722
+ function getProjectIdentifier (packageJson, projectRoot, repositoryRoot) {
723
+ if (packageJson.name) return packageJson.name.replaceAll(/[^A-Za-z0-9._-]+/g, '-')
724
+ return (path.relative(repositoryRoot, projectRoot) || 'root').replaceAll(path.sep, '-')
725
+ }
726
+
727
+ function isPathInside (root, filename) {
728
+ const relative = path.relative(root, filename)
729
+ return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
730
+ }
731
+
732
+ function readJson (filename) {
733
+ try {
734
+ return JSON.parse(fs.readFileSync(filename, 'utf8'))
735
+ } catch {}
736
+ }
737
+
738
+ module.exports = { createManifestScaffold }