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
@@ -1,10 +1,97 @@
1
1
  'use strict'
2
2
 
3
+ const fs = require('node:fs')
3
4
  const path = require('node:path')
4
5
 
6
+ const { matchesLiteralGlob } = require('../literal-glob')
7
+
5
8
  const CUCUMBER_PACKAGE = '@cucumber/cucumber'
6
9
  const CONFIG_PATTERN = /^cucumber\.(?:[cm]?js|json|ya?ml)$/
10
+ const MAX_CONFIG_BYTES = 512 * 1024
7
11
  const GENERATED_STEPS_FILENAME = 'dd-test-optimization-validation.steps.cjs'
12
+ const ISOLATION_CONFIG_PATH = path.join(__dirname, 'cucumber-validation.json')
13
+ const PROFILE_VALUE_OPTIONS = new Map([
14
+ ['import', '--import'],
15
+ ['language', '--language'],
16
+ ['loader', '--loader'],
17
+ ['parallel', '--parallel'],
18
+ ['require', '--require'],
19
+ ['requireModule', '--require-module'],
20
+ ['worldParameters', '--world-parameters'],
21
+ ])
22
+ const PROFILE_FLAG_OPTIONS = new Map([
23
+ ['backtrace', '--backtrace'],
24
+ ['failFast', '--fail-fast'],
25
+ ['strict', '--strict'],
26
+ ])
27
+ const OMITTED_PROFILE_KEYS = new Set([
28
+ 'dryRun',
29
+ 'forceExit',
30
+ 'format',
31
+ 'formatOptions',
32
+ 'name',
33
+ 'order',
34
+ 'paths',
35
+ 'publish',
36
+ 'publishQuiet',
37
+ 'retry',
38
+ 'retryTagFilter',
39
+ 'tags',
40
+ ])
41
+ const OMITTED_PROFILE_OPTIONS = new Map([
42
+ ['-f', true],
43
+ ['--dry-run', false],
44
+ ['--exit', false],
45
+ ['--force-exit', false],
46
+ ['--format', true],
47
+ ['--format-options', true],
48
+ ['--name', true],
49
+ ['--order', true],
50
+ ['--publish', false],
51
+ ['--publish-quiet', false],
52
+ ['--retry', true],
53
+ ['--retry-tag-filter', true],
54
+ ['--tags', true],
55
+ ['-t', true],
56
+ ])
57
+ const RETAINED_PROFILE_FLAGS = new Set(['--backtrace', '--fail-fast', '--no-strict', '--strict'])
58
+ const RETAINED_PROFILE_VALUES = new Set([
59
+ '-i',
60
+ '-r',
61
+ '--import',
62
+ '--language',
63
+ '--loader',
64
+ '--parallel',
65
+ '--require',
66
+ '--require-module',
67
+ '--world-parameters',
68
+ ])
69
+ const JAVASCRIPT_STRING_ESCAPES = Object.freeze({
70
+ '"': '"',
71
+ "'": "'",
72
+ '/': '/',
73
+ '\\': '\\',
74
+ b: '\b',
75
+ f: '\f',
76
+ n: '\n',
77
+ r: '\r',
78
+ t: '\t',
79
+ v: '\v',
80
+ })
81
+
82
+ /**
83
+ * Reports whether the installed Cucumber CLI can bypass customer profiles with a validator-owned config.
84
+ *
85
+ * Cucumber 7 always loads cucumber.js from its working directory and has no --config option. Changing the working
86
+ * directory would alter project test semantics, so those versions remain a validator limitation.
87
+ *
88
+ * @param {string|null|undefined} version installed Cucumber version
89
+ * @returns {boolean} whether validator-owned config isolation is supported
90
+ */
91
+ function supportsConfigIsolation (version) {
92
+ const match = /^[v=]?(\d+)(?:\.|$)/.exec(String(version || ''))
93
+ return Boolean(match && Number(match[1]) >= 8)
94
+ }
8
95
 
9
96
  /**
10
97
  * Reports whether a file follows the Cucumber feature convention.
@@ -23,7 +110,7 @@ function isTestFile (filename) {
23
110
  * @returns {number} declared scenario count
24
111
  */
25
112
  function getScenarioCount (source) {
26
- return [...source.matchAll(/^\s*Scenario(?: Outline)?:\s*\S/gm)].length
113
+ return [...source.matchAll(/^[ \t]*(?:Example|Scenario(?: Outline| Template)?):[ \t]*\S/gm)].length
27
114
  }
