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,16 +1,23 @@
1
1
  'use strict'
2
2
 
3
3
  // Capture real timers at module load time, before any test can install fake timers.
4
+ const realClearTimeout = clearTimeout
4
5
  const realSetTimeout = setTimeout
5
6
 
6
7
  const { readFileSync } = require('node:fs')
7
8
  const { builtinModules, createRequire } = require('node:module')
9
+ const { performance } = require('node:perf_hooks')
8
10
  const path = require('path')
9
11
  const satisfies = require('../../../vendor/dist/semifies')
10
12
  const { DD_MAJOR } = require('../../../version')
11
13
  const shimmer = require('../../datadog-shimmer')
12
14
  const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
13
15
  const log = require('../../dd-trace/src/log')
16
+ const {
17
+ EMPTY_EFD_RETRY_POLICY,
18
+ getEfdRetryCountForDuration,
19
+ hasEfdRetries,
20
+ } = require('../../dd-trace/src/ci-visibility/efd-retry-policy')
14
21
  const {
15
22
  getCoveredFilesFromCoverage,
16
23
  getExecutableFilesFromCoverage,
@@ -27,10 +34,11 @@ const {
27
34
  isModifiedTest,
28
35
  DYNAMIC_NAME_RE,
29
36
  collectDynamicNamesFromTraces,
37
+ recordTestManagementExecution,
30
38
  recordAttemptToFixExecution,
31
39
  logAttemptToFixTestExecution,
32
40
  logTestOptimizationSummary,
33
- getEfdRetryCount,
41
+ TEST_IMPACT_ANALYSIS_ALL_TESTS_SKIPPED_MESSAGE,
34
42
  getTestCoverageLinesPercentage,
35
43
  applySkippedCoverageToCoverage,
36
44
  getTestOptimizationRequestResults,
@@ -96,7 +104,7 @@ const DD_JEST_HANDLE_TEST_EVENT_WRAPPED = Symbol('dd-trace:jest:handle-test-even
96
104
  const DD_JEST_HANDLE_TEST_EVENT_DATADOG = Symbol('dd-trace:jest:handle-test-event-datadog')
97
105
  const DD_JEST_CONCURRENT_TEST_ORIGINAL = Symbol('dd-trace:jest:concurrent-test-original')
98
106
  const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
99
- const jestSessionState = globalThis[JEST_SESSION_STATE] || (globalThis[JEST_SESSION_STATE] = {})
107
+ const jestSessionState = (globalThis[JEST_SESSION_STATE] ||= {})
100
108
 
101
109
  // https://github.com/jestjs/jest/blob/41f842a46bb2691f828c3a5f27fc1d6290495b82/packages/jest-circus/src/types.ts#L9C8-L9C54
102
110
  const RETRY_TIMES = Symbol.for('RETRY_TIMES')
@@ -115,8 +123,6 @@ let numSkippedSuites = 0
115
123
  let hasUnskippableSuites = false
116
124
  let hasForcedToRunSuites = false
117
125
  let isEarlyFlakeDetectionEnabled = false
118
- let earlyFlakeDetectionNumRetries = 0
119
- let earlyFlakeDetectionSlowTestRetries = {}
120
126
  let earlyFlakeDetectionFaultyThreshold = 30
121
127
  let isEarlyFlakeDetectionFaulty = false
122
128
  let hasFilteredSkippableSuites = false
@@ -141,8 +147,10 @@ const originalTestFns = new WeakMap()
141
147
  const originalHookFns = new WeakMap()
142
148
  const concurrentHookContextQueues = new WeakMap()
143
149
  const concurrentHookFns = new WeakMap()
150
+ const efdRetryMetadataByTest = new WeakMap()
151
+ const removedRetryTests = new WeakSet()
144
152
  const retriedTestsToNumAttempts = new Map()
145
- const newTestsTestStatuses = new Map()
153
+ const efdTestStatuses = new Map()
146
154
  const attemptToFixRetriedTestsStatuses = new Map()
147
155
  const wrappedWorkerChannels = new WeakMap()
148
156
  // New tests whose names contain likely dynamic data (timestamps, UUIDs, etc.)
@@ -153,10 +161,12 @@ const testSuiteMockedFiles = new Map()
153
161
  const testsToBeRetried = new Set()
154
162
  // Per-test: how many EFD retries were determined after the first execution.
155
163
  const efdDeterminedRetries = new Map()
164
+ // Per-test: total executions to report, including retries Jest ran before the test they belong to.
165
+ const efdExpectedExecutions = new Map()
156
166
  // Tests whose first run exceeded the 5-min threshold — tagged "slow".
157
167
  const efdSlowAbortedTests = new Set()
158
- // Tests added as EFD new-test candidates (not ATF, not impacted).
159
- const efdNewTestCandidates = new Set()
168
+ // Tests whose first execution determines the duration-based EFD retry count.
169
+ const efdCandidates = new Set()
160
170
  // Tests that are genuinely new (not in known tests list).
161
171
  const newTests = new Set()
162
172
  const testSuiteJestObjects = new Map()
@@ -173,13 +183,39 @@ const handledJestEvents = new WeakSet()
173
183
  * @typedef {object} ConcurrentTestOptions
174
184
  * @property {(...args: unknown[]) => unknown} [concurrentTest]
175
185
  * @property {unknown} [concurrentTestThisArg]
186
+ * @property {EfdRetryGate[]} [efdRetryGates]
176
187
  * @property {boolean} [isAttemptToFixRetry]
177
188
  * @property {boolean} [isEfdRetry]
189
+ * @property {number} [efdRetryIndex]
178
190
  * @property {boolean} [isModified]
179
- * @property {(...args: unknown[]) => unknown} [serialTest]
180
- * @property {unknown} [serialTestThisArg]
181
191
  * @property {(...args: unknown[]) => unknown} [sourceTestFn]
182
192
  * @property {string} [testParameters]
193
+ * @property {number} [timeout]
194
+ */
195
+
196
+ /**
197
+ * @typedef {object} JestRetryOptions
198
+ * @property {object} [concurrentTestState]
199
+ * @property {EfdRetryGate[]} [efdRetryGates]
200
+ * @property {object} jestEvent
201
+ * @property {object} state
202
+ * @property {boolean} [isModified]
203
+ * @property {number} retryCount
204
+ * @property {string} retryType
205
+ * @property {string} testFullName
206
+ */
207
+
208
+ /**
209
+ * @typedef {object} EfdRetryGate
210
+ * @property {Promise<boolean>} promise
211
+ * @property {(shouldRun: boolean) => void} resolve
212
+ */
213
+
214
+ /**
215
+ * @typedef {object} DetachedEfdRetry
216
+ * @property {object} ctx
217
+ * @property {() => void} resolve
218
+ * @property {() => void} start
183
219
  */
184
220
 
185
221
  const ATR_RETRY_SUPPRESSION_FLAG = '_ddDisableAtrRetry'
@@ -278,6 +314,10 @@ function getTestEnvironmentOptions (config) {
278
314
 
279
315
  const MAX_IGNORED_TEST_NAMES = 10
280
316
 
317
+ /**
318
+ * @typedef {Parameters<typeof logTestOptimizationSummary>[0]} TestOptimizationSummary
319
+ */
320
+
281
321
  function getTestStats (testStatuses) {
282
322
  return testStatuses.reduce((acc, testStatus) => {
283
323
  acc[testStatus]++
@@ -288,22 +328,20 @@ function getTestStats (testStatuses) {
288
328
  /**
289
329
  * Formats the ignored-failure section for the Test Optimization summary.
290
330
  *
291
- * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
331
+ * @param {{
332
+ * efdNames: string[],
333
+ * quarantineNames: string[],
334
+ * totalCount: number,
335
+ * efdFailureCount: number
336
+ * } | undefined} ignoredFailures
292
337
  * @returns {string}
293
338
  */
294
339
  function formatIgnoredFailuresSummary (ignoredFailures) {
295
- if (!ignoredFailures) return ''
296
-
297
- const items = []
340
+ if (!ignoredFailures?.efdFailureCount) return ''
298
341
 
299
- for (const n of ignoredFailures.efdNames) {
300
- items.push({ text: n, suffix: 'Early Flake Detection' })
301
- }
302
- for (const n of ignoredFailures.quarantineNames) {
303
- items.push({ text: n, suffix: 'Quarantine' })
304
- }
342
+ const items = ignoredFailures.efdNames.map(text => ({ text, suffix: 'Early Flake Detection' }))
305
343
 
306
- if (items.length === 0 || ignoredFailures.totalCount <= 0) return ''
344
+ if (items.length === 0) return ''
307
345
 
308
346
  const shown = items.slice(0, MAX_IGNORED_TEST_NAMES)
309
347
  const more = items.length - shown.length
@@ -312,18 +350,28 @@ function formatIgnoredFailuresSummary (ignoredFailures) {
312
350
  .map(({ text, suffix }) => ` • ${text}${suffix ? ` (${suffix})` : ''}`)
313
351
  .join('\n') + moreSuffix
314
352
 
315
- return `${ignoredFailures.totalCount} test failure(s) were ignored. Exit code set to 0.\n\n${formattedItems}`
353
+ return `${ignoredFailures.efdFailureCount} test failure(s) were ignored. Exit code set to 0.\n\n${formattedItems}`
316
354
  }
317
355
 
318
356
  /**
319
357
  * Logs a single "Datadog Test Optimization" summary at session end.
320
358
  *
321
- * @param {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} ignoredFailures
359
+ * @param {{
360
+ * efdNames: string[],
361
+ * quarantineNames: string[],
362
+ * totalCount: number,
363
+ * efdFailureCount: number
364
+ * } | undefined} ignoredFailures
365
+ * @param {NonNullable<TestOptimizationSummary['attemptToFixExecutions']>} attemptToFixExecutions
366
+ * @param {boolean} allTestsSkipped
322
367
  */
323
- function logSessionSummary (ignoredFailures, attemptToFixExecutions) {
368
+ function logSessionSummary (ignoredFailures, attemptToFixExecutions, allTestsSkipped) {
324
369
  logTestOptimizationSummary({
325
370
  attemptToFixExecutions,
326
- extraSections: [formatIgnoredFailuresSummary(ignoredFailures)],
371
+ extraSections: [
372
+ formatIgnoredFailuresSummary(ignoredFailures),
373
+ allTestsSkipped ? TEST_IMPACT_ANALYSIS_ALL_TESTS_SKIPPED_MESSAGE : '',
374
+ ],
327
375
  newTestsWithDynamicNames,
328
376
  })
329
377
  loggedAttemptToFixTests.clear()
@@ -349,11 +397,11 @@ function getAttemptToFixExecutionsFromJestResults (result) {
349
397
 
350
398
  for (const { fullName, status } of testResults) {
351
399
  const testName = removeSeedSuffixFromTestName(fullName)
352
- const testStatus = getTestStatusFromJestResult(status)
353
- if (!testStatus) continue
354
-
355
400
  const testManagementTest = testManagementTestsForSuite[testName]?.properties
356
401
  if (!testManagementTest?.attempt_to_fix) continue
402
+ const testStatus = getTestStatusFromJestResult(status) ||
403
+ (status === 'pending' || status === 'todo' ? 'skip' : undefined)
404
+ if (!testStatus) continue
357
405
 
358
406
  recordAttemptToFixExecution(executions, {
359
407
  testSuite,
@@ -368,6 +416,51 @@ function getAttemptToFixExecutionsFromJestResults (result) {
368
416
  return executions
369
417
  }
370
418
 
419
+ /**
420
+ * Records test-management results reported by Jest after all retries have finished.
421
+ *
422
+ * @param {{
423
+ * globalConfig?: { rootDir?: string },
424
+ * results: {
425
+ * testResults: Array<{
426
+ * testResults: Array<{ fullName: string, status: string }>,
427
+ * testFilePath: string
428
+ * }>
429
+ * }
430
+ * }} result
431
+ * @param {string[]} quarantineFailureNames
432
+ */
433
+ function recordTestManagementExecutionsFromJestResults (result, quarantineFailureNames) {
434
+ const rootDir = result.globalConfig?.rootDir || process.cwd()
435
+ const failedQuarantinedTests = new Set(quarantineFailureNames)
436
+
437
+ for (const { testResults, testFilePath } of result.results.testResults) {
438
+ const testSuite = getTestSuitePath(testFilePath, rootDir)
439
+ const testManagementTestsForSuite = testManagementTests
440
+ ?.jest
441
+ ?.suites
442
+ ?.[testSuite]
443
+ ?.tests
444
+ if (!testManagementTestsForSuite) continue
445
+
446
+ for (const { fullName, status } of testResults) {
447
+ const testName = removeSeedSuffixFromTestName(fullName)
448
+ const testManagementTest = testManagementTestsForSuite[testName]?.properties
449
+ if (!testManagementTest) continue
450
+
451
+ const name = `${testSuite} › ${testName}`
452
+ recordTestManagementExecution({
453
+ testSuite,
454
+ testName,
455
+ status: failedQuarantinedTests.has(name) ? 'fail' : getTestStatusFromJestResult(status),
456
+ isAttemptToFix: testManagementTest.attempt_to_fix,
457
+ isDisabled: testManagementTest.disabled,
458
+ isQuarantined: testManagementTest.quarantined,
459
+ })
460
+ }
461
+ }
462
+ }
463
+
371
464
  function wrapConsoleErrorForJestReferenceErrors () {
372
465
  if (isConsoleErrorWrapped) return
373
466
 
@@ -465,8 +558,52 @@ function getWrappedCustomHandleTestEvent (handleTestEvent, datadogHandleTestEven
465
558
  return wrappedHandleTestEvent
466
559
  }
467
560
 
561
+ /**
562
+ * Mirrors the test selection checks Jest Circus performs after publishing `test_start`.
563
+ *
564
+ * @param {object} test
565
+ * @param {boolean} hasFocusedTests
566
+ * @param {RegExp|undefined} testNamePattern
567
+ * @returns {boolean}
568
+ */
569
+ function isJestTestSkipped (test, hasFocusedTests, testNamePattern) {
570
+ if (
571
+ test.mode === 'skip' ||
572
+ (hasFocusedTests && test.mode !== 'only') ||
573
+ (testNamePattern && !testNamePattern.test(getRawJestTestName(test)))
574
+ ) {
575
+ return true
576
+ }
577
+
578
+ let parent = test.parent
579
+ while (parent) {
580
+ if (parent.mode === 'skip') return true
581
+ parent = parent.parent
582
+ }
583
+ return false
584
+ }
585
+
468
586
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
587
+ const hasConcurrentTestsStartEvent = satisfies(jestVersion, '>=30.0.0')
588
+ const hasTestsInChildren = satisfies(jestVersion, '>=26.0.0')
589
+
590
+ /**
591
+ * @param {object} describeBlock
592
+ * @returns {object[]|undefined}
593
+ */
594
+ function getTestEntries (describeBlock) {
595
+ return hasTestsInChildren ? describeBlock?.children : describeBlock?.tests
596
+ }
597
+
469
598
  return class DatadogEnvironment extends BaseEnvironment {
599
+ #activeDetachedEfdRetries
600
+ #detachedEfdRetryConcurrency
601
+ #detachedEfdRetryQueue
602
+ #discardedEfdRetryTests
603
+ #earlyFlakeDetectionRetryPolicy
604
+ #efdRetryGatesByName
605
+ #pendingPre30ConcurrentTests
606
+
470
607
  constructor (config, context) {
471
608
  super(config, context)
472
609
  const rootDir = config.globalConfig ? config.globalConfig.rootDir : config.rootDir
@@ -504,11 +641,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
504
641
  this.concurrentTestContexts = new Map()
505
642
  this.concurrentTestStates = new WeakMap()
506
643
  this.concurrentTestSourceFns = new WeakMap()
644
+ this.testParametersByFunction = new WeakMap()
507
645
  this.wrappedConcurrentTestFunctions = new WeakSet()
508
646
  this.jestEachBind = undefined
647
+ this.#activeDetachedEfdRetries = 0
648
+ this.#detachedEfdRetryConcurrency = 1
649
+ this.#pendingPre30ConcurrentTests = 0
650
+ this.#earlyFlakeDetectionRetryPolicy =
651
+ this.testEnvironmentOptions._ddEarlyFlakeDetectionRetryPolicy ?? EMPTY_EFD_RETRY_POLICY
509
652
 
510
653
  if (this.isKnownTestsEnabled) {
511
- earlyFlakeDetectionSlowTestRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionSlowTestRetries ?? {}
512
654
  try {
513
655
  this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
514
656
 
@@ -579,6 +721,53 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
579
721
  }
580
722
  }
581
723
 
724
+ /**
725
+ * Rebuilds serial `test.each` so every generated row function keeps its parameters.
726
+ *
727
+ * @param {Function|undefined} test
728
+ * @returns {void}
729
+ */
730
+ bindTestEach (test) {
731
+ if (typeof test?.each !== 'function') return
732
+
733
+ const bind = this.getJestEachBind()
734
+ if (typeof bind !== 'function') {
735
+ const environment = this
736
+ shimmer.wrap(test, 'each', each => function (...args) {
737
+ const testParameters = getFormattedJestTestParameters(args)
738
+ const eachBind = each.apply(this, args)
739
+ return function (...args) {
740
+ const [testName] = args
741
+ environment.setNameToParams(testName, testParameters)
742
+ return eachBind.apply(this, args)
743
+ }
744
+ })
745
+ return
746
+ }
747
+
748
+ // Jest creates each row function at runtime, so Orchestrion cannot associate parameters statically.
749
+ const environment = this
750
+ test.each = function wrappedTestEach (...eachArgs) {
751
+ const testParameters = getFormattedJestTestParameters(eachArgs)
752
+ return function (...testArgs) {
753
+ let parameterIndex = 0
754
+ const eachTest = function (testName, testFn, ...callArgs) {
755
+ const parameters = testParameters?.[parameterIndex++]
756
+ if (parameters !== undefined && typeof testFn === 'function') {
757
+ environment.appendNameToParams(testName, parameters)
758
+ environment.testParametersByFunction.set(
759
+ testFn,
760
+ getTestParametersString(environment.nameToParams, testName)
761
+ )
762
+ }
763
+ return test.call(this, testName, testFn, ...callArgs)
764
+ }
765
+ const eachBind = bind(eachTest).apply(this, eachArgs)
766
+ return eachBind.apply(this, testArgs)
767
+ }
768
+ }
769
+ }
770
+
582
771
  /**
583
772
  * Wraps Jest's concurrent test registration methods so eager concurrent bodies
584
773
  * in older Jest versions execute inside their test span context.
@@ -604,10 +793,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
604
793
  const concurrentTest = test?.concurrent
605
794
  if (typeof concurrentTest !== 'function') return
606
795
 
607
- this.wrapConcurrentTestFunction(test, 'concurrent', state, test, test)
608
- this.wrapConcurrentTestFunction(test.concurrent, 'only', state, test.only, test)
609
- this.wrapConcurrentTestFunction(test.concurrent, 'failing', state, test.failing, test)
610
- this.wrapConcurrentTestFunction(test.concurrent.only, 'failing', state, test.only?.failing, test.only)
796
+ this.wrapConcurrentTestFunction(test, 'concurrent', state)
797
+ this.wrapConcurrentTestFunction(test.concurrent, 'only', state)
798
+ this.wrapConcurrentTestFunction(test.concurrent, 'failing', state)
799
+ this.wrapConcurrentTestFunction(test.concurrent.only, 'failing', state)
611
800
  }
612
801
 
613
802
  /**
@@ -616,11 +805,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
616
805
  * @param {object} target
617
806
  * @param {string} methodName
618
807
  * @param {object} state
619
- * @param {Function|undefined} serialTest
620
- * @param {unknown} serialTestThisArg
621
808
  * @returns {void}
622
809
  */
623
- wrapConcurrentTestFunction (target, methodName, state, serialTest, serialTestThisArg) {
810
+ wrapConcurrentTestFunction (target, methodName, state) {
624
811
  let concurrentTest = target?.[methodName]
625
812
  if (typeof concurrentTest !== 'function') return
626
813
  if (this.wrappedConcurrentTestFunctions.has(concurrentTest)) return
@@ -646,9 +833,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
646
833
  const wrappedTestFn = environment.createConcurrentTestFn(testName, testFn, asyncError, state, {
647
834
  concurrentTest,
648
835
  concurrentTestThisArg: this,
649
- serialTest,
650
- serialTestThisArg,
651
836
  sourceTestFn: environment.concurrentTestSourceFns.get(testFn),
837
+ timeout: args[0],
652
838
  })
653
839
  return concurrentTest.call(this, testName, wrappedTestFn, ...args)
654
840
  }
@@ -711,8 +897,6 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
711
897
  concurrentTestThisArg: options?.concurrentTestThisArg,
712
898
  ctx: this.createConcurrentTestContext(testName, testFn, asyncError, state, options),
713
899
  numExecutions: 0,
714
- serialTest: options?.serialTest,
715
- serialTestThisArg: options?.serialTestThisArg,
716
900
  state,
717
901
  testFn,
718
902
  }
@@ -777,6 +961,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
777
961
  frameworkVersion: jestVersion,
778
962
  isNew: isNewTest,
779
963
  isEfdRetry: options?.isEfdRetry === true,
964
+ efdRetryGates: options?.efdRetryGates,
965
+ efdRetryIndex: options?.efdRetryIndex,
780
966
  isAttemptToFix,
781
967
  isAttemptToFixRetry: options?.isAttemptToFixRetry === true,
782
968
  isJestRetry: false,
@@ -785,6 +971,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
785
971
  isModified: options?.isModified ?? this.isTestModified(asyncError, sourceTestFn),
786
972
  hasDynamicName: isNewTest && DYNAMIC_NAME_RE.test(testFullName),
787
973
  testSuiteAbsolutePath: this.testSuiteAbsolutePath,
974
+ testTimeout: options?.timeout || state.testTimeout,
788
975
  }
789
976
  let contexts = this.concurrentTestContexts.get(testFullName)
790
977
  if (contexts) {
@@ -880,6 +1067,183 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
880
1067
  }
881
1068
  }
882
1069
 
1070
+ /**
1071
+ * @param {object} concurrentTestState
1072
+ * @param {number|undefined} startedAt
1073
+ * @returns {void}
1074
+ */
1075
+ completePre30ConcurrentTest (concurrentTestState, startedAt) {
1076
+ if (!concurrentTestState.trackEfdCompletion) return
1077
+
1078
+ concurrentTestState.trackEfdCompletion = false
1079
+ const { ctx } = concurrentTestState
1080
+ if (startedAt !== undefined) {
1081
+ ctx.efdDurationMs = performance.now() - startedAt
1082
+ this.determineEfdRetries(ctx.name, ctx.efdDurationMs)
1083
+ }
1084
+ this.#pendingPre30ConcurrentTests--
1085
+ this.drainDetachedEfdRetries()
1086
+ }
1087
+
1088
+ /**
1089
+ * @param {object} concurrentTestState
1090
+ * @param {unknown} result
1091
+ * @param {number|undefined} startedAt
1092
+ * @returns {unknown}
1093
+ */
1094
+ trackPre30ConcurrentTest (concurrentTestState, result, startedAt) {
1095
+ if (typeof result?.then !== 'function') {
1096
+ this.completePre30ConcurrentTest(concurrentTestState, startedAt)
1097
+ return result
1098
+ }
1099
+
1100
+ return Promise.resolve(result).finally(() => {
1101
+ this.completePre30ConcurrentTest(concurrentTestState, startedAt)
1102
+ })
1103
+ }
1104
+
1105
+ /**
1106
+ * @param {EfdRetryGate} retryGate
1107
+ * @param {object} ctx
1108
+ * @param {() => unknown} run
1109
+ * @returns {Promise<void>}
1110
+ */
1111
+ enqueueDetachedEfdRetry (retryGate, ctx, run) {
1112
+ /**
1113
+ * @param {() => void} resolve
1114
+ * @returns {void}
1115
+ */
1116
+ const enqueue = (resolve) => {
1117
+ /**
1118
+ * @param {boolean} shouldRun
1119
+ * @returns {void}
1120
+ */
1121
+ const onDecision = (shouldRun) => {
1122
+ if (!shouldRun) {
1123
+ ctx.isDiscardedEfdRetry = true
1124
+ resolve()
1125
+ return
1126
+ }
1127
+
1128
+ /**
1129
+ * @type {() => void}
1130
+ */
1131
+ let start
1132
+ const startPromise = new Promise(resolve => {
1133
+ start = resolve
1134
+ })
1135
+ const retry = { ctx, resolve, start }
1136
+ /**
1137
+ * @param {unknown} error
1138
+ * @returns {void}
1139
+ */
1140
+ const onRejected = (error) => {
1141
+ retry.ctx.detachedEfdRetryError = error
1142
+ this.finishDetachedEfdRetry(retry)
1143
+ }
1144
+ startPromise.then(() => this.runDetachedEfdRetryWithTimeout(ctx, run)).then(
1145
+ () => this.finishDetachedEfdRetry(retry),
1146
+ onRejected
1147
+ )
1148
+ this.#detachedEfdRetryQueue ??= []
1149
+ this.#detachedEfdRetryQueue.push(retry)
1150
+ this.drainDetachedEfdRetries()
1151
+ }
1152
+ retryGate.promise.then(onDecision)
1153
+ }
1154
+ return new Promise(enqueue)
1155
+ }
1156
+
1157
+ /**
1158
+ * Applies the timeout Jest 28/29 cannot start around a detached concurrent retry body.
1159
+ *
1160
+ * @param {object} ctx
1161
+ * @param {() => unknown} run
1162
+ * @returns {Promise<void>}
1163
+ */
1164
+ runDetachedEfdRetryWithTimeout (ctx, run) {
1165
+ return new Promise((resolve, reject) => {
1166
+ let completed = false
1167
+ const timeoutId = realSetTimeout(() => {
1168
+ if (completed) return
1169
+
1170
+ completed = true
1171
+ reject(new Error(`Exceeded timeout of ${ctx.testTimeout} ms for a test.`))
1172
+ }, ctx.testTimeout)
1173
+
1174
+ const settle = (callback, value) => {
1175
+ if (completed) return
1176
+
1177
+ completed = true
1178
+ timeoutId.unref?.()
1179
+ realClearTimeout(timeoutId)
1180
+ callback(value)
1181
+ }
1182
+
1183
+ let result
1184
+ try {
1185
+ result = run()
1186
+ } catch (error) {
1187
+ settle(reject, error)
1188
+ return
1189
+ }
1190
+ Promise.resolve(result).then(
1191
+ () => settle(resolve),
1192
+ error => settle(reject, error)
1193
+ )
1194
+ })
1195
+ }
1196
+
1197
+ /**
1198
+ * @returns {void}
1199
+ */
1200
+ drainDetachedEfdRetries () {
1201
+ const queue = this.#detachedEfdRetryQueue
1202
+ if (!queue) return
1203
+
1204
+ while (
1205
+ this.#pendingPre30ConcurrentTests === 0 &&
1206
+ this.#activeDetachedEfdRetries < this.#detachedEfdRetryConcurrency &&
1207
+ queue.length > 0
1208
+ ) {
1209
+ this.#activeDetachedEfdRetries++
1210
+ queue.shift().start()
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * @param {DetachedEfdRetry} retry
1216
+ * @returns {void}
1217
+ */
1218
+ finishDetachedEfdRetry (retry) {
1219
+ this.#activeDetachedEfdRetries--
1220
+ retry.resolve()
1221
+ this.drainDetachedEfdRetries()
1222
+ }
1223
+
1224
+ /**
1225
+ * Preserves Jest's immediate error when a callback test also returns a value.
1226
+ *
1227
+ * @param {() => unknown} run
1228
+ * @param {(error?: Error) => void} done
1229
+ * @returns {void}
1230
+ */
1231
+ runCallbackEfdRetry (run, done) {
1232
+ let result
1233
+ try {
1234
+ result = run()
1235
+ } catch (error) {
1236
+ done(error)
1237
+ return
1238
+ }
1239
+ if (result !== undefined) {
1240
+ done(new Error(
1241
+ "Test functions cannot both take a 'done' callback and return something. " +
1242
+ "Either use a 'done' callback, or return a promise."
1243
+ ))
1244
+ }
1245
+ }
1246
+
883
1247
  /**
884
1248
  * Runs a concurrent test function inside its test span context.
885
1249
  *
@@ -896,24 +1260,51 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
896
1260
  if (typeof done === 'function') {
897
1261
  done()
898
1262
  }
1263
+ this.completePre30ConcurrentTest(concurrentTestState)
899
1264
  return
900
1265
  }
901
1266
 
902
1267
  const runTest = () => testFn.apply(thisArg, args)
903
- if (!ctx.isAttemptToFixRetry && !ctx.isEfdRetry) {
904
- ctx.isJestRetry = concurrentTestState.numExecutions > 0
1268
+ const runTestInContext = () => {
1269
+ if (!ctx.isAttemptToFixRetry && !ctx.isEfdRetry) {
1270
+ ctx.isJestRetry = concurrentTestState.numExecutions > 0
1271
+ }
1272
+ concurrentTestState.numExecutions++
1273
+
1274
+ if (ctx.currentStore) {
1275
+ return testFnCh.runStores(ctx, runTest)
1276
+ }
1277
+
1278
+ let result
1279
+ testStartCh.runStores(ctx, () => {
1280
+ result = testFnCh.runStores(ctx, runTest)
1281
+ })
1282
+ return result
905
1283
  }
906
- concurrentTestState.numExecutions++
907
1284
 
908
- if (ctx.currentStore) {
909
- return testFnCh.runStores(ctx, runTest)
1285
+ const retryGate = ctx.efdRetryGates?.[ctx.efdRetryIndex - 1]
1286
+ if (retryGate && !hasConcurrentTestsStartEvent) {
1287
+ ctx.detachedEfdRetryPromise = this.enqueueDetachedEfdRetry(retryGate, ctx, runTestInContext)
1288
+ return
1289
+ }
1290
+ if (retryGate && typeof args[0] === 'function') {
1291
+ this.runCallbackEfdRetry(runTestInContext, args[0])
1292
+ return
910
1293
  }
911
1294
 
1295
+ if (!concurrentTestState.trackEfdCompletion) {
1296
+ return runTestInContext()
1297
+ }
1298
+
1299
+ const startedAt = efdCandidates.has(ctx.name) ? performance.now() : undefined
912
1300
  let result
913
- testStartCh.runStores(ctx, () => {
914
- result = testFnCh.runStores(ctx, runTest)
915
- })
916
- return result
1301
+ try {
1302
+ result = runTestInContext()
1303
+ } catch (error) {
1304
+ this.completePre30ConcurrentTest(concurrentTestState, startedAt)
1305
+ throw error
1306
+ }
1307
+ return this.trackPre30ConcurrentTest(concurrentTestState, result, startedAt)
917
1308
  }
918
1309
 
919
1310
  /**
@@ -1047,30 +1438,63 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1047
1438
  return result
1048
1439
  }
1049
1440
 
1050
- // Generic function to handle test retries
1441
+ /**
1442
+ * @param {number} retryCount
1443
+ * @returns {EfdRetryGate[]}
1444
+ */
1445
+ createEfdRetryGates (retryCount) {
1446
+ const gates = []
1447
+ for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
1448
+ /** @type {(shouldRun: boolean) => void} */
1449
+ let resolveGate
1450
+ const promise = new Promise(resolve => {
1451
+ resolveGate = resolve
1452
+ })
1453
+ gates.push({ promise, resolve: resolveGate })
1454
+ }
1455
+ return gates
1456
+ }
1457
+
1458
+ /**
1459
+ * Registers retry tests while Jest still accepts additions to the test tree.
1460
+ *
1461
+ * @param {JestRetryOptions} options
1462
+ * @returns {number}
1463
+ */
1051
1464
  retryTest ({
1465
+ concurrentTestState: registeredConcurrentTestState,
1466
+ efdRetryGates,
1052
1467
  jestEvent,
1053
- forceSerial,
1468
+ state,
1469
+ isModified,
1054
1470
  retryCount,
1055
1471
  retryType,
1056
1472
  }) {
1057
- const { testName, fn, timeout } = jestEvent
1473
+ const { testName, fn, mode, timeout } = jestEvent
1058
1474
  const isAttemptToFixRetry = retryType === 'Test Management (Attempt to Fix)'
1059
1475
  const isEfdRetry = retryType === 'Impacted tests' || retryType === 'Early flake detection'
1060
- for (let retryIndex = 0; retryIndex < retryCount; retryIndex++) {
1061
- const concurrentTestState = this.concurrentTestStates.get(fn)
1476
+ const concurrentTestState = registeredConcurrentTestState || this.concurrentTestStates.get(fn)
1477
+ let originalTest
1478
+ if (isEfdRetry && !efdRetryGates) {
1479
+ originalTest = getTestEntries(state.currentDescribeBlock)?.at(-1)
1480
+ if (originalTest?.fn !== fn) {
1481
+ log.error('%s could not retain its original Jest test', retryType)
1482
+ return 0
1483
+ }
1484
+ }
1485
+ let registeredRetryCount = 0
1486
+ for (let retryIndex = 1; retryIndex <= retryCount; retryIndex++) {
1487
+ const testEntries = getTestEntries(state.currentDescribeBlock)
1488
+ const initialTestCount = testEntries?.length
1489
+ let retryFn
1062
1490
  if (concurrentTestState) {
1063
- const test = forceSerial
1064
- ? concurrentTestState.serialTest
1065
- : (
1066
- concurrentTestState.concurrentTest ||
1067
- getOriginalConcurrentTest(this.global.test?.concurrent)
1068
- )
1491
+ const test = concurrentTestState.concurrentTest ??
1492
+ getOriginalConcurrentTest(this.global.test?.concurrent)
1069
1493
  if (typeof test !== 'function') {
1070
1494
  log.error('%s could not retry concurrent test because test is undefined', retryType)
1071
1495
  continue
1072
1496
  }
1073
- const retryFn = this.createConcurrentTestFn(
1497
+ retryFn = this.createConcurrentTestFn(
1074
1498
  testName,
1075
1499
  concurrentTestState.testFn,
1076
1500
  undefined,
@@ -1078,65 +1502,427 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1078
1502
  {
1079
1503
  concurrentTest: concurrentTestState.concurrentTest,
1080
1504
  concurrentTestThisArg: concurrentTestState.concurrentTestThisArg,
1505
+ efdRetryGates,
1506
+ efdRetryIndex: retryIndex,
1081
1507
  isAttemptToFixRetry,
1082
1508
  isEfdRetry,
1083
- isModified: concurrentTestState.ctx.isModified,
1084
- serialTest: concurrentTestState.serialTest,
1085
- serialTestThisArg: concurrentTestState.serialTestThisArg,
1509
+ isModified: isModified ?? concurrentTestState.ctx.isModified,
1086
1510
  testParameters: concurrentTestState.ctx.testParameters,
1511
+ timeout,
1087
1512
  }
1088
1513
  )
1089
- const thisArg = forceSerial
1090
- ? concurrentTestState.serialTestThisArg
1091
- : concurrentTestState.concurrentTestThisArg
1092
- test.call(thisArg, testName, retryFn, timeout)
1514
+ test.call(concurrentTestState.concurrentTestThisArg, testName, retryFn, timeout)
1093
1515
  } else if (this.global.test) {
1094
- this.global.test(testName, fn, timeout)
1516
+ const test = mode === 'only' ? this.global.test.only : this.global.test
1517
+ // The child-count check below proves Jest added a node, so retries can share the test
1518
+ // function instead of retaining one wrapper per scheduled execution.
1519
+ retryFn = fn
1520
+ test(testName, retryFn, timeout)
1095
1521
  } else {
1096
1522
  log.error('%s could not retry test because global.test is undefined', retryType)
1523
+ continue
1524
+ }
1525
+
1526
+ if (isEfdRetry) {
1527
+ const retryTest = testEntries?.length === initialTestCount + 1 ? testEntries.at(-1) : undefined
1528
+ if (retryTest?.fn !== retryFn) {
1529
+ log.error('%s could not retain its pre-registered Jest retry', retryType)
1530
+ continue
1531
+ }
1532
+ efdRetryMetadataByTest.set(retryTest, {
1533
+ isModified: Boolean(isModified),
1534
+ originalTest,
1535
+ retryIndex,
1536
+ })
1097
1537
  }
1538
+ registeredRetryCount++
1098
1539
  }
1540
+ return registeredRetryCount
1099
1541
  }
1100
1542
 
1101
- // At the `add_test` event we don't have the test object yet, so we can't use it
1543
+ /**
1544
+ * Registers retries for the test Jest just added, keeping a registration failure out of the
1545
+ * `add_test` dispatch, where Jest would report it as a test file error.
1546
+ *
1547
+ * @param {JestRetryOptions} retryOptions
1548
+ * @returns {boolean}
1549
+ */
1550
+ registerRetryTests (retryOptions) {
1551
+ // Jest turns later additions into "Cannot add a test after tests have started running".
1552
+ if (retryOptions.state.hasStarted) {
1553
+ log.error('%s could not register retries after Jest started', retryOptions.retryType)
1554
+ return false
1555
+ }
1556
+
1557
+ const testEntries = getTestEntries(retryOptions.state.currentDescribeBlock)
1558
+ const initialTestCount = testEntries?.length
1559
+ try {
1560
+ const registeredRetryCount = this.retryTest(retryOptions)
1561
+ if (registeredRetryCount === retryOptions.retryCount) return true
1562
+ } catch (error) {
1563
+ log.error('%s could not register retries', retryOptions.retryType, error)
1564
+ }
1565
+
1566
+ if (testEntries && initialTestCount !== undefined) {
1567
+ const retryTests = testEntries.splice(initialTestCount)
1568
+ for (const retryTest of retryTests) {
1569
+ removedRetryTests.add(retryTest)
1570
+ const retryCtx = this.concurrentTestStates.get(retryTest.fn)?.ctx
1571
+ if (retryCtx) {
1572
+ this.removeConcurrentTestContext(retryOptions.testFullName, retryCtx)
1573
+ }
1574
+ }
1575
+ const tests = retryOptions.state.currentDescribeBlock?.tests
1576
+ if (tests) {
1577
+ let writeIndex = 0
1578
+ for (const test of tests) {
1579
+ if (!removedRetryTests.has(test)) {
1580
+ tests[writeIndex++] = test
1581
+ }
1582
+ }
1583
+ tests.length = writeIndex
1584
+ }
1585
+ }
1586
+ this.#efdRetryGatesByName?.delete(retryOptions.testFullName)
1587
+ log.error('%s did not register every retry', retryOptions.retryType)
1588
+ return false
1589
+ }
1590
+
1591
+ /**
1592
+ * Drops the pre-registered EFD retries that exceed the retry count Jest may still run.
1593
+ *
1594
+ * @param {string} testName
1595
+ * @param {object} executedTest The Jest test node that selected the retry count.
1596
+ * @param {number} [retryCount] Retries to keep. Every retry is dropped when omitted.
1597
+ * @returns {number} Retries left to run.
1598
+ */
1599
+ discardEfdRetries (testName, executedTest, retryCount = 0) {
1600
+ const siblings = getTestEntries(executedTest.parent)
1601
+ const executedTestIndex = siblings ? siblings.indexOf(executedTest) : -1
1602
+ if (executedTestIndex === -1) return 0
1603
+
1604
+ const originalTest = efdRetryMetadataByTest.get(executedTest)?.originalTest ?? executedTest
1605
+
1606
+ // Circus iterates children live, so only entries after the test that just ran may move.
1607
+ // Whichever pre-registered slot runs first is the original execution; the next retryCount
1608
+ // slots are its retries, independent of Jest's randomized physical node order.
1609
+ let runnableRetryCount = 0
1610
+ let writeIndex = executedTestIndex + 1
1611
+ for (let readIndex = writeIndex; readIndex < siblings.length; readIndex++) {
1612
+ const sibling = siblings[readIndex]
1613
+ const siblingOriginalTest = efdRetryMetadataByTest.get(sibling)?.originalTest
1614
+ if (sibling === originalTest || siblingOriginalTest === originalTest) {
1615
+ if (runnableRetryCount >= retryCount) {
1616
+ removedRetryTests.add(sibling)
1617
+ const discardedCtx = this.concurrentTestStates.get(sibling.fn)?.ctx
1618
+ if (discardedCtx) {
1619
+ this.removeConcurrentTestContext(testName, discardedCtx)
1620
+ }
1621
+ continue
1622
+ }
1623
+ runnableRetryCount++
1624
+ }
1625
+ siblings[writeIndex++] = sibling
1626
+ }
1627
+ siblings.length = writeIndex
1628
+
1629
+ const tests = executedTest.parent?.tests
1630
+ if (tests) {
1631
+ writeIndex = 0
1632
+ for (const test of tests) {
1633
+ if (!removedRetryTests.has(test)) {
1634
+ tests[writeIndex++] = test
1635
+ }
1636
+ }
1637
+ tests.length = writeIndex
1638
+ }
1639
+ return runnableRetryCount
1640
+ }
1641
+
1642
+ /**
1643
+ * @param {object} describeBlock
1644
+ * @param {boolean} hasFocusedTests
1645
+ * @param {RegExp|undefined} testNamePattern
1646
+ * @returns {number}
1647
+ */
1648
+ markPre30ConcurrentTests (describeBlock, hasFocusedTests, testNamePattern) {
1649
+ if (describeBlock.mode === 'skip') return 0
1650
+
1651
+ let testCount = 0
1652
+ for (const child of describeBlock.children) {
1653
+ if (child.type === 'describeBlock') {
1654
+ testCount += this.markPre30ConcurrentTests(child, hasFocusedTests, testNamePattern)
1655
+ continue
1656
+ }
1657
+ if (
1658
+ child.type !== 'test' ||
1659
+ !child.concurrent ||
1660
+ efdRetryMetadataByTest.has(child) ||
1661
+ isJestTestSkipped(child, hasFocusedTests, testNamePattern)
1662
+ ) {
1663
+ continue
1664
+ }
1665
+
1666
+ const concurrentTestState = this.concurrentTestStates.get(child.fn)
1667
+ if (concurrentTestState) {
1668
+ concurrentTestState.trackEfdCompletion = true
1669
+ testCount++
1670
+ }
1671
+ }
1672
+ return testCount
1673
+ }
1674
+
1675
+ /**
1676
+ * @param {object} describeBlock
1677
+ * @param {object} state
1678
+ * @returns {void}
1679
+ */
1680
+ preparePre30ConcurrentTests (describeBlock, state) {
1681
+ const testNamePattern = state.testNamePattern
1682
+ ? new RegExp(state.testNamePattern.source, state.testNamePattern.flags)
1683
+ : undefined
1684
+ this.#detachedEfdRetryConcurrency = state.maxConcurrency ?? 1
1685
+ this.#detachedEfdRetryQueue = []
1686
+ this.#pendingPre30ConcurrentTests = this.markPre30ConcurrentTests(
1687
+ describeBlock,
1688
+ state.hasFocusedTests,
1689
+ testNamePattern
1690
+ )
1691
+ this.drainDetachedEfdRetries()
1692
+ }
1693
+
1694
+ /**
1695
+ * @param {object[]} tests
1696
+ * @returns {void}
1697
+ */
1698
+ orderConcurrentEfdRetries (tests) {
1699
+ if (!this.#efdRetryGatesByName) return
1700
+
1701
+ const retryTests = []
1702
+ let originalTestIndex = 0
1703
+ for (const test of tests) {
1704
+ if (efdRetryMetadataByTest.has(test)) {
1705
+ retryTests.push(test)
1706
+ } else {
1707
+ tests[originalTestIndex++] = test
1708
+ }
1709
+ }
1710
+ for (const retryTest of retryTests) {
1711
+ tests[originalTestIndex++] = retryTest
1712
+ }
1713
+ }
1714
+
1715
+ /**
1716
+ * @param {string} testName
1717
+ * @param {number} durationMs
1718
+ * @returns {void}
1719
+ */
1720
+ determineEfdRetries (testName, durationMs) {
1721
+ if (efdDeterminedRetries.has(testName)) return
1722
+
1723
+ const retryCount = getEfdRetryCountForDuration(durationMs, this.#earlyFlakeDetectionRetryPolicy)
1724
+ efdDeterminedRetries.set(testName, retryCount)
1725
+ efdExpectedExecutions.set(testName, retryCount + 1)
1726
+ const retryGates = this.#efdRetryGatesByName?.get(testName)
1727
+ if (retryGates) {
1728
+ for (let retryIndex = 0; retryIndex < retryGates.length; retryIndex++) {
1729
+ retryGates[retryIndex].resolve(retryIndex < retryCount)
1730
+ }
1731
+ }
1732
+ if (retryCount === 0) {
1733
+ efdSlowAbortedTests.add(testName)
1734
+ }
1735
+ }
1736
+
1737
+ /**
1738
+ * @returns {void}
1739
+ */
1740
+ removeDiscardedEfdRetries () {
1741
+ if (!this.#discardedEfdRetryTests) return
1742
+
1743
+ for (const retryTest of this.#discardedEfdRetryTests) {
1744
+ const siblings = getTestEntries(retryTest.parent)
1745
+ const retryTestIndex = siblings?.indexOf(retryTest) ?? -1
1746
+ if (retryTestIndex !== -1) {
1747
+ siblings.splice(retryTestIndex, 1)
1748
+ }
1749
+ }
1750
+ this.#discardedEfdRetryTests = undefined
1751
+ }
1752
+
1753
+ // The `add_test` payload has no test node, so derive its full name from the current describe block.
1102
1754
  getTestNameFromAddTestEvent (event, state) {
1103
1755
  const describeSuffix = getRawJestTestName(state.currentDescribeBlock)
1104
1756
  const testName = describeSuffix ? `${describeSuffix} ${event.testName}` : event.testName
1105
1757
  return removeSeedSuffixFromTestName(testName)
1106
1758
  }
1107
1759
 
1760
+ /**
1761
+ * Classifies a newly registered test and pre-registers every retry Jest may need.
1762
+ *
1763
+ * @param {object} event
1764
+ * @param {object} state
1765
+ * @returns {void}
1766
+ */
1767
+ handleAddTestEvent (event, state) {
1768
+ if (event.concurrent) {
1769
+ this.hasConcurrentTests = true
1770
+ }
1771
+ if (event.failing) {
1772
+ return
1773
+ }
1774
+
1775
+ const testFullName = this.getTestNameFromAddTestEvent(event, state)
1776
+ const concurrentTestContexts = this.concurrentTestContexts.get(testFullName)
1777
+ const concurrentTestState = this.concurrentTestStates.get(event.fn) ||
1778
+ concurrentTestContexts?.at(-1)?.concurrentTestState
1779
+ const registeredTest = getTestEntries(state.currentDescribeBlock)?.at(-1)
1780
+ if (concurrentTestState && registeredTest?.fn === event.fn) {
1781
+ testContexts.set(registeredTest, concurrentTestState.ctx)
1782
+ }
1783
+ if (retriedTestsToNumAttempts.has(testFullName)) {
1784
+ return
1785
+ }
1786
+ const isSkipped = event.mode === 'todo' || event.mode === 'skip'
1787
+ const isAttemptToFix = this.isTestManagementTestsEnabled &&
1788
+ this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
1789
+ const isDisabled = this.isTestManagementTestsEnabled &&
1790
+ this.testManagementTestsForThisSuite?.disabled?.includes(testFullName)
1791
+ if (isAttemptToFix && !isSkipped) {
1792
+ retriedTestsToNumAttempts.set(testFullName, 0)
1793
+ testsToBeRetried.add(testFullName)
1794
+ const retriesRegistered = this.registerRetryTests({
1795
+ concurrentTestState,
1796
+ jestEvent: event,
1797
+ state,
1798
+ retryCount: testManagementAttemptToFixRetries,
1799
+ retryType: 'Test Management (Attempt to Fix)',
1800
+ testFullName,
1801
+ })
1802
+ if (!retriesRegistered) {
1803
+ retriedTestsToNumAttempts.delete(testFullName)
1804
+ testsToBeRetried.delete(testFullName)
1805
+ }
1806
+ }
1807
+
1808
+ const isModified = this.isImpactedTestsEnabled && (
1809
+ concurrentTestState?.ctx?.isModified ||
1810
+ this.isTestModified(event.asyncError, event.fn)
1811
+ )
1812
+ const isNew = this.isKnownTestsEnabled && !this.knownTestsForThisSuite.includes(testFullName)
1813
+
1814
+ if (isNew && !isSkipped && !isAttemptToFix) {
1815
+ newTests.add(testFullName)
1816
+ if (DYNAMIC_NAME_RE.test(testFullName)) {
1817
+ // Populated directly for runInBand; for parallel workers the main process
1818
+ // collects these from the TEST_HAS_DYNAMIC_NAME span tag via worker-report:trace.
1819
+ newTestsWithDynamicNames.add(`${this.testSuite} › ${testFullName}`)
1820
+ }
1821
+ }
1822
+
1823
+ if (
1824
+ isAttemptToFix ||
1825
+ isDisabled ||
1826
+ isSkipped ||
1827
+ !this.isEarlyFlakeDetectionEnabled ||
1828
+ !hasEfdRetries(this.#earlyFlakeDetectionRetryPolicy) ||
1829
+ (!isNew && !isModified)
1830
+ ) {
1831
+ return
1832
+ }
1833
+
1834
+ retriedTestsToNumAttempts.set(testFullName, 0)
1835
+ testsToBeRetried.add(testFullName)
1836
+ const schedulingRetryCount = this.#earlyFlakeDetectionRetryPolicy.schedulingRetryCount
1837
+ const efdRetryGates = concurrentTestState
1838
+ ? this.createEfdRetryGates(schedulingRetryCount)
1839
+ : undefined
1840
+ if (efdRetryGates) {
1841
+ this.#efdRetryGatesByName ??= new Map()
1842
+ this.#efdRetryGatesByName.set(testFullName, efdRetryGates)
1843
+ }
1844
+ const retriesRegistered = this.registerRetryTests({
1845
+ concurrentTestState,
1846
+ efdRetryGates,
1847
+ isModified,
1848
+ jestEvent: event,
1849
+ state,
1850
+ retryCount: schedulingRetryCount,
1851
+ retryType: isModified ? 'Impacted tests' : 'Early flake detection',
1852
+ testFullName,
1853
+ })
1854
+ if (retriesRegistered) {
1855
+ efdCandidates.add(testFullName)
1856
+ } else {
1857
+ retriedTestsToNumAttempts.delete(testFullName)
1858
+ testsToBeRetried.delete(testFullName)
1859
+ }
1860
+ }
1861
+
1108
1862
  async handleTestEvent (event, state) {
1109
1863
  if (isDatadogJestEventHandled(event)) return
1110
1864
  markDatadogJestEventHandled(event)
1111
1865
 
1866
+ if (event.name === 'add_test') {
1867
+ this.handleAddTestEvent(event, state)
1868
+ }
1869
+
1112
1870
  if (super.handleTestEvent) {
1113
1871
  await super.handleTestEvent(event, state)
1114
1872
  }
1115
1873
 
1116
1874
  if (event.name === 'setup') {
1117
1875
  this.wrapConcurrentTest(state)
1876
+ this.bindTestEach(this.global.test)
1118
1877
  }
1119
- if (event.name === 'setup' && this.global.test) {
1120
- const environment = this
1121
- shimmer.wrap(this.global.test, 'each', each => function (...args) {
1122
- const testParameters = getFormattedJestTestParameters(args)
1123
- const eachBind = each.apply(this, args)
1124
- return function (...args) {
1125
- const [testName] = args
1126
- environment.setNameToParams(testName, testParameters)
1127
- return eachBind.apply(this, args)
1128
- }
1129
- })
1878
+ if (
1879
+ event.name === 'run_describe_start' &&
1880
+ !hasConcurrentTestsStartEvent &&
1881
+ !event.describeBlock.parent &&
1882
+ this.#efdRetryGatesByName?.size > 0
1883
+ ) {
1884
+ this.preparePre30ConcurrentTests(event.describeBlock, state)
1130
1885
  }
1131
- if (event.name === 'test_start') {
1886
+ if (event.name === 'concurrent_tests_start') {
1887
+ this.orderConcurrentEfdRetries(event.tests)
1888
+ } else if (event.name === 'test_start') {
1132
1889
  const testName = getJestTestName(event.test)
1133
- const concurrentTestState = this.concurrentTestStates.get(event.test.fn)
1890
+ let isAttemptToFix = false
1891
+ let isDisabled = false
1892
+ let isQuarantined = false
1893
+ if (this.isTestManagementTestsEnabled) {
1894
+ isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
1895
+ isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(testName)
1896
+ isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(testName)
1897
+ if (!isAttemptToFix && isDisabled) {
1898
+ event.test.mode = 'skip'
1899
+ }
1900
+ }
1901
+ if (
1902
+ event.test.mode === 'todo' ||
1903
+ isJestTestSkipped(event.test, state.hasFocusedTests, state.testNamePattern)
1904
+ ) {
1905
+ return
1906
+ }
1907
+
1908
+ const registeredConcurrentTestState = testContexts.get(event.test)?.concurrentTestState
1909
+ const concurrentTestState = this.concurrentTestStates.get(event.test.fn) ||
1910
+ registeredConcurrentTestState
1134
1911
  let concurrentCtx = concurrentTestState?.ctx
1135
1912
  if (concurrentCtx) {
1136
1913
  this.removeConcurrentTestContext(testName, concurrentCtx)
1137
1914
  } else {
1138
1915
  concurrentCtx = this.getNextConcurrentTestContext(testName)
1139
1916
  }
1917
+ if (hasConcurrentTestsStartEvent && concurrentCtx?.efdRetryGates) {
1918
+ const retryGate = concurrentCtx.efdRetryGates[concurrentCtx.efdRetryIndex - 1]
1919
+ const shouldRun = await retryGate.promise
1920
+ if (!shouldRun) {
1921
+ concurrentCtx.isDiscardedEfdRetry = true
1922
+ event.test.mode = 'skip'
1923
+ return
1924
+ }
1925
+ }
1140
1926
  if (testsToBeRetried.has(testName)) {
1141
1927
  // This is needed because we're retrying tests with the same name
1142
1928
  this.resetSnapshotState()
@@ -1148,24 +1934,17 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1148
1934
  let numOfAttemptsToFixRetries = null
1149
1935
  const testParameters = concurrentCtx?.testParameters ||
1150
1936
  concurrentTestState?.ctx?.testParameters ||
1937
+ this.testParametersByFunction.get(event.test.fn) ||
1151
1938
  getTestParametersString(this.nameToParams, event.test.name)
1152
1939
 
1153
- let isAttemptToFix = false
1154
- let isDisabled = false
1155
- let isQuarantined = false
1156
- if (this.isTestManagementTestsEnabled) {
1157
- isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
1158
- isDisabled = this.testManagementTestsForThisSuite?.disabled?.includes(testName)
1159
- isQuarantined = this.testManagementTestsForThisSuite?.quarantined?.includes(testName)
1160
- if (isAttemptToFix) {
1161
- numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(testName)
1162
- retriedTestsToNumAttempts.set(testName, numOfAttemptsToFixRetries + 1)
1163
- } else if (isDisabled) {
1164
- event.test.mode = 'skip'
1165
- }
1940
+ if (isAttemptToFix) {
1941
+ numOfAttemptsToFixRetries = retriedTestsToNumAttempts.get(testName)
1942
+ retriedTestsToNumAttempts.set(testName, numOfAttemptsToFixRetries + 1)
1166
1943
  }
1167
1944
 
1168
- let isModified = this.isTestModified(event.test.asyncError, event.test.fn)
1945
+ const efdRetryMetadata = efdRetryMetadataByTest.get(event.test)
1946
+ let isModified = efdRetryMetadata?.isModified ||
1947
+ this.isTestModified(event.test.asyncError, event.test.fn)
1169
1948
  if (concurrentCtx?.isModified || concurrentTestState?.ctx?.isModified) {
1170
1949
  isModified = true
1171
1950
  }
@@ -1174,7 +1953,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1174
1953
  isNewTest = newTests.has(testName)
1175
1954
  }
1176
1955
 
1177
- const willRunEfd = this.isEarlyFlakeDetectionEnabled && (isNewTest || isModified)
1956
+ const willRunEfd = this.isEarlyFlakeDetectionEnabled &&
1957
+ hasEfdRetries(this.#earlyFlakeDetectionRetryPolicy) &&
1958
+ (isNewTest || isModified)
1178
1959
  event.test[ATR_RETRY_SUPPRESSION_FLAG] = Boolean(isAttemptToFix || willRunEfd)
1179
1960
 
1180
1961
  if (!isAttemptToFix && willRunEfd) {
@@ -1192,9 +1973,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1192
1973
  ctx.testParameters = testParameters
1193
1974
  ctx.frameworkVersion = jestVersion
1194
1975
  ctx.isNew = isNewTest
1195
- ctx.isEfdRetry = ctx.isEfdRetry || numEfdRetry > 0
1976
+ ctx.isEfdRetry ||= numEfdRetry > 0
1977
+ ctx.efdRetryIndex ??= efdRetryMetadata?.retryIndex
1196
1978
  ctx.isAttemptToFix = isAttemptToFix
1197
- ctx.isAttemptToFixRetry = ctx.isAttemptToFixRetry || numOfAttemptsToFixRetries > 0
1979
+ ctx.isAttemptToFixRetry ||= numOfAttemptsToFixRetries > 0
1198
1980
  ctx.isJestRetry = isJestRetry
1199
1981
  ctx.isDisabled = isDisabled
1200
1982
  ctx.isQuarantined = isQuarantined
@@ -1301,68 +2083,23 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1301
2083
  })
1302
2084
  }
1303
2085
 
1304
- if (event.name === 'add_test') {
1305
- if (event.concurrent) {
1306
- this.hasConcurrentTests = true
1307
- }
1308
-
1309
- if (event.failing) {
1310
- return
1311
- }
1312
-
1313
- const testFullName = this.getTestNameFromAddTestEvent(event, state)
1314
- const isSkipped = event.mode === 'todo' || event.mode === 'skip'
1315
- const isAttemptToFix = this.isTestManagementTestsEnabled &&
1316
- this.testManagementTestsForThisSuite?.attemptToFix?.includes(testFullName)
1317
- if (
1318
- isAttemptToFix &&
1319
- !isSkipped &&
1320
- !retriedTestsToNumAttempts.has(testFullName)
1321
- ) {
1322
- retriedTestsToNumAttempts.set(testFullName, 0)
1323
- testsToBeRetried.add(testFullName)
1324
- this.retryTest({
1325
- jestEvent: event,
1326
- retryCount: testManagementAttemptToFixRetries,
1327
- retryType: 'Test Management (Attempt to Fix)',
1328
- })
2086
+ if (event.name === 'test_done') {
2087
+ const retryCtx = testContexts.get(event.test)
2088
+ if (!hasConcurrentTestsStartEvent && retryCtx?.concurrentTestState) {
2089
+ this.completePre30ConcurrentTest(retryCtx.concurrentTestState)
1329
2090
  }
1330
- if (!isAttemptToFix && this.isImpactedTestsEnabled) {
1331
- const concurrentTestState = this.concurrentTestStates.get(event.fn)
1332
- const isModified = concurrentTestState?.ctx?.isModified ||
1333
- this.isTestModified(event.asyncError, event.fn)
1334
- if (isModified && !retriedTestsToNumAttempts.has(testFullName) && this.isEarlyFlakeDetectionEnabled) {
1335
- retriedTestsToNumAttempts.set(testFullName, 0)
1336
- testsToBeRetried.add(testFullName)
1337
- this.retryTest({
1338
- jestEvent: event,
1339
- retryCount: earlyFlakeDetectionNumRetries,
1340
- retryType: 'Impacted tests',
1341
- })
2091
+ if (retryCtx?.detachedEfdRetryPromise) {
2092
+ await retryCtx.detachedEfdRetryPromise
2093
+ if (retryCtx.detachedEfdRetryError !== undefined) {
2094
+ event.test.errors.push(retryCtx.detachedEfdRetryError)
1342
2095
  }
1343
2096
  }
1344
- if (!isAttemptToFix && this.isKnownTestsEnabled) {
1345
- const isNew = !this.knownTestsForThisSuite.includes(testFullName)
1346
- if (isNew && !isSkipped) {
1347
- newTests.add(testFullName)
1348
- }
1349
- if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testFullName)) {
1350
- if (DYNAMIC_NAME_RE.test(testFullName)) {
1351
- // Populated directly for runInBand; for parallel workers the main process
1352
- // collects these from the TEST_HAS_DYNAMIC_NAME span tag via worker-report:trace.
1353
- newTestsWithDynamicNames.add(`${this.testSuite} › ${testFullName}`)
1354
- }
1355
- retriedTestsToNumAttempts.set(testFullName, 0)
1356
- if (this.isEarlyFlakeDetectionEnabled) {
1357
- testsToBeRetried.add(testFullName)
1358
- efdNewTestCandidates.add(testFullName)
1359
- // Cloning is deferred to test_done after the first execution,
1360
- // when we know the duration and can choose the right retry count.
1361
- }
1362
- }
2097
+ if (retryCtx?.isDiscardedEfdRetry) {
2098
+ this.#discardedEfdRetryTests ??= new Set()
2099
+ this.#discardedEfdRetryTests.add(event.test)
2100
+ return
1363
2101
  }
1364
- }
1365
- if (event.name === 'test_done') {
2102
+
1366
2103
  const originalError = event.test?.errors?.[0]
1367
2104
  let status = 'pass'
1368
2105
  if (event.test.errors && event.test.errors.length) {
@@ -1410,71 +2147,32 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1410
2147
  }
1411
2148
  }
1412
2149
 
1413
- // EFD dynamic cloning: on first execution of a new EFD candidate,
1414
- // determine the retry count from the test's duration.
2150
+ // The first execution selects how many pre-registered EFD retries remain runnable.
1415
2151
  if (
1416
2152
  this.isEarlyFlakeDetectionEnabled &&
1417
- this.isKnownTestsEnabled &&
1418
- efdNewTestCandidates.has(testName) &&
2153
+ efdCandidates.has(testName) &&
1419
2154
  event.test.invocations === 1 &&
1420
2155
  !efdDeterminedRetries.has(testName)
1421
2156
  ) {
1422
- const durationMs = event.test.duration ?? 0
1423
- const retryCount = getEfdRetryCount(durationMs, earlyFlakeDetectionSlowTestRetries)
1424
- efdDeterminedRetries.set(testName, retryCount)
1425
- if (retryCount > 0) {
1426
- // Temporarily adjust jest-circus state so that retry tests are registered
1427
- // into the correct describe block and bypass the "tests have started" guard.
1428
- //
1429
- // Problem 1 (jest-circus ≤24): currentDescribeBlock points to ROOT during
1430
- // execution, and ROOT's tests loop already finished before children ran.
1431
- //
1432
- // Problem 2 (jest-circus ≥27): `hasStarted = true` causes `test()` to throw
1433
- // "Cannot add a test after tests have started running".
1434
- //
1435
- // Fix: temporarily point currentDescribeBlock to the test's parent (so retries
1436
- // land in the still-iterating children array) and set hasStarted = false (so the
1437
- // guard is bypassed). Both are restored immediately after scheduling the retries.
1438
- const originalDescribeBlock = state.currentDescribeBlock
1439
- const originalHasStarted = state.hasStarted
1440
- state.currentDescribeBlock = event.test.parent ?? originalDescribeBlock
1441
- state.hasStarted = false
1442
- this.retryTest({
1443
- forceSerial: true,
1444
- jestEvent: {
1445
- testName: event.test.name,
1446
- fn: event.test.fn,
1447
- timeout: event.test.timeout,
1448
- },
1449
- retryCount,
1450
- retryType: 'Early flake detection',
1451
- })
1452
- state.currentDescribeBlock = originalDescribeBlock
1453
- state.hasStarted = originalHasStarted
1454
- } else {
1455
- efdSlowAbortedTests.add(testName)
2157
+ this.determineEfdRetries(testName, event.test.duration ?? 0)
2158
+ if (!this.#efdRetryGatesByName?.has(testName)) {
2159
+ this.discardEfdRetries(testName, event.test, efdDeterminedRetries.get(testName))
1456
2160
  }
1457
2161
  }
1458
2162
 
1459
- let isEfdRetry = false
1460
- // We'll store the test statuses of the retries
1461
- if (this.isKnownTestsEnabled) {
1462
- const isNewTest = newTests.has(testName)
1463
- if (isNewTest) {
1464
- if (newTestsTestStatuses.has(testName)) {
1465
- newTestsTestStatuses.get(testName).push(status)
1466
- isEfdRetry = true
1467
- } else {
1468
- newTestsTestStatuses.set(testName, [status])
1469
- }
1470
- const testStatuses = newTestsTestStatuses.get(testName)
1471
- // Check if this is the last EFD retry.
1472
- // If it is, we'll set the failedAllTests flag to true if all the tests failed
1473
- const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
1474
- if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
1475
- testStatuses.every(status => status === 'fail')) {
1476
- failedAllTests = true
1477
- }
2163
+ const isEfdRetry = retryCtx?.isEfdRetry === true
2164
+ if (efdCandidates.has(testName)) {
2165
+ let testStatuses = efdTestStatuses.get(testName)
2166
+ if (testStatuses) {
2167
+ testStatuses.push(status)
2168
+ } else {
2169
+ testStatuses = [status]
2170
+ efdTestStatuses.set(testName, testStatuses)
2171
+ }
2172
+ const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
2173
+ if (efdRetryCount > 0 && testStatuses.length === efdExpectedExecutions.get(testName) &&
2174
+ testStatuses.every(status => status === 'fail')) {
2175
+ failedAllTests = true
1478
2176
  }
1479
2177
  }
1480
2178
 
@@ -1511,6 +2209,16 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1511
2209
  log.warn('"ci:jest:test_done": no context found for test "%s"', testName)
1512
2210
  return
1513
2211
  }
2212
+ if (!isJestWorker) {
2213
+ recordTestManagementExecution({
2214
+ testSuite: ctx.suite,
2215
+ testName: ctx.name,
2216
+ status,
2217
+ isAttemptToFix: ctx.isAttemptToFix,
2218
+ isDisabled: ctx.isDisabled,
2219
+ isQuarantined: ctx.isQuarantined,
2220
+ })
2221
+ }
1514
2222
  if (ctx.concurrentTestState && !ctx.currentStore && !ctx.isDisabled) {
1515
2223
  testStartCh.runStores(ctx, () => {})
1516
2224
  }
@@ -1565,8 +2273,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1565
2273
  if (ctx.concurrentTestState) {
1566
2274
  ctx.currentStore = undefined
1567
2275
  }
1568
- }
1569
- if (event.name === 'run_finish') {
2276
+ } else if (event.name === 'run_finish') {
2277
+ this.removeDiscardedEfdRetries()
1570
2278
  for (const [test, errors] of atrSuppressedErrors) {
1571
2279
  // Do not restore errors for non-ATF quarantined tests — they should stay suppressed
1572
2280
  // so Jest doesn't see the failure (prevents --bail from stopping the run).
@@ -1588,16 +2296,31 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1588
2296
  }
1589
2297
 
1590
2298
  efdDeterminedRetries.clear()
2299
+ efdExpectedExecutions.clear()
1591
2300
  efdSlowAbortedTests.clear()
1592
- efdNewTestCandidates.clear()
2301
+ efdCandidates.clear()
1593
2302
  newTests.clear()
1594
2303
  retriedTestsToNumAttempts.clear()
1595
2304
  attemptToFixRetriedTestsStatuses.clear()
1596
2305
  testsToBeRetried.clear()
2306
+ this.#efdRetryGatesByName = undefined
2307
+ this.#detachedEfdRetryQueue = undefined
1597
2308
  testSuiteDatadogEnvironments.delete(this.testSuiteAbsolutePath)
1598
- }
1599
- if (event.name === 'test_skip' || event.name === 'test_todo') {
2309
+ } else if (event.name === 'test_skip' || event.name === 'test_todo') {
1600
2310
  const testName = getJestTestName(event.test)
2311
+ const retryGates = this.#efdRetryGatesByName?.get(testName)
2312
+ if (retryGates) {
2313
+ if (efdRetryMetadataByTest.has(event.test)) {
2314
+ this.#discardedEfdRetryTests ??= new Set()
2315
+ this.#discardedEfdRetryTests.add(event.test)
2316
+ return
2317
+ }
2318
+ for (const gate of retryGates) {
2319
+ gate.resolve(false)
2320
+ }
2321
+ } else {
2322
+ this.discardEfdRetries(testName, event.test)
2323
+ }
1601
2324
  testSkippedCh.publish({
1602
2325
  test: {
1603
2326
  name: testName,
@@ -1612,18 +2335,18 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1612
2335
  }
1613
2336
  }
1614
2337
 
1615
- getEfdResult ({ testName, isNewTest, isModifiedTest, isEfdRetry, numberOfExecutedRetries }) {
2338
+ getEfdResult ({ testName, isNewTest, isModifiedTest }) {
1616
2339
  const isEfdEnabled = this.isEarlyFlakeDetectionEnabled
1617
- const isEfdActive = isEfdEnabled && (isNewTest || isModifiedTest)
1618
- const retryCount = efdDeterminedRetries.get(testName) ?? 0
1619
- const isSlowAbort = efdSlowAbortedTests.has(testName)
1620
- const isLastEfdRetry = (isEfdRetry && numberOfExecutedRetries >= (retryCount + 1)) || isSlowAbort
1621
- const isFinalEfdTestExecution = isEfdActive && isLastEfdRetry
2340
+ const isEfdActive = isEfdEnabled &&
2341
+ hasEfdRetries(this.#earlyFlakeDetectionRetryPolicy) &&
2342
+ (isNewTest || isModifiedTest)
2343
+ const testStatuses = efdTestStatuses.get(testName)
2344
+ const isFinalEfdTestExecution = isEfdActive &&
2345
+ (efdSlowAbortedTests.has(testName) || testStatuses?.length === (efdExpectedExecutions.get(testName) ?? 0))
1622
2346
 
1623
2347
  let finalStatus
1624
2348
  if (isEfdActive && isFinalEfdTestExecution) {
1625
2349
  // For EFD: The framework reports 'pass' if ANY attempt passed (flaky but not failing)
1626
- const testStatuses = newTestsTestStatuses.get(testName)
1627
2350
  finalStatus = testStatuses && testStatuses.includes('pass') ? 'pass' : 'fail'
1628
2351
  }
1629
2352
 
@@ -1662,15 +2385,21 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
1662
2385
  return { isAttemptToFixEnabled, isFinalAttemptToFixExecution, finalStatus }
1663
2386
  }
1664
2387
 
1665
- getFinalStatus (testName, status, isNewTest, isModifiedTest, isEfdRetry, isAttemptToFix, numberOfTestInvocations) {
2388
+ getFinalStatus (
2389
+ testName,
2390
+ status,
2391
+ isNewTest,
2392
+ isModifiedTest,
2393
+ isEfdRetry,
2394
+ isAttemptToFix,
2395
+ numberOfTestInvocations
2396
+ ) {
1666
2397
  const numberOfExecutedRetries = retriedTestsToNumAttempts.get(testName) ?? 0
1667
2398
 
1668
2399
  const efdResult = this.getEfdResult({
1669
2400
  testName,
1670
2401
  isNewTest,
1671
2402
  isModifiedTest,
1672
- isEfdRetry,
1673
- numberOfExecutedRetries,
1674
2403
  })
1675
2404
  const atrResult = this.getAtrResult({ status, isEfdRetry, isAttemptToFix, numberOfTestInvocations })
1676
2405
  const attemptToFixResult = this.getAttemptToFixResult({
@@ -1869,8 +2598,6 @@ function resetLibraryConfiguration () {
1869
2598
  isItrEnabled = false
1870
2599
  isSuitesSkippingEnabled = false
1871
2600
  isEarlyFlakeDetectionEnabled = false
1872
- earlyFlakeDetectionNumRetries = 0
1873
- earlyFlakeDetectionSlowTestRetries = {}
1874
2601
  earlyFlakeDetectionFaultyThreshold = 30
1875
2602
  isEarlyFlakeDetectionFaulty = false
1876
2603
  isKnownTestsEnabled = false
@@ -1898,8 +2625,8 @@ function applySuiteSkipping (originalTests, rootDir, frameworkVersion) {
1898
2625
  hasUnskippableSuites = jestSuitesToRun.hasUnskippableSuites
1899
2626
  hasForcedToRunSuites = jestSuitesToRun.hasForcedToRunSuites
1900
2627
 
1901
- isSuitesSkipped = jestSuitesToRun.suitesToRun.length !== originalTests.length
1902
- numSkippedSuites = jestSuitesToRun.skippedSuites.length
2628
+ isSuitesSkipped ||= jestSuitesToRun.suitesToRun.length !== originalTests.length
2629
+ numSkippedSuites += jestSuitesToRun.skippedSuites.length
1903
2630
  skippedSuitesCoverage = isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage()
1904
2631
  ? skippableSuitesCoverage
1905
2632
  : {}
@@ -2245,8 +2972,6 @@ function getCliWrapper (isNewJestVersion) {
2245
2972
  isItrEnabled = libraryConfig.isItrEnabled
2246
2973
  isSuitesSkippingEnabled = isItrEnabled && libraryConfig.isSuitesSkippingEnabled
2247
2974
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
2248
- earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
2249
- earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
2250
2975
  earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
2251
2976
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
2252
2977
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
@@ -2298,12 +3023,11 @@ function getCliWrapper (isNewJestVersion) {
2298
3023
  } = skippableSuitesResponse || await getChannelPromise(skippableSuitesCh)
2299
3024
  if (err) {
2300
3025
  skippableSuitesCoverage = {}
2301
- skippedSuitesCoverage = {}
2302
3026
  } else {
2303
3027
  skippableSuites = receivedSkippableSuites
2304
3028
  skippableSuitesCoverage = receivedSkippableSuitesCoverage || {}
2305
- skippedSuitesCoverage = {}
2306
3029
  }
3030
+ skippedSuitesCoverage = {}
2307
3031
  } catch (err) {
2308
3032
  log.error('Jest test-suite skippable error', err)
2309
3033
  }
@@ -2381,15 +3105,24 @@ function getCliWrapper (isNewJestVersion) {
2381
3105
  }
2382
3106
  }
2383
3107
 
2384
- /** @type {{ efdNames: string[], quarantineNames: string[], totalCount: number } | undefined} */
3108
+ /**
3109
+ * @type {{
3110
+ * efdNames: string[],
3111
+ * quarantineNames: string[],
3112
+ * totalCount: number,
3113
+ * efdFailureCount: number
3114
+ * } | undefined}
3115
+ */
2385
3116
  let ignoredFailuresSummary
2386
3117
  if (isEarlyFlakeDetectionEnabled) {
2387
- for (const [testName, testStatuses] of newTestsTestStatuses) {
3118
+ for (const [testName, testStatuses] of efdTestStatuses) {
2388
3119
  const { pass, fail } = getTestStats(testStatuses)
2389
3120
  if (pass > 0) { // as long as one passes, we'll consider the test passed
2390
3121
  numEfdFailedTestsToIgnore += fail
2391
- const suite = fullNameToSuite.get(testName)
2392
- efdIgnoredNames.push(suite ? `${suite} ${testName}` : testName)
3122
+ if (fail > 0) {
3123
+ const suite = fullNameToSuite.get(testName)
3124
+ efdIgnoredNames.push(suite ? `${suite} › ${testName}` : testName)
3125
+ }
2393
3126
  }
2394
3127
  }
2395
3128
  // If every test that failed was an EFD retry, we'll consider the suite passed
@@ -2403,6 +3136,7 @@ function getCliWrapper (isNewJestVersion) {
2403
3136
  efdNames: efdIgnoredNames,
2404
3137
  quarantineNames: [],
2405
3138
  totalCount: numEfdFailedTestsToIgnore,
3139
+ efdFailureCount: numEfdFailedTestsToIgnore,
2406
3140
  }
2407
3141
  }
2408
3142
  }
@@ -2475,6 +3209,7 @@ function getCliWrapper (isNewJestVersion) {
2475
3209
  efdNames: [],
2476
3210
  quarantineNames: quarantineIgnoredNames,
2477
3211
  totalCount: totalQuarantineFailures,
3212
+ efdFailureCount: 0,
2478
3213
  }
2479
3214
  }
2480
3215
  }
@@ -2500,6 +3235,7 @@ function getCliWrapper (isNewJestVersion) {
2500
3235
  efdNames: efdIgnoredNames,
2501
3236
  quarantineNames: quarantineIgnoredNames,
2502
3237
  totalCount: visibleIgnoredFailures + numSuppressedQuarantinedTests,
3238
+ efdFailureCount: numEfdFailedTestsToIgnore,
2503
3239
  }
2504
3240
  }
2505
3241
  }
@@ -2522,7 +3258,13 @@ function getCliWrapper (isNewJestVersion) {
2522
3258
  await getChannelPromise(codeCoverageReportCh, { rootDir })
2523
3259
  }
2524
3260
 
2525
- logSessionSummary(ignoredFailuresSummary, getAttemptToFixExecutionsFromJestResults(result))
3261
+ recordTestManagementExecutionsFromJestResults(result, quarantineIgnoredNames)
3262
+ const allTestsSkipped = isSuitesSkipped && numTotalTests === 0 && numTotalTestSuites === 0
3263
+ logSessionSummary(
3264
+ ignoredFailuresSummary,
3265
+ getAttemptToFixExecutionsFromJestResults(result),
3266
+ allTestsSkipped
3267
+ )
2526
3268
 
2527
3269
  resetSuiteSkippingRunState()
2528
3270
 
@@ -2865,7 +3607,7 @@ const DD_TEST_ENVIRONMENT_OPTION_KEYS = [
2865
3607
  '_ddItrCorrelationId',
2866
3608
  '_ddKnownTests',
2867
3609
  '_ddIsEarlyFlakeDetectionEnabled',
2868
- '_ddEarlyFlakeDetectionSlowTestRetries',
3610
+ '_ddEarlyFlakeDetectionRetryPolicy',
2869
3611
  '_ddRepositoryRoot',
2870
3612
  '_ddTestCodeCoverageEnabled',
2871
3613
  '_ddIsFlakyTestRetriesEnabled',
@@ -3037,9 +3779,7 @@ function wrapJestObject (jestObject, suiteFilePath) {
3037
3779
 
3038
3780
  shimmer.wrap(jestObject, 'mock', mock => function (moduleName) {
3039
3781
  // If the library is mocked with `jest.mock`, we don't want to bypass jest's own require engine
3040
- if (LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.has(moduleName)) {
3041
- LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName)
3042
- }
3782
+ LIBRARIES_BYPASSING_JEST_REQUIRE_ENGINE.delete(moduleName)
3043
3783
  recordMockedFile(suiteFilePath, moduleName)
3044
3784
  return mock.apply(this, arguments)
3045
3785
  })