dd-trace 6.7.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 (272) 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 +14 -8
  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/playwright.js +5 -5
  20. package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
  21. package/ci/test-optimization-validation/generated-files.js +47 -37
  22. package/ci/test-optimization-validation/generated-test-contract.js +9 -7
  23. package/ci/test-optimization-validation/generated-verifier.js +24 -17
  24. package/ci/test-optimization-validation/literal-glob.js +52 -0
  25. package/ci/test-optimization-validation/manifest-loader.js +3 -0
  26. package/ci/test-optimization-validation/manifest-scaffold.js +301 -52
  27. package/ci/test-optimization-validation/manifest-schema.js +80 -17
  28. package/ci/test-optimization-validation/offline-fixtures.js +13 -4
  29. package/ci/test-optimization-validation/offline-output.js +6 -6
  30. package/ci/test-optimization-validation/package-check.js +94 -0
  31. package/ci/test-optimization-validation/plan-writer.js +183 -10
  32. package/ci/test-optimization-validation/preflight-runner.js +110 -31
  33. package/ci/test-optimization-validation/project-build-artifact.js +31 -0
  34. package/ci/test-optimization-validation/redaction.js +18 -27
  35. package/ci/test-optimization-validation/report-writer.js +182 -48
  36. package/ci/test-optimization-validation/result-semantics.js +20 -3
  37. package/ci/test-optimization-validation/runner-command.js +41 -21
  38. package/ci/test-optimization-validation/runner-contract.js +79 -43
  39. package/ci/test-optimization-validation/safe-files.js +4 -4
  40. package/ci/test-optimization-validation/scenarios/basic-reporting.js +60 -6
  41. package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
  42. package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
  43. package/ci/test-optimization-validation/scenarios/test-management.js +2 -2
  44. package/ci/test-optimization-validation/static-diagnosis.js +10 -3
  45. package/ci/test-optimization-validation/test-output.js +2 -1
  46. package/ci/test-optimization-validation/validation-blocker.js +21 -0
  47. package/ci/vitest-no-worker-init-setup.mjs +377 -84
  48. package/index.d.ts +175 -14
  49. package/init.js +1 -17
  50. package/initialize.mjs +11 -0
  51. package/loader-hook.mjs +6 -4
  52. package/package.json +12 -4
  53. package/packages/datadog-core/src/utils/src/kebabcase.js +8 -2
  54. package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
  55. package/packages/datadog-esbuild/index.js +4 -2
  56. package/packages/datadog-esbuild/src/utils.js +17 -2
  57. package/packages/datadog-instrumentations/src/ai.js +61 -34
  58. package/packages/datadog-instrumentations/src/bluebird.js +6 -6
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +85 -9
  60. package/packages/datadog-instrumentations/src/cucumber.js +131 -51
  61. package/packages/datadog-instrumentations/src/dns.js +21 -2
  62. package/packages/datadog-instrumentations/src/electron.js +1 -0
  63. package/packages/datadog-instrumentations/src/fastify.js +3 -1
  64. package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
  65. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
  66. package/packages/datadog-instrumentations/src/helpers/extract-package-and-module-path.js +10 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +19 -2
  68. package/packages/datadog-instrumentations/src/helpers/hooks.js +4 -0
  69. package/packages/datadog-instrumentations/src/helpers/register.js +9 -3
  70. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +34 -9
  71. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  72. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +175 -0
  73. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
  75. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
  76. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +50 -18
  77. package/packages/datadog-instrumentations/src/helpers/router-helper.js +38 -7
  78. package/packages/datadog-instrumentations/src/helpers/shared-utils.js +39 -0
  79. package/packages/datadog-instrumentations/src/http/client.js +1 -3
  80. package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
  81. package/packages/datadog-instrumentations/src/jest.js +986 -246
  82. package/packages/datadog-instrumentations/src/mocha/common.js +1 -3
  83. package/packages/datadog-instrumentations/src/mocha/main.js +109 -51
  84. package/packages/datadog-instrumentations/src/mocha/utils.js +120 -32
  85. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +44 -0
  86. package/packages/datadog-instrumentations/src/mocha/worker.js +441 -43
  87. package/packages/datadog-instrumentations/src/moleculer/client.js +1 -1
  88. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  89. package/packages/datadog-instrumentations/src/next.js +155 -23
  90. package/packages/datadog-instrumentations/src/openai.js +14 -0
  91. package/packages/datadog-instrumentations/src/pg.js +180 -6
  92. package/packages/datadog-instrumentations/src/playwright.js +176 -81
  93. package/packages/datadog-instrumentations/src/promise-js.js +4 -4
  94. package/packages/datadog-instrumentations/src/promise.js +3 -3
  95. package/packages/datadog-instrumentations/src/rhea.js +1 -1
  96. package/packages/datadog-instrumentations/src/router.js +195 -19
  97. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +567 -65
  98. package/packages/datadog-instrumentations/src/vitest-main.js +438 -54
  99. package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
  100. package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
  101. package/packages/datadog-instrumentations/src/webdriverio.js +1090 -0
  102. package/packages/datadog-instrumentations/src/when.js +3 -3
  103. package/packages/datadog-instrumentations/src/ws.js +5 -1
  104. package/packages/datadog-plugin-aerospike/src/index.js +1 -3
  105. package/packages/datadog-plugin-amqplib/src/client.js +1 -1
  106. package/packages/datadog-plugin-amqplib/src/producer.js +1 -1
  107. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
  108. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +2 -2
  109. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +2 -2
  110. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +1 -3
  111. package/packages/datadog-plugin-azure-service-bus/src/producer.js +5 -3
  112. package/packages/datadog-plugin-bullmq/src/producer.js +3 -3
  113. package/packages/datadog-plugin-child_process/src/index.js +3 -3
  114. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -11
  115. package/packages/datadog-plugin-cucumber/src/index.js +5 -1
  116. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
  117. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -1
  118. package/packages/datadog-plugin-cypress/src/support.js +7 -4
  119. package/packages/datadog-plugin-electron/src/net.js +10 -4
  120. package/packages/datadog-plugin-fastify/src/tracing.js +1 -1
  121. package/packages/datadog-plugin-fs/src/index.js +0 -4
  122. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +14 -7
  123. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +19 -10
  124. package/packages/datadog-plugin-graphql/src/execute.js +1 -0
  125. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -1
  126. package/packages/datadog-plugin-http/src/client.js +3 -2
  127. package/packages/datadog-plugin-http2/src/client.js +1 -1
  128. package/packages/datadog-plugin-jest/src/index.js +3 -21
  129. package/packages/datadog-plugin-jest/src/util.js +4 -1
  130. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
  131. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  132. package/packages/datadog-plugin-mocha/src/index.js +342 -6
  133. package/packages/datadog-plugin-next/src/index.js +57 -9
  134. package/packages/datadog-plugin-openai/src/tracing.js +4 -0
  135. package/packages/datadog-plugin-pg/src/index.js +65 -1
  136. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  137. package/packages/datadog-plugin-prisma/src/index.js +5 -1
  138. package/packages/datadog-plugin-rhea/src/consumer.js +16 -13
  139. package/packages/datadog-plugin-rhea/src/producer.js +1 -1
  140. package/packages/datadog-plugin-undici/src/index.js +2 -1
  141. package/packages/datadog-plugin-vitest/src/index.js +179 -18
  142. package/packages/datadog-shimmer/src/shimmer.js +6 -2
  143. package/packages/datadog-webpack/index.js +2 -2
  144. package/packages/dd-trace/src/aiguard/sdk.js +3 -0
  145. package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
  146. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -1
  147. package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
  148. package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
  149. package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
  150. package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
  151. package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
  152. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-base-analyzer.js +1 -1
  153. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-analyzer.js +1 -1
  154. package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
  155. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +1 -1
  156. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +1 -3
  157. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +2 -2
  158. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +2 -2
  159. package/packages/dd-trace/src/appsec/index.js +48 -476
  160. package/packages/dd-trace/src/appsec/sdk/track_event.js +10 -1
  161. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  162. package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
  163. package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
  164. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +7 -6
  165. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +29 -15
  166. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +19 -1
  167. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +25 -1
  168. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/webdriverio.js +26 -0
  169. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -1
  170. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
  171. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
  172. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -3
  173. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
  174. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +2 -2
  175. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
  176. package/packages/dd-trace/src/config/defaults.js +14 -6
  177. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -2
  178. package/packages/dd-trace/src/config/helper.js +2 -0
  179. package/packages/dd-trace/src/config/index.js +31 -5
  180. package/packages/dd-trace/src/config/parsers.js +19 -1
  181. package/packages/dd-trace/src/config/remote_config.js +1 -1
  182. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  183. package/packages/dd-trace/src/datastreams/encoding.js +3 -4
  184. package/packages/dd-trace/src/debugger/devtools_client/log.js +2 -1
  185. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +1 -4
  186. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -2
  187. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +2 -2
  188. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +16 -2
  189. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  190. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
  191. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  192. package/packages/dd-trace/src/exporters/common/url.js +3 -5
  193. package/packages/dd-trace/src/exporters/electron/index.js +4 -1
  194. package/packages/dd-trace/src/external-logger/src/index.js +1 -2
  195. package/packages/dd-trace/src/flare/index.js +2 -2
  196. package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
  197. package/packages/dd-trace/src/guardrails/log.js +1 -10
  198. package/packages/dd-trace/src/id.js +1 -1
  199. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -1
  200. package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
  201. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -2
  202. package/packages/dd-trace/src/llmobs/experiments/dataset.js +66 -14
  203. package/packages/dd-trace/src/llmobs/experiments/experiment.js +289 -95
  204. package/packages/dd-trace/src/llmobs/experiments/index.js +66 -19
  205. package/packages/dd-trace/src/llmobs/experiments/noop.js +95 -10
  206. package/packages/dd-trace/src/llmobs/experiments/result.js +13 -2
  207. package/packages/dd-trace/src/llmobs/experiments/util.js +220 -0
  208. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  209. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
  210. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
  211. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +1 -1
  212. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
  213. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +1 -1
  214. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +1 -1
  215. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +1 -1
  216. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
  217. package/packages/dd-trace/src/llmobs/sdk.js +152 -78
  218. package/packages/dd-trace/src/llmobs/span_processor.js +9 -7
  219. package/packages/dd-trace/src/llmobs/tagger.js +1 -2
  220. package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
  221. package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
  222. package/packages/dd-trace/src/llmobs/writers/spans.js +12 -6
  223. package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
  224. package/packages/dd-trace/src/log/channels.js +1 -9
  225. package/packages/dd-trace/src/log/levels.js +12 -0
  226. package/packages/dd-trace/src/noop/proxy.js +2 -2
  227. package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
  228. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  229. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
  230. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
  231. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  232. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +3 -2
  233. package/packages/dd-trace/src/opentelemetry/span.js +4 -0
  234. package/packages/dd-trace/src/opentracing/span.js +1 -1
  235. package/packages/dd-trace/src/opentracing/span_context.js +1 -1
  236. package/packages/dd-trace/src/plugins/ci_plugin.js +124 -23
  237. package/packages/dd-trace/src/plugins/index.js +2 -0
  238. package/packages/dd-trace/src/plugins/plugin.js +1 -1
  239. package/packages/dd-trace/src/plugins/tracing.js +1 -0
  240. package/packages/dd-trace/src/plugins/util/ci.js +13 -6
  241. package/packages/dd-trace/src/plugins/util/git.js +7 -5
  242. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +3 -3
  243. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  244. package/packages/dd-trace/src/plugins/util/llm.js +2 -2
  245. package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
  246. package/packages/dd-trace/src/plugins/util/tags.js +2 -0
  247. package/packages/dd-trace/src/plugins/util/test.js +356 -86
  248. package/packages/dd-trace/src/plugins/util/url.js +5 -5
  249. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  250. package/packages/dd-trace/src/plugins/util/web.js +24 -2
  251. package/packages/dd-trace/src/priority_sampler.js +6 -3
  252. package/packages/dd-trace/src/profiler.js +1 -1
  253. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
  254. package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
  255. package/packages/dd-trace/src/profiling/profilers/poisson.js +1 -1
  256. package/packages/dd-trace/src/profiling/profilers/shared.js +2 -2
  257. package/packages/dd-trace/src/proxy.js +1 -0
  258. package/packages/dd-trace/src/ritm.js +10 -20
  259. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
  260. package/packages/dd-trace/src/sampling_rule.js +4 -2
  261. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
  262. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
  263. package/packages/dd-trace/src/startup-log.js +3 -0
  264. package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
  265. package/packages/dd-trace/src/telemetry/logs/log-collector.js +3 -1
  266. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  267. package/packages/dd-trace/src/util.js +16 -0
  268. package/vendor/dist/@apm-js-collab/code-transformer/index.js +12 -12
  269. package/vendor/dist/pprof-format/index.js +1 -1
  270. package/vendor/dist/source-map/index.js +1 -1
  271. package/version.js +10 -8
  272. /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
@@ -4,9 +4,12 @@ const fs = require('node:fs')
4
4
  const path = require('node:path')
5
5
 
6
6
  const { runDiagnosis } = require('../diagnose')
7
+ const { BLOCKER_CATEGORIES } = require('./blocker-category')
7
8
  const cucumber = require('./framework-adapters/cucumber')
8
9
  const cypress = require('./framework-adapters/cypress')
9
10
  const playwright = require('./framework-adapters/playwright')
11
+ const vitest = require('./framework-adapters/vitest')
12
+ const { isProjectBuildArtifactPath } = require('./project-build-artifact')
10
13
  const {
11
14
  GENERATED_SCENARIOS,
12
15
  getGeneratedRetryStatePath,
@@ -15,6 +18,7 @@ const {
15
18
  const { validateManifest } = require('./manifest-schema')
16
19
  const {
17
20
  getProjectNodeRunner,
21
+ getRunnerConfigurationContract,
18
22
  getRunnerContract,
19
23
  getRunnerSearchRoots,
20
24
  } = require('./runner-contract')
@@ -54,7 +58,7 @@ const CONFIG_PATTERNS = {
54
58
  jest: /^(?:jest|config-jest)\.config\.(?:[cm]?[jt]s|json)$/,
55
59
  mocha: /^\.mocharc\.(?:json|ya?ml|[cm]?js)$/,
56
60
  playwright: playwright.CONFIG_PATTERN,
57
- vitest: /^(?:vite|vitest)\.config\.[cm]?[jt]s$/,
61
+ vitest: /^(?:vite\.config|vitest\.(?:config|workspace))\.[cm]?[jt]s$/,
58
62
  }
59
63
  const JS_CONFIG_EXTENSIONS = ['js', 'cjs', 'mjs', 'ts', 'cts', 'mts']
60
64
  const IMPLICIT_CONFIG_FILENAMES = {
@@ -72,14 +76,20 @@ const IMPLICIT_CONFIG_FILENAMES = {
72
76
  vitest: JS_CONFIG_EXTENSIONS.flatMap(extension => [
73
77
  `vite.config.${extension}`,
74
78
  `vitest.config.${extension}`,
79
+ `vitest.workspace.${extension}`,
75
80
  ]),
76
81
  }
77
82
  const TEST_FILE_PATTERN = /^.+[._-](?:test|spec)\.[cm]?[jt]sx?$/
78
83
  const BARE_TEST_FILE_PATTERN = /^test\.[cm]?[jt]sx?$/
79
84
  const TYPE_ONLY_TEST_PATTERN = /\.(?:test|spec)-d\.[cm]?tsx?$|\.d\.[cm]?ts$/i
80
- const TYPE_ONLY_DIRECTORY_PATTERN = /(?:^|\/)(?:type[-_]?tests?|typetests?|test-dts?)(?:\/|$)/i
85
+ const TYPE_ONLY_DIRECTORY_PATTERN = /(?:^|\/)(?:type[-_]?tests?|test-dts?)(?:\/|$)/i
81
86
  const LOCAL_SOCKET_PATTERN =
82
- /(?:\bcreateServer\s*\(|\.listen\s*\(|\b(?:localhost|127\.0\.0\.1)\b|\bcy\.(?:visit|request)\s*\()/
87
+ /\bcreateServer\s*\(|\.listen\s*\(|\b(?:localhost|127\.0\.0\.1|supertest)\b|\bcy\.(?:visit|request)\s*\(/
88
+ const IMPORT_SPECIFIER_PATTERN = /(?:from\s+|require\s*\(\s*|import\s*\(\s*)(['"])([^'"]+)\1/g
89
+ const CUCUMBER_BROWSER_SUPPORT_DIRECTORY_PATTERN =
90
+ /(?:^|\/)(?:features?|helpers?|step_definitions|steps?|support)(?:\/|$)/i
91
+ const CUCUMBER_BROWSER_DRIVER_PATTERN =
92
+ /(?:from\s+|(?:import|require)\s*\(\s*)['"](?:@playwright\/test|nightwatch|playwright(?:-core)?|puppeteer(?:-core)?|selenium-webdriver|webdriverio)['"]/
83
93
  const CYPRESS_LOCAL_ORIGIN_PATTERN =
84
94
  /\bcy\.(?:visit|request)\s*\([\s\S]{0,512}\b(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)(?=[:/'"`\s}])/i
85
95
 
@@ -121,7 +131,11 @@ function createManifestScaffold ({ root, frameworks = new Set() }) {
121
131
  ciDiscovery,
122
132
  frameworks: [
123
133
  ...eligible.map(detection => buildFramework(repositoryRoot, detection, ciDiscovery)),
124
- ...detectedOnly.map(detection => buildDetectedOnlyFramework(repositoryRoot, detection, ciDiscovery)),
134
+ ...detectedOnly.map(detection => {
135
+ return detection.supportedVersion && SUPPORTED_FRAMEWORKS.has(detection.id)
136
+ ? buildFramework(repositoryRoot, detection, ciDiscovery)
137
+ : buildDetectedOnlyFramework(repositoryRoot, detection, ciDiscovery)
138
+ }),
125
139
  ...unsupported.map(detection => buildUnsupportedFramework(repositoryRoot, detection, ciDiscovery)),
126
140
  ],
127
141
  omitted: [],
@@ -144,7 +158,15 @@ function createManifestScaffold ({ root, frameworks = new Set() }) {
144
158
  */
145
159
  function buildFramework (repositoryRoot, detection, ciDiscovery) {
146
160
  const framework = detection.id
147
- const packageJson = getDetectionPackageJson(repositoryRoot, detection.commandLocation)
161
+ const packageJson = getFrameworkPackageJson(repositoryRoot, detection)
162
+ if (!packageJson) {
163
+ return buildDetectedOnlyFramework(
164
+ repositoryRoot,
165
+ detection,
166
+ ciDiscovery,
167
+ `The package that owns ${detection.name} could not be identified from the bounded detection evidence.`
168
+ )
169
+ }
148
170
  const projectRoot = path.dirname(packageJson.path)
149
171
  const base = getFrameworkBase({
150
172
  ciDiscovery,
@@ -158,16 +180,32 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
158
180
  if (!SUPPORTED_FRAMEWORKS.has(framework)) {
159
181
  return {
160
182
  ...base,
183
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
161
184
  status: 'unsupported_by_validator',
162
185
  notes: [`${detection.name} is supported by dd-trace, but this validator has no direct-runner adapter.`],
163
186
  }
164
187
  }
165
188
 
189
+ if (framework === 'cucumber' && !cucumber.supportsConfigIsolation(base.frameworkVersion)) {
190
+ return {
191
+ ...base,
192
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
193
+ status: 'requires_manual_setup',
194
+ notes: [
195
+ `${detection.name} ${base.frameworkVersion || 'with an unknown version'} is supported by dd-trace, but its ` +
196
+ 'CLI cannot bypass customer profiles with a validator-owned config. Live validation is unavailable because ' +
197
+ 'changing the working directory or loading the profile dynamically would weaken isolation. The static CI ' +
198
+ 'audit can still run.',
199
+ ],
200
+ }
201
+ }
202
+
166
203
  const projectRunner = getProjectNodeRunner(detection.command, projectRoot, repositoryRoot)
167
204
  const runner = projectRunner || resolveRunner(framework, projectRoot, repositoryRoot)
168
205
  if (!runner) {
169
206
  return {
170
207
  ...base,
208
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
171
209
  status: 'requires_manual_setup',
172
210
  notes: [
173
211
  `${detection.name} is detected, but its repository-contained executable is unavailable. Install this ` +
@@ -176,10 +214,13 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
176
214
  }
177
215
  }
178
216
 
179
- const runnerContract = getRunnerContract(framework, detection.command, projectRoot, repositoryRoot)
217
+ const projectFiles = collectProjectFiles(projectRoot)
218
+ const commandRoots = getRunnerSearchRoots(framework, detection.command, projectRoot, repositoryRoot)
219
+ let runnerContract = getRunnerContract(framework, detection.command, projectRoot, repositoryRoot)
180
220
  if (runnerContract.error) {
181
221
  return {
182
222
  ...base,
223
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
183
224
  status: 'requires_manual_setup',
184
225
  notes: [
185
226
  `${detection.name} runner configuration was not retained because ${runnerContract.error}. ` +
@@ -187,23 +228,91 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
187
228
  ],
188
229
  }
189
230
  }
190
- const commandRoots = getRunnerSearchRoots(framework, detection.command, projectRoot, repositoryRoot)
191
- const representativeRoot = commandRoots[0] || findPreferredRepresentativeRoot(projectRoot, repositoryRoot)
231
+ const implicitConfigFiles = [...new Set([
232
+ ...getImplicitConfigFiles(framework, projectRoot, repositoryRoot),
233
+ ...(framework === 'vitest' && commandRoots[0]
234
+ ? getImplicitConfigFiles(framework, commandRoots[0], repositoryRoot)
235
+ : []),
236
+ ])]
237
+ if (framework === 'cucumber') {
238
+ const expanded = cucumber.expandProfiles({
239
+ configFiles: [...new Set([...runnerContract.inputFiles, ...implicitConfigFiles])],
240
+ projectFiles,
241
+ projectRoot,
242
+ runnerArgs: runnerContract.runnerArgs,
243
+ })
244
+ if (expanded.error) {
245
+ return {
246
+ ...base,
247
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
248
+ status: 'requires_manual_setup',
249
+ notes: [
250
+ `${detection.name} runner configuration was not retained because ${expanded.error}. ` +
251
+ 'Use a literal Cucumber profile or a direct command with bounded support-code options, then create a ' +
252
+ 'fresh plan.',
253
+ ],
254
+ }
255
+ }
256
+ const expandedContract = getRunnerConfigurationContract(
257
+ framework,
258
+ expanded.runnerArgs,
259
+ runnerContract.environment,
260
+ projectRoot,
261
+ repositoryRoot
262
+ )
263
+ if (expandedContract.error) {
264
+ return {
265
+ ...base,
266
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
267
+ status: 'requires_manual_setup',
268
+ notes: [
269
+ `${detection.name} profile configuration was not retained because ${expandedContract.error}. ` +
270
+ 'Use repository-contained support-code inputs, then create a fresh plan.',
271
+ ],
272
+ }
273
+ }
274
+ runnerContract = {
275
+ ...expandedContract,
276
+ omittedOptions: runnerContract.omittedOptions,
277
+ }
278
+ }
279
+ const vitestProject = framework === 'vitest'
280
+ ? vitest.bindLiteralProject({
281
+ configFiles: [...new Set([...runnerContract.inputFiles, ...implicitConfigFiles])],
282
+ projectFiles,
283
+ projectRoot,
284
+ runnerArgs: runnerContract.runnerArgs,
285
+ })
286
+ : undefined
287
+ if (vitestProject?.error) {
288
+ return {
289
+ ...base,
290
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
291
+ status: 'requires_manual_setup',
292
+ notes: [
293
+ `${detection.name} runner configuration was not retained because ${vitestProject.error}. ` +
294
+ 'Use one literal, uniquely named Vitest project with static configuration, then create a fresh plan.',
295
+ ],
296
+ }
297
+ }
298
+ const representativeRoot = vitestProject?.root ||
299
+ commandRoots[0] ||
300
+ findPreferredRepresentativeRoot(projectRoot, repositoryRoot)
192
301
  const representativePackage = readJson(path.join(representativeRoot, 'package.json')) || packageJson.json
193
- const projectFiles = collectProjectFiles(projectRoot)
194
- const candidateFiles = representativeRoot === projectRoot
195
- ? projectFiles
196
- : collectProjectFiles(representativeRoot)
197
- const candidate = selectRepresentativeTest(
302
+ const candidateFiles = vitestProject?.files ||
303
+ (representativeRoot === projectRoot ? projectFiles : collectProjectFiles(representativeRoot))
304
+ const candidates = selectRepresentativeTests(
198
305
  candidateFiles,
199
306
  framework,
200
307
  representativeRoot,
201
308
  representativePackage.name,
202
- commandRoots.length > 0
309
+ commandRoots.length > 0 || vitestProject !== undefined
203
310
  )
311
+ const candidate = candidates[0]
204
312
  if (!candidate) {
205
313
  return {
206
314
  ...base,
315
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
207
316
  status: 'requires_manual_setup',
208
317
  notes: [
209
318
  `No single ${detection.name} test file could be selected confidently. Choose a normal framework-owned ` +
@@ -214,6 +323,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
214
323
  if (candidate.requiresExternalService) {
215
324
  return {
216
325
  ...base,
326
+ blockerCategory: BLOCKER_CATEGORIES.PROJECT_SETUP_REQUIRED,
217
327
  status: 'requires_manual_setup',
218
328
  notes: [
219
329
  `No self-contained ${detection.name} spec could be selected. The available spec accesses a localhost ` +
@@ -223,14 +333,23 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
223
333
  }
224
334
  }
225
335
 
226
- const generatedTestStrategy = buildGeneratedTestStrategy({
336
+ const plannedGeneratedTestStrategy = buildGeneratedTestStrategy({
227
337
  framework,
228
- projectRoot,
338
+ projectRoot: vitestProject?.root || projectRoot,
229
339
  representative: candidate.path,
230
340
  })
341
+ const generatedTestStrategy = vitestProject &&
342
+ !vitest.supportsGeneratedFiles(vitestProject, plannedGeneratedTestStrategy)
343
+ ? {
344
+ reason: 'The selected Vitest project include or exclude patterns do not collect ' +
345
+ 'validator-generated test files.',
346
+ status: 'not_possible',
347
+ }
348
+ : plannedGeneratedTestStrategy
231
349
  const configFiles = [...new Set([
232
350
  ...runnerContract.inputFiles,
233
- ...getImplicitConfigFiles(framework, projectRoot, repositoryRoot),
351
+ ...(vitestProject?.configFile ? [vitestProject.configFile] : []),
352
+ ...implicitConfigFiles,
234
353
  ...(representativeRoot === projectRoot
235
354
  ? []
236
355
  : getImplicitConfigFiles(framework, representativeRoot, repositoryRoot)),
@@ -239,6 +358,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
239
358
  if (configFiles.length > 20) {
240
359
  return {
241
360
  ...base,
361
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
242
362
  status: 'requires_manual_setup',
243
363
  notes: [
244
364
  `${detection.name} loads more configuration files than the validator can approval-bind safely. ` +
@@ -247,13 +367,38 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
247
367
  }
248
368
  }
249
369
  const runnerDescription = projectRunner ? 'repository test wrapper' : `installed ${framework} runner`
370
+ const runnerConfigurationNote = detection.command
371
+ ? 'The validator retained only allowlisted runner configuration from the detected package script.'
372
+ : 'The validator uses the installed runner with repository-contained configuration files; the unresolved ' +
373
+ 'package wrapper is not executed.'
374
+ const cucumberBrowserRequired = framework === 'cucumber' &&
375
+ hasCucumberBrowserSupport(candidateFiles, runnerContract.inputFiles, representativeRoot)
250
376
  const browserRequired = framework === 'cypress' ||
251
377
  framework === 'playwright' ||
378
+ cucumberBrowserRequired ||
252
379
  (framework === 'vitest' && runnerContract.runnerArgs.includes('--browser'))
380
+ const fallbackCandidates = candidates
381
+ .slice(1)
382
+ .filter(candidate => hasGeneratedTestContract(
383
+ candidate.path,
384
+ framework,
385
+ vitestProject?.root || projectRoot,
386
+ plannedGeneratedTestStrategy
387
+ ))
388
+ const fallbackTests = fallbackCandidates
389
+ .map(candidate => ({
390
+ buildArtifactRequired: candidate.requiresBuildArtifact,
391
+ localSocketRequired: candidate.requiresLocalSocket,
392
+ testFile: candidate.path,
393
+ }))
394
+ const allCandidatesRequireLocalSocket =
395
+ [candidate, ...fallbackCandidates].every(candidate => candidate.requiresLocalSocket)
253
396
 
254
397
  return {
255
398
  ...base,
399
+ allCandidatesRequireLocalSocket,
256
400
  browserRequired,
401
+ buildArtifactRequired: candidate.requiresBuildArtifact,
257
402
  language: /\.[cm]?tsx?$/.test(candidate.path) ? 'typescript' : 'javascript',
258
403
  localSocketRequired: candidate.requiresLocalSocket,
259
404
  status: 'runnable',
@@ -272,6 +417,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
272
417
  runnerArgs: runnerContract.runnerArgs,
273
418
  selectorScope: projectRunner ? 'instrumented_event_identity' : 'bounded_direct_runner',
274
419
  testFile: candidate.path,
420
+ ...(fallbackTests.length > 0 ? { fallbackTests } : {}),
275
421
  timeoutMs: browserRequired ? 300_000 : 180_000,
276
422
  },
277
423
  preflight: { status: 'pending' },
@@ -280,7 +426,7 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
280
426
  `Basic Reporting will invoke the ${runnerDescription} ` +
281
427
  'directly for ' +
282
428
  `${path.relative(repositoryRoot, candidate.path)}.`,
283
- 'The validator retained only allowlisted runner configuration from the detected package script.',
429
+ runnerConfigurationNote,
284
430
  ...(projectRunner
285
431
  ? [
286
432
  'Basic Reporting will remain incomplete unless captured test events identify only the approved ' +
@@ -289,8 +435,20 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
289
435
  : []),
290
436
  ...(candidate.requiresLocalSocket
291
437
  ? [
292
- 'The selected test appears to require localhost. A restricted sandbox may leave local validation ' +
293
- 'incomplete.',
438
+ `${allCandidatesRequireLocalSocket ? 'Every approved candidate appears' : 'The selected test appears'} ` +
439
+ 'to require localhost. A restricted execution environment may leave local validation incomplete.',
440
+ ]
441
+ : []),
442
+ ...(candidate.requiresBuildArtifact
443
+ ? [
444
+ 'The selected test appears to load a build or dist artifact. Complete the project\'s normal build before ' +
445
+ 'validation if that artifact is not already present.',
446
+ ]
447
+ : []),
448
+ ...(cucumberBrowserRequired
449
+ ? [
450
+ 'The selected Cucumber support code imports a browser driver. The approved validation command may launch ' +
451
+ 'the project browser.',
294
452
  ]
295
453
  : []),
296
454
  ],
@@ -299,15 +457,17 @@ function buildFramework (repositoryRoot, detection, ciDiscovery) {
299
457
 
300
458
  function getImplicitConfigFiles (framework, projectRoot, repositoryRoot) {
301
459
  const files = []
302
- for (const basename of IMPLICIT_CONFIG_FILENAMES[framework] || []) {
303
- const filename = path.join(projectRoot, basename)
304
- try {
305
- const stat = fs.lstatSync(filename)
306
- const physical = fs.realpathSync(filename)
307
- if (stat.isFile() && !stat.isSymbolicLink() &&
308
- fs.statSync(physical).isFile() &&
309
- isPathInside(fs.realpathSync(repositoryRoot), physical)) files.push(physical)
310
- } catch {}
460
+ if (IMPLICIT_CONFIG_FILENAMES[framework]) {
461
+ for (const basename of IMPLICIT_CONFIG_FILENAMES[framework]) {
462
+ const filename = path.join(projectRoot, basename)
463
+ try {
464
+ const stat = fs.lstatSync(filename)
465
+ const physical = fs.realpathSync(filename)
466
+ if (stat.isFile() && !stat.isSymbolicLink() &&
467
+ fs.statSync(physical).isFile() &&
468
+ isPathInside(fs.realpathSync(repositoryRoot), physical)) files.push(physical)
469
+ } catch {}
470
+ }
311
471
  }
312
472
  return files
313
473
  }
@@ -318,14 +478,17 @@ function getImplicitConfigFiles (framework, projectRoot, repositoryRoot) {
318
478
  * @param {string} repositoryRoot repository root
319
479
  * @param {object} detection supported framework detection
320
480
  * @param {object} ciDiscovery bounded CI discovery result
481
+ * @param {string} [blockerNote] precise reason live validation is unavailable
321
482
  * @returns {object} framework manifest entry
322
483
  */
323
- function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery) {
324
- const packageJson = getDetectionPackageJson(repositoryRoot, detection.locations?.[0])
484
+ function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery, blockerNote) {
485
+ const packageJson = getDetectionPackageJson(repositoryRoot, getDetectionPackageLocation(detection))
325
486
  const projectRoot = path.dirname(packageJson.path)
326
487
  const version = detection.supportedVersion?.version ||
327
488
  detection.versionDetections?.[0]?.version ||
328
489
  detection.versionDetections?.[0]?.rawVersion
490
+ const unsupportedVersion = !detection.supportedVersion &&
491
+ (detection.versionDetections?.[0]?.version || detection.versionDetections?.[0]?.rawVersion)
329
492
  return {
330
493
  ...getFrameworkBase({
331
494
  ciDiscovery,
@@ -335,11 +498,18 @@ function buildDetectedOnlyFramework (repositoryRoot, detection, ciDiscovery) {
335
498
  projectRoot,
336
499
  repositoryRoot,
337
500
  }),
501
+ blockerCategory: unsupportedVersion
502
+ ? BLOCKER_CATEGORIES.UNSUPPORTED_VERSION
503
+ : BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
338
504
  status: 'detected_not_runnable',
339
505
  notes: [
340
- detection.supportedVersion
341
- ? `A supported ${detection.name} installation was detected, but no project-local validation target was found.`
342
- : `${detection.name} was detected, but no supported installed version was confirmed.`,
506
+ blockerNote || (unsupportedVersion
507
+ ? `${detection.name} ${unsupportedVersion} was detected, but live validation requires ` +
508
+ `${detection.supportedRange}. Upgrade ${detection.name} through the project's normal dependency workflow ` +
509
+ 'before creating a fresh validation plan.'
510
+ : detection.supportedVersion
511
+ ? `A supported ${detection.name} installation was detected, but no project-local validation target was found.`
512
+ : `${detection.name} was detected, but no supported installed version was confirmed.`),
343
513
  ],
344
514
  }
345
515
  }
@@ -365,6 +535,7 @@ function buildUnsupportedFramework (repositoryRoot, detection, ciDiscovery) {
365
535
  projectRoot,
366
536
  repositoryRoot,
367
537
  }),
538
+ blockerCategory: BLOCKER_CATEGORIES.VALIDATOR_LIMITATION,
368
539
  status: 'unsupported_by_validator',
369
540
  notes: [`${detection.name} is not supported by this Test Optimization validator.`],
370
541
  }
@@ -477,8 +648,7 @@ function buildGeneratedTestStrategy ({ framework, projectRoot, representative })
477
648
  const stepsFile = cucumber.getGeneratedStepsPath(testDirectory)
478
649
  files.push({ path: stepsFile, contentLines: cucumber.getGeneratedStepsContent().split('\n') })
479
650
  cleanupPaths.push(stepsFile)
480
- }
481
- if (framework === 'playwright') {
651
+ } else if (framework === 'playwright') {
482
652
  const configFile = playwright.getGeneratedConfigPath(testDirectory)
483
653
  files.push({ path: configFile, contentLines: playwright.getGeneratedConfigContent().split('\n') })
484
654
  cleanupPaths.push(configFile)
@@ -499,17 +669,29 @@ function buildGeneratedTestStrategy ({ framework, projectRoot, representative })
499
669
  }
500
670
  }
501
671
 
672
+ function hasGeneratedTestContract (representative, framework, projectRoot, strategy) {
673
+ const convention = getGeneratedTestConvention(framework, representative, projectRoot)
674
+ return convention.fileExtension === strategy.fileExtension &&
675
+ convention.testDirectory === strategy.testDirectory &&
676
+ getModuleSystem(representative, projectRoot) === strategy.moduleSystem
677
+ }
678
+
502
679
  /**
503
- * Selects one representative framework-owned test file.
680
+ * Selects up to three representative framework-owned test files.
504
681
  *
505
682
  * @param {string[]} files bounded project files
506
683
  * @param {string} framework framework name
507
684
  * @param {string} projectRoot project root
508
685
  * @param {string} packageName project package name
509
- * @param {boolean} allowDirectoryConvention whether a literal runner selector chose this root
510
- * @returns {{path: string, requiresExternalService: boolean, requiresLocalSocket: boolean}|undefined} selected test
686
+ * @param {boolean} allowDirectoryConvention whether a literal runner selector owns this root
687
+ * @returns {Array<{
688
+ * path: string,
689
+ * requiresBuildArtifact: boolean,
690
+ * requiresExternalService: boolean,
691
+ * requiresLocalSocket: boolean
692
+ * }>} selected tests
511
693
  */
512
- function selectRepresentativeTest (files, framework, projectRoot, packageName, allowDirectoryConvention) {
694
+ function selectRepresentativeTests (files, framework, projectRoot, packageName, allowDirectoryConvention) {
513
695
  const candidates = []
514
696
  for (const filename of files) {
515
697
  const source = readText(filename)
@@ -521,18 +703,39 @@ function selectRepresentativeTest (files, framework, projectRoot, packageName, a
521
703
  : getStaticTestCount(source) === 0) continue
522
704
  if (!hasFrameworkOwnership(filename, source, framework, packageName)) continue
523
705
  candidates.push({
706
+ explicitFilename: TEST_FILE_PATTERN.test(path.basename(filename)),
524
707
  path: filename,
525
708
  rank: getTestRank(filename, source, projectRoot),
709
+ requiresBuildArtifact: requiresProjectBuildArtifact(source, projectRoot, path.dirname(filename)),
526
710
  requiresExternalService: framework === 'cypress' && CYPRESS_LOCAL_ORIGIN_PATTERN.test(source),
527
711
  requiresLocalSocket: LOCAL_SOCKET_PATTERN.test(source),
528
712
  })
529
713
  }
530
714
  candidates.sort((left, right) => {
531
715
  return Number(left.requiresExternalService) - Number(right.requiresExternalService) ||
716
+ Number(right.explicitFilename) - Number(left.explicitFilename) ||
532
717
  left.rank - right.rank ||
533
718
  left.path.localeCompare(right.path)
534
719
  })
535
- return candidates[0]
720
+ return candidates.slice(0, 3)
721
+ }
722
+
723
+ function requiresProjectBuildArtifact (source, projectRoot, sourceDirectory) {
724
+ for (const match of source.matchAll(IMPORT_SPECIFIER_PATTERN)) {
725
+ if (isProjectBuildArtifactPath(match[2], projectRoot, sourceDirectory)) return true
726
+ }
727
+ return false
728
+ }
729
+
730
+ function hasCucumberBrowserSupport (files, retainedInputs, projectRoot) {
731
+ const retained = new Set(retainedInputs)
732
+ for (const filename of new Set([...files, ...retained])) {
733
+ const relative = path.relative(projectRoot, filename).replaceAll('\\', '/')
734
+ if (!retained.has(filename) && !CUCUMBER_BROWSER_SUPPORT_DIRECTORY_PATTERN.test(relative)) continue
735
+ const source = readText(filename)
736
+ if (source !== undefined && CUCUMBER_BROWSER_DRIVER_PATTERN.test(source)) return true
737
+ }
738
+ return false
536
739
  }
537
740
 
538
741
  /**
@@ -542,7 +745,7 @@ function selectRepresentativeTest (files, framework, projectRoot, packageName, a
542
745
  * @param {string} source candidate source
543
746
  * @param {string} framework framework name
544
747
  * @param {string} projectRoot project root
545
- * @param {boolean} allowDirectoryConvention whether a literal runner selector chose this root
748
+ * @param {boolean} allowDirectoryConvention whether a literal runner selector owns this root
546
749
  * @returns {boolean} whether the file is a candidate
547
750
  */
548
751
  function isTestFile (filename, source, framework, projectRoot, allowDirectoryConvention) {
@@ -562,7 +765,7 @@ function isTestFile (filename, source, framework, projectRoot, allowDirectoryCon
562
765
  return ['__tests__', 'spec', 'test', 'tests'].includes(directory)
563
766
  })
564
767
  if (BARE_TEST_FILE_PATTERN.test(basename)) {
565
- return inTestDirectory || hasExplicitFrameworkImport(source, framework)
768
+ return (allowDirectoryConvention && inTestDirectory) || hasExplicitFrameworkImport(source, framework)
566
769
  }
567
770
  return allowDirectoryConvention && inTestDirectory && /\.[cm]?[jt]sx?$/.test(basename)
568
771
  }
@@ -598,14 +801,14 @@ function hasExplicitFrameworkImport (source, framework) {
598
801
  */
599
802
  function hasFrameworkOwnership (filename, source, framework, packageName) {
600
803
  const normalized = filename.replaceAll('\\', '/').toLowerCase()
601
- if (framework === 'cucumber') return /^\s*(?:Feature|Rule):/m.test(source)
804
+ if (framework === 'cucumber') return /^[ \t]*(?:Feature|Rule):/m.test(source)
602
805
  if (framework === 'cypress') return /\.cy\./.test(filename) || normalized.includes('/cypress/')
603
806
  if (framework === 'playwright') {
604
807
  return /@playwright\/test/.test(source) || normalized.includes('/playwright/') ||
605
808
  normalized.includes('/e2e/')
606
809
  }
607
- if (framework === 'vitest') return /\bvitest\b/.test(source) || /\b(?:describe|it|test)\s*\(/.test(source)
608
- if (framework === 'jest') return /\bjest\b/.test(source) || /\b(?:describe|it|test)\s*\(/.test(source)
810
+ if (framework === 'vitest') return /\bvitest\b/.test(source) || getStaticTestCount(source) > 0
811
+ if (framework === 'jest') return /\bjest\b/.test(source) || getStaticTestCount(source) > 0
609
812
  if (framework === 'mocha') {
610
813
  return /\bmocha\b/.test(source) || /\bdescribe\s*\(/.test(source) ||
611
814
  packageName === 'mocha'
@@ -644,7 +847,7 @@ function getTestRank (filename, source, projectRoot) {
644
847
  let rank = relative.split('/').length
645
848
  if (/(?:^|\/)(?:test|tests|__tests__|spec)\//.test(relative)) rank -= 20
646
849
  if (/(?:^|[._/-])unit(?:[._/-]|$)/.test(relative)) rank -= 15
647
- if (/(?:e2e|integration|acceptance|browser|conformance|fixtures?)/.test(relative)) rank += 20
850
+ if (/e2e|integration|acceptance|browser|conformance|fixtures?/.test(relative)) rank += 20
648
851
  if (LOCAL_SOCKET_PATTERN.test(source)) rank += 30
649
852
  rank += Math.min(getStaticTestCount(source), 50)
650
853
  return rank
@@ -657,9 +860,12 @@ function getTestRank (filename, source, projectRoot) {
657
860
  * @returns {number} approximate test declaration count
658
861
  */
659
862
  function getStaticTestCount (source) {
660
- return [...source.matchAll(/\b(?:it|test)((?:\.[A-Za-z]+)*)\s*\(/g)]
863
+ const direct = [...source.matchAll(/\b(?:it|test)((?:\.[A-Za-z]+)*)\s*\(\s*(['"`])/g)]
661
864
  .filter(match => !/\.(?:skip|todo)\b/.test(match[1]))
662
- .length
865
+ const parameterized = [...source.matchAll(
866
+ /\b(?:it|test)((?:\.[A-Za-z]+)*)\.each\s*(?:\([^()]*\)|`(?:\\[\s\S]|[^\\`])*`)((?:\.[A-Za-z]+)*)\s*\(\s*(['"`])/g
867
+ )].filter(match => !/\.(?:skip|todo)\b/.test(`${match[1]}${match[2]}`))
868
+ return direct.length + parameterized.length
663
869
  }
664
870
 
665
871
  /**
@@ -824,9 +1030,9 @@ function getTestExtension (framework, representative) {
824
1030
  if (framework === 'cucumber') return '.feature'
825
1031
  if (framework === 'cypress') return cypress.getTestExtension(representative)
826
1032
  if (framework === 'playwright') return playwright.getTestExtension(representative)
827
- const match = /((?:[.-](?:test|spec))\.[cm]?[jt]sx?)$/.exec(representative)
1033
+ const match = /([.-](?:test|spec)\.[cm]?[jt]sx?)$/.exec(representative)
828
1034
  if (match) return match[1]
829
- const moduleExtension = /\.((?:[cm]js|[cm]ts))$/.exec(representative)?.[1]
1035
+ const moduleExtension = /\.([cm]js|[cm]ts)$/.exec(representative)?.[1]
830
1036
  return moduleExtension ? `.test.${moduleExtension}` : '.test.js'
831
1037
  }
832
1038
 
@@ -900,7 +1106,7 @@ function rankCiReviewTargets (files) {
900
1106
  */
901
1107
  function getCiRank (filename) {
902
1108
  const value = filename.toLowerCase()
903
- if (/(?:test|ci|build|unit|integration)/.test(value)) return 0
1109
+ if (/test|ci|build|unit|integration/.test(value)) return 0
904
1110
  return 10
905
1111
  }
906
1112
 
@@ -923,6 +1129,49 @@ function getDetectionPackageJson (repositoryRoot, location) {
923
1129
  }
924
1130
  }
925
1131
 
1132
+ function getFrameworkPackageJson (repositoryRoot, detection) {
1133
+ const preciseLocation = detection.commandLocation ||
1134
+ detection.supportedVersion?.relativePath ||
1135
+ detection.eligibleCommand?.relativePath ||
1136
+ detection.versionLocation
1137
+ if (preciseLocation) return findOwningPackageJson(repositoryRoot, preciseLocation)
1138
+
1139
+ const owners = new Map()
1140
+ if (detection.locations) {
1141
+ for (const location of detection.locations) {
1142
+ const owner = findOwningPackageJson(repositoryRoot, location)
1143
+ if (owner) owners.set(owner.path, owner)
1144
+ }
1145
+ }
1146
+ if (owners.size === 1) return [...owners.values()][0]
1147
+ }
1148
+
1149
+ function findOwningPackageJson (repositoryRoot, location) {
1150
+ let candidate = path.resolve(repositoryRoot, location)
1151
+ if (!isPathInside(repositoryRoot, candidate)) return
1152
+ try {
1153
+ if (!fs.statSync(candidate).isDirectory()) candidate = path.dirname(candidate)
1154
+ } catch {
1155
+ candidate = path.dirname(candidate)
1156
+ }
1157
+
1158
+ while (isPathInside(repositoryRoot, candidate)) {
1159
+ const packageJson = path.join(candidate, 'package.json')
1160
+ const json = readJson(packageJson)
1161
+ if (json) return { json, path: packageJson }
1162
+ if (candidate === repositoryRoot) return
1163
+ candidate = path.dirname(candidate)
1164
+ }
1165
+ }
1166
+
1167
+ function getDetectionPackageLocation (detection) {
1168
+ return detection.commandLocation ||
1169
+ detection.supportedVersion?.relativePath ||
1170
+ detection.eligibleCommand?.relativePath ||
1171
+ detection.versionLocation ||
1172
+ detection.locations?.[0]
1173
+ }
1174
+
926
1175
  /**
927
1176
  * Safely reads a bounded directory.
928
1177
  *