28
115
 
29
116
  /**
@@ -77,10 +164,11 @@ function getGeneratedStepsPath (testDirectory) {
77
164
  * Returns Cucumber arguments that select one existing feature.
78
165
  *
79
166
  * @param {string} filename selected Cucumber feature
167
+ * @param {string} cwd project working directory
80
168
  * @returns {string[]} focused Cucumber arguments
81
169
  */
82
- function getFocusedTestArgs (filename) {
83
- return [filename, '--format', 'progress']
170
+ function getFocusedTestArgs (filename, cwd) {
171
+ return [...getIsolationArgs(cwd), filename, '--format', 'json']
84
172
  }
85
173
 
86
174
  /**
@@ -88,10 +176,11 @@ function getFocusedTestArgs (filename) {
88
176
  *
89
177
  * @param {string} filename generated Cucumber feature
90
178
  * @param {string} stepsFile generated Cucumber step definitions
179
+ * @param {string} cwd project working directory
91
180
  * @returns {string[]} generated scenario arguments
92
181
  */
93
- function getGeneratedTestArgs (filename, stepsFile) {
94
- return ['--require', stepsFile, '--format', 'progress', filename]
182
+ function getGeneratedTestArgs (filename, stepsFile, cwd) {
183
+ return [...getIsolationArgs(cwd), '--require', stepsFile, '--format', 'json', filename]
95
184
  }
96
185
 
97
186
  /**
@@ -101,13 +190,575 @@ function getGeneratedTestArgs (filename, stepsFile) {
101
190
  * @returns {number|null} final scenario count when present
102
191
  */
103
192
  function getObservedTestCount (output) {
193
+ const jsonCount = getJsonScenarioCount(output)
194
+ if (jsonCount !== null) return jsonCount
195
+
104
196
  let count = null
105
197
  for (const match of output.matchAll(/\b(\d+)\s+scenarios?\b/gi)) count = Number(match[1])
106
198
  return count
107
199
  }
108
200
 
201
+ /**
202
+ * Expands selected Cucumber profiles without loading customer JavaScript.
203
+ *
204
+ * Profile feature paths, filters, retries, publishing, and formatters are intentionally omitted. The validator
205
+ * supplies one exact feature and a machine-readable formatter.
206
+ *
207
+ * @param {object} input profile inputs
208
+ * @param {string[]} input.configFiles bounded Cucumber config files
209
+ * @param {string} input.projectRoot project root
210
+ * @param {string[]} input.projectFiles bounded project files
211
+ * @param {string[]} input.runnerArgs retained command-line arguments
212
+ * @returns {{error?: string, runnerArgs: string[]}} expanded profile arguments
213
+ */
214
+ function expandProfiles ({ configFiles, projectFiles, projectRoot, runnerArgs }) {
215
+ const selectedProfiles = getOptionValues(runnerArgs, new Set(['-p', '--profile']))
216
+ const explicitConfig = getOptionValues(runnerArgs, new Set(['--config']))[0]
217
+ const explicitConfigFile = explicitConfig && getPhysicalPath(path.resolve(projectRoot, explicitConfig))
218
+ const physicalProjectRoot = getPhysicalDirectory(projectRoot)
219
+ const configFile = explicitConfig
220
+ ? configFiles.find(filename => path.resolve(filename) === explicitConfigFile)
221
+ : configFiles.find(filename =>
222
+ path.dirname(filename) === physicalProjectRoot && CONFIG_PATTERN.test(path.basename(filename)))
223
+ const directArgs = omitOptions(runnerArgs, new Set(['-p', '--config', '--profile']))
224
+
225
+ if (explicitConfig && !configFile) {
226
+ return {
227
+ error: `explicit Cucumber config ${JSON.stringify(explicitConfig)} is not an approval-bound regular file`,
228
+ runnerArgs: [],
229
+ }
230
+ }
231
+ if (!configFile) return expandSupportPaths(directArgs, projectFiles, projectRoot)
232
+
233
+ let definitions
234
+ try {
235
+ definitions = readProfileDefinitions(configFile)
236
+ } catch (error) {
237
+ return { error: `Cucumber configuration could not be expanded statically: ${error.message}`, runnerArgs: [] }
238
+ }
239
+
240
+ const profiles = selectedProfiles.length > 0 ? selectedProfiles : ['default']
241
+ const expanded = []
242
+ for (const profile of profiles) {
243
+ const definition = definitions.get(profile)
244
+ if (definition === undefined) {
245
+ return {
246
+ error: `Cucumber profile ${JSON.stringify(profile)} is not a static literal in ${path.basename(configFile)}`,
247
+ runnerArgs: [],
248
+ }
249
+ }
250
+ const parsed = getProfileArgs(definition)
251
+ if (parsed.error) {
252
+ return {
253
+ error: `Cucumber profile ${JSON.stringify(profile)} ${parsed.error}`,
254
+ runnerArgs: [],
255
+ }
256
+ }
257
+ expanded.push(...parsed.args)
258
+ }
259
+ return expandSupportPaths([...expanded, ...directArgs], projectFiles, projectRoot)
260
+ }
261
+
262
+ function getIsolationArgs (cwd) {
263
+ return ['--config', path.relative(cwd, ISOLATION_CONFIG_PATH)]
264
+ }
265
+
266
+ function readProfileDefinitions (filename) {
267
+ const stat = fs.lstatSync(filename)
268
+ if (!stat.isFile() || stat.isSymbolicLink()) {
269
+ throw new Error('configuration must be a regular non-symbolic-link file')
270
+ }
271
+ if (stat.size > MAX_CONFIG_BYTES) {
272
+ throw new Error(`configuration exceeds the ${MAX_CONFIG_BYTES}-byte limit`)
273
+ }
274
+ const source = fs.readFileSync(filename, 'utf8')
275
+ const extension = path.extname(filename)
276
+ if (extension === '.json') return getDataProfileDefinitions(JSON.parse(source))
277
+ if (extension === '.yaml' || extension === '.yml') {
278
+ return getYamlStringProfileDefinitions(source)
279
+ }
280
+ return getJavascriptProfileDefinitions(source)
281
+ }
282
+
283
+ function getJavascriptProfileDefinitions (source) {
284
+ const syntax = maskJavascriptCommentsAndStrings(source)
285
+ const exports = [...syntax.matchAll(/^\s*(?:module\s*\.\s*exports\s*=|export\s+default)\s*\{/gm)]
286
+ if (exports.length === 0) return new Map()
287
+ if (exports.length !== 1) throw new Error('configuration must export one literal profile object')
288
+
289
+ const objectStart = exports[0].index + exports[0][0].lastIndexOf('{')
290
+ const objectEnd = findClosingBrace(syntax, objectStart)
291
+ if (objectEnd === -1) throw new Error('configuration contains an unterminated profile object')
292
+ const definitions = new Map()
293
+ let index = objectStart + 1
294
+ while (index < objectEnd) {
295
+ index = skipWhitespace(syntax, index)
296
+ if (syntax[index] === ',') {
297
+ index++
298
+ continue
299
+ }
300
+ if (index >= objectEnd) break
301
+
302
+ let name
303
+ const quotedName = readQuotedString(source, index)
304
+ if (quotedName) {
305
+ name = quotedName.value
306
+ index = quotedName.end
307
+ } else {
308
+ const identifier = /^[A-Za-z_$][\w$]*/.exec(syntax.slice(index))
309
+ if (!identifier) throw new Error('profile names must be static literal properties')
310
+ name = identifier[0]
311
+ index += identifier[0].length
312
+ }
313
+
314
+ index = skipWhitespace(syntax, index)
315
+ if (syntax[index] !== ':') throw new Error(`profile ${JSON.stringify(name)} must have a literal value`)
316
+ index = skipWhitespace(syntax, index + 1)
317
+ const definition = readJavascriptLiteral(source, syntax, index)
318
+ if (!definition || (typeof definition.value !== 'string' &&
319
+ (!definition.value || typeof definition.value !== 'object' || Array.isArray(definition.value)))) {
320
+ throw new Error(`profile ${JSON.stringify(name)} must be a literal string or object`)
321
+ }
322
+ definitions.set(name, definition.value)
323
+ index = skipWhitespace(syntax, definition.end)
324
+ if (index < objectEnd && syntax[index] !== ',') {
325
+ throw new Error(`profile ${JSON.stringify(name)} must be followed by a comma`)
326
+ }
327
+ }
328
+ if (!/^[\s;]*$/.test(syntax.slice(objectEnd + 1))) {
329
+ throw new Error('configuration contains code after the exported profile object')
330
+ }
331
+ return definitions
332
+ }
333
+
334
+ function maskJavascriptCommentsAndStrings (source) {
335
+ const syntax = [...source]
336
+ let quote
337
+ let lineComment = false
338
+ let blockComment = false
339
+ for (let index = 0; index < source.length; index++) {
340
+ const character = source[index]
341
+ const next = source[index + 1]
342
+ if (lineComment) {
343
+ if (character === '\n') lineComment = false
344
+ else syntax[index] = ' '
345
+ continue
346
+ }
347
+ if (blockComment) {
348
+ syntax[index] = ' '
349
+ if (character === '*' && next === '/') {
350
+ syntax[++index] = ' '
351
+ blockComment = false
352
+ }
353
+ continue
354
+ }
355
+ if (quote) {
356
+ if (character === '\\') {
357
+ syntax[index] = ' '
358
+ if (index + 1 < source.length) syntax[++index] = ' '
359
+ } else if (character === quote) {
360
+ quote = undefined
361
+ } else {
362
+ syntax[index] = ' '
363
+ }
364
+ continue
365
+ }
366
+ if (character === '/' && next === '/') {
367
+ syntax[index] = syntax[++index] = ' '
368
+ lineComment = true
369
+ } else if (character === '/' && next === '*') {
370
+ syntax[index] = syntax[++index] = ' '
371
+ blockComment = true
372
+ } else if (character === '"' || character === "'" || character === '`') {
373
+ quote = character
374
+ }
375
+ }
376
+ return syntax.join('')
377
+ }
378
+
379
+ function findClosingBrace (source, start) {
380
+ let depth = 0
381
+ for (let index = start; index < source.length; index++) {
382
+ if (source[index] === '{') depth++
383
+ else if (source[index] === '}' && --depth === 0) return index
384
+ }
385
+ return -1
386
+ }
387
+
388
+ function skipWhitespace (source, start) {
389
+ let index = start
390
+ while (/\s/.test(source[index] || '')) index++
391
+ return index
392
+ }
393
+
394
+ /**
395
+ * Reads a bounded JSON-shaped JavaScript literal without evaluating customer code.
396
+ *
397
+ * @param {string} source original JavaScript source
398
+ * @param {string} syntax source with comments and string contents masked
399
+ * @param {number} start literal start offset
400
+ * @param {number} [depth] current literal nesting depth
401
+ * @returns {{end: number, value: unknown}|undefined} parsed literal and ending offset
402
+ */
403
+ function readJavascriptLiteral (source, syntax, start, depth = 0) {
404
+ if (depth > 16) return
405
+ let index = skipWhitespace(syntax, start)
406
+ const quoted = readQuotedString(source, index)
407
+ if (quoted) return quoted
408
+
409
+ const primitive = /^(true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)(?![\w$.])/.exec(
410
+ syntax.slice(index)
411
+ )
412
+ if (primitive) {
413
+ const values = { false: false, null: null, true: true }
414
+ const value = Object.hasOwn(values, primitive[1]) ? values[primitive[1]] : Number(primitive[1])
415
+ if (typeof value === 'number' && (!Number.isFinite(value) || Object.is(value, -0))) return
416
+ return {
417
+ end: index + primitive[0].length,
418
+ value,
419
+ }
420
+ }
421
+
422
+ const open = syntax[index]
423
+ if (open !== '[' && open !== '{') return
424
+ const close = open === '[' ? ']' : '}'
425
+ const value = open === '[' ? [] : Object.create(null)
426
+ index++
427
+ while (index < syntax.length) {
428
+ index = skipWhitespace(syntax, index)
429
+ if (syntax[index] === close) return { end: index + 1, value }
430
+
431
+ let item
432
+ if (open === '[') {
433
+ item = readJavascriptLiteral(source, syntax, index, depth + 1)
434
+ if (!item) return
435
+ value.push(item.value)
436
+ } else {
437
+ const property = readJavascriptPropertyName(source, syntax, index)
438
+ if (!property || property.value === '__proto__' || Object.hasOwn(value, property.value)) return
439
+ index = skipWhitespace(syntax, property.end)
440
+ if (syntax[index] !== ':') return
441
+ item = readJavascriptLiteral(source, syntax, index + 1, depth + 1)
442
+ if (!item) return
443
+ value[property.value] = item.value
444
+ }
445
+ index = item.end
446
+
447
+ index = skipWhitespace(syntax, index)
448
+ if (syntax[index] === close) return { end: index + 1, value }
449
+ if (syntax[index] !== ',') return
450
+ index++
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Reads one static quoted or identifier JavaScript object property name.
456
+ *
457
+ * @param {string} source original JavaScript source
458
+ * @param {string} syntax source with comments and string contents masked
459
+ * @param {number} start property start offset
460
+ * @returns {{end: number, value: string}|undefined} property name and ending offset
461
+ */
462
+ function readJavascriptPropertyName (source, syntax, start) {
463
+ const index = skipWhitespace(syntax, start)
464
+ const quoted = readQuotedString(source, index)
465
+ if (quoted) return quoted
466
+ const identifier = /^[A-Za-z_$][\w$]*/.exec(syntax.slice(index))
467
+ if (identifier) return { end: index + identifier[0].length, value: identifier[0] }
468
+ }
469
+
470
+ function getDataProfileDefinitions (value) {
471
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
472
+ throw new Error('configuration must contain a profile object')
473
+ }
474
+ return new Map(Object.entries(value))
475
+ }
476
+
477
+ function getYamlStringProfileDefinitions (source) {
478
+ const definitions = new Map()
479
+ for (const line of source.split(/\r?\n/)) {
480
+ if (!line.trim() || /^\s*#/.test(line)) continue
481
+ const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
482
+ const definition = match && !/^\s/.test(line) ? getYamlStringProfileDefinition(match[2]) : undefined
483
+ if (definition === undefined) {
484
+ throw new Error('YAML profiles must be literal one-line command strings')
485
+ }
486
+ definitions.set(match[1], definition)
487
+ }
488
+ return definitions
489
+ }
490
+
491
+ function getYamlStringProfileDefinition (source) {
492
+ const value = source.trimStart()
493
+ const quote = value[0]
494
+ if (quote !== '"' && quote !== "'") {
495
+ const comment = /\s+#/.exec(value)
496
+ const definition = value.slice(0, comment?.index ?? value.length).trimEnd()
497
+ return definition || undefined
498
+ }
499
+
500
+ let definition = ''
501
+ for (let index = 1; index < value.length; index++) {
502
+ const character = value[index]
503
+ if (quote === '"' && character === '\\') {
504
+ return
505
+ }
506
+ if (quote === "'" && character === quote && value[index + 1] === quote) {
507
+ definition += quote
508
+ index++
509
+ continue
510
+ }
511
+ if (character !== quote) {
512
+ definition += character
513
+ continue
514
+ }
515
+ const remainder = value.slice(index + 1)
516
+ if (!remainder.trim() || /^\s+#/.test(remainder)) return definition
517
+ return
518
+ }
519
+ }
520
+
521
+ function readQuotedString (source, start) {
522
+ const quote = source[start]
523
+ if (quote !== '"' && quote !== "'") return
524
+ let value = ''
525
+ let index = start + 1
526
+ while (index < source.length) {
527
+ const character = source[index]
528
+ if (character === quote) return { end: index + 1, value }
529
+ if (character === '\r' || character === '\n') return
530
+ if (character !== '\\') {
531
+ value += character
532
+ index++
533
+ continue
534
+ }
535
+ const escaped = readJavascriptStringEscape(source, index + 1)
536
+ if (!escaped) return
537
+ value += escaped.value
538
+ index = escaped.end
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Decodes one bounded JavaScript string escape without evaluating source.
544
+ *
545
+ * @param {string} source original JavaScript source
546
+ * @param {number} start first character after the backslash
547
+ * @returns {{end: number, value: string}|undefined} decoded character and ending offset
548
+ */
549
+ function readJavascriptStringEscape (source, start) {
550
+ const escaped = source[start]
551
+ if (Object.hasOwn(JAVASCRIPT_STRING_ESCAPES, escaped)) {
552
+ return { end: start + 1, value: JAVASCRIPT_STRING_ESCAPES[escaped] }
553
+ }
554
+ if (escaped === '\n') return { end: start + 1, value: '' }
555
+ if (escaped === '\r') {
556
+ return { end: source[start + 1] === '\n' ? start + 2 : start + 1, value: '' }
557
+ }
558
+
559
+ if (escaped === 'x') {
560
+ const hexadecimal = source.slice(start + 1, start + 3)
561
+ if (/^[\da-fA-F]{2}$/.test(hexadecimal)) {
562
+ return { end: start + 3, value: String.fromCharCode(Number.parseInt(hexadecimal, 16)) }
563
+ }
564
+ return
565
+ }
566
+ if (escaped !== 'u') return
567
+
568
+ if (source[start + 1] === '{') {
569
+ const match = /^\{([\da-fA-F]{1,6})\}/.exec(source.slice(start + 1))
570
+ if (!match) return
571
+ const codePoint = Number.parseInt(match[1], 16)
572
+ if (codePoint > 1_114_111) return
573
+ return { end: start + 1 + match[0].length, value: String.fromCodePoint(codePoint) }
574
+ }
575
+
576
+ const hexadecimal = source.slice(start + 1, start + 5)
577
+ if (/^[\da-fA-F]{4}$/.test(hexadecimal)) {
578
+ return { end: start + 5, value: String.fromCharCode(Number.parseInt(hexadecimal, 16)) }
579
+ }
580
+ }
581
+
582
+ function getProfileArgs (definition) {
583
+ if (typeof definition === 'string') return getStringProfileArgs(definition)
584
+ if (!definition || typeof definition !== 'object' || Array.isArray(definition)) {
585
+ return { error: 'must be a literal string or object', args: [] }
586
+ }
587
+
588
+ const args = []
589
+ for (const [key, value] of Object.entries(definition)) {
590
+ if (OMITTED_PROFILE_KEYS.has(key)) continue
591
+ const flag = PROFILE_FLAG_OPTIONS.get(key)
592
+ if (flag) {
593
+ if (typeof value !== 'boolean') return { error: `contains non-boolean ${key}`, args: [] }
594
+ if (key === 'strict') args.push(value ? '--strict' : '--no-strict')
595
+ else if (value) args.push(flag)
596
+ continue
597
+ }
598
+ const option = PROFILE_VALUE_OPTIONS.get(key)
599
+ if (!option) return { error: `contains unsupported option ${key}`, args: [] }
600
+
601
+ if (key === 'worldParameters') {
602
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
603
+ return { error: 'contains non-object worldParameters', args: [] }
604
+ }
605
+ args.push(option, JSON.stringify(value))
606
+ continue
607
+ }
608
+
609
+ const values = Array.isArray(value) ? value : [value]
610
+ for (const item of values) {
611
+ if (item !== null && typeof item === 'object') {
612
+ return { error: `contains unsupported object value for ${key}`, args: [] }
613
+ }
614
+ args.push(option, String(item))
615
+ }
616
+ }
617
+ return { args }
618
+ }
619
+
620
+ function getStringProfileArgs (definition) {
621
+ const tokens = tokenizeProfile(definition)
622
+ if (!tokens) return { error: 'contains shell control syntax or unbalanced quoting', args: [] }
623
+
624
+ const args = []
625
+ for (let index = 0; index < tokens.length; index++) {
626
+ const token = tokens[index]
627
+ if (!token.startsWith('-')) continue
628
+ const option = token.split('=', 1)[0]
629
+ if (RETAINED_PROFILE_FLAGS.has(option)) {
630
+ args.push(token)
631
+ continue
632
+ }
633
+ if (RETAINED_PROFILE_VALUES.has(option)) {
634
+ const value = token.includes('=') ? token.slice(token.indexOf('=') + 1) : tokens[++index]
635
+ if (!value || value.startsWith('-')) return { error: `has a missing value for ${option}`, args: [] }
636
+ args.push(option, value)
637
+ continue
638
+ }
639
+ if (OMITTED_PROFILE_OPTIONS.has(option)) {
640
+ if (OMITTED_PROFILE_OPTIONS.get(option) && !token.includes('=')) index++
641
+ continue
642
+ }
643
+ return { error: `contains unsupported option ${option}`, args: [] }
644
+ }
645
+ return { args }
646
+ }
647
+
648
+ function tokenizeProfile (source) {
649
+ if (/[\0\r\n;&|`]|\$\(|\$\{/.test(source)) return
650
+ const tokens = []
651
+ const pattern = /"([^"\r\n]*)"|'([^'\r\n]*)'|([^\s"']+)/g
652
+ let consumed = 0
653
+ for (const match of source.matchAll(pattern)) {
654
+ if (source.slice(consumed, match.index).trim()) return
655
+ tokens.push(match[1] ?? match[2] ?? match[3])
656
+ consumed = match.index + match[0].length
657
+ }
658
+ if (source.slice(consumed).trim()) return
659
+ return tokens
660
+ }
661
+
662
+ function getOptionValues (args, options) {
663
+ const values = []
664
+ for (let index = 0; index < args.length; index++) {
665
+ const option = args[index].split('=', 1)[0]
666
+ if (!options.has(option)) continue
667
+ values.push(args[index].includes('=') ? args[index].slice(args[index].indexOf('=') + 1) : args[++index])
668
+ }
669
+ return values.filter(Boolean)
670
+ }
671
+
672
+ function getPhysicalPath (filename) {
673
+ try {
674
+ return fs.realpathSync(filename)
675
+ } catch {}
676
+ }
677
+
678
+ function omitOptions (args, options) {
679
+ const retained = []
680
+ for (let index = 0; index < args.length; index++) {
681
+ const option = args[index].split('=', 1)[0]
682
+ if (!options.has(option)) {
683
+ retained.push(args[index])
684
+ continue
685
+ }
686
+ if (!args[index].includes('=')) index++
687
+ }
688
+ return retained
689
+ }
690
+
691
+ function expandSupportPaths (args, projectFiles, projectRoot) {
692
+ const expanded = []
693
+ const pathOptions = new Set(['-i', '-r', '--import', '--require'])
694
+ const aliases = { '-i': '--import', '-r': '--require' }
695
+ for (let index = 0; index < args.length; index++) {
696
+ const option = args[index].split('=', 1)[0]
697
+ if (!pathOptions.has(option)) {
698
+ expanded.push(args[index])
699
+ continue
700
+ }
701
+
702
+ const value = args[index].includes('=') ? args[index].slice(args[index].indexOf('=') + 1) : args[++index]
703
+ const matches = getSupportPathMatches(value, projectFiles, projectRoot)
704
+ if (matches.length === 0) {
705
+ return {
706
+ error: `Cucumber support path ${JSON.stringify(value)} does not match a repository-contained file`,
707
+ runnerArgs: [],
708
+ }
709
+ }
710
+ for (const filename of matches) expanded.push(aliases[option] || option, filename)
711
+ }
712
+ return { runnerArgs: expanded }
713
+ }
714
+
715
+ function getSupportPathMatches (value, projectFiles, projectRoot) {
716
+ const normalized = String(value || '').replaceAll('\\', '/')
717
+ const candidate = path.resolve(projectRoot, value)
718
+ try {
719
+ if (fs.statSync(candidate).isFile()) return [candidate]
720
+ if (fs.statSync(candidate).isDirectory()) {
721
+ return projectFiles.filter(filename => {
722
+ const relative = path.relative(candidate, filename)
723
+ return relative && !relative.startsWith('..') && !path.isAbsolute(relative) &&
724
+ /\.[cm]?[jt]s$/.test(filename)
725
+ }).sort()
726
+ }
727
+ } catch {}
728
+ if (!/[*?[\]{}]/.test(normalized)) return []
729
+ return projectFiles.filter(filename => {
730
+ return matchesLiteralGlob(path.relative(projectRoot, filename), normalized)
731
+ }).sort()
732
+ }
733
+
734
+ function getJsonScenarioCount (output) {
735
+ const source = output.trim()
736
+ const end = source.lastIndexOf(']')
737
+ if (end === -1) return null
738
+ let attempts = 0
739
+ for (let start = source.indexOf('['); start !== -1 && start < end && attempts < 20;
740
+ start = source.indexOf('[', start + 1), attempts++) {
741
+ try {
742
+ const features = JSON.parse(source.slice(start, end + 1))
743
+ if (!Array.isArray(features)) continue
744
+ return features.reduce((count, feature) => {
745
+ return count + (feature.elements || []).filter(element => element.type === 'scenario').length
746
+ }, 0)
747
+ } catch {}
748
+ }
749
+ return null
750
+ }
751
+
752
+ function getPhysicalDirectory (directory) {
753
+ try {
754
+ const physical = fs.realpathSync(directory)
755
+ if (fs.statSync(physical).isDirectory()) return physical
756
+ } catch {}
757
+ }
758
+
109
759
  module.exports = {
110
760
  CONFIG_PATTERN,
761
+ expandProfiles,
111
762
  getFocusedTestArgs,
112
763
  getGeneratedStepsContent,
113
764
  getGeneratedStepsPath,
@@ -116,4 +767,5 @@ module.exports = {
116
767
  getObservedTestCount,
117
768
  getScenarioCount,
118
769
  isTestFile,
770
+ supportsConfigIsolation,
119
771
  }