dd-trace 6.20.1 → 6.22.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 (225) hide show
  1. package/ci/diagnose.js +9 -14
  2. package/ci/init.js +1 -2
  3. package/ci/test-optimization-validation/ci-discovery.js +9 -4
  4. package/ci/test-optimization-validation/executable.js +1 -5
  5. package/ci/test-optimization-validation/framework-adapters/cypress.js +1 -4
  6. package/ci/test-optimization-validation/framework-adapters/vitest.js +1 -1
  7. package/ci/test-optimization-validation/generated-files.js +6 -4
  8. package/ci/test-optimization-validation/generated-test-contract.js +7 -3
  9. package/ci/test-optimization-validation/manifest-scaffold.js +7 -2
  10. package/ci/test-optimization-validation/manifest-schema.js +6 -7
  11. package/ci/test-optimization-validation/report-writer.js +6 -2
  12. package/ci/test-optimization-validation/runner-command.js +2 -4
  13. package/ci/test-optimization-validation/runner-contract.js +1 -2
  14. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +1 -15
  15. package/ci/vitest-no-worker-init-setup.mjs +10 -7
  16. package/index.d.ts +116 -17
  17. package/initialize.mjs +1 -3
  18. package/openfeature/rules-based.d.ts +11 -0
  19. package/openfeature/rules-based.js +7 -0
  20. package/package.json +13 -11
  21. package/packages/datadog-core/src/storage.js +16 -0
  22. package/packages/datadog-esbuild/index.js +18 -12
  23. package/packages/datadog-instrumentations/src/aerospike.js +3 -3
  24. package/packages/datadog-instrumentations/src/ai.js +3 -3
  25. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -6
  26. package/packages/datadog-instrumentations/src/aws-sdk.js +21 -13
  27. package/packages/datadog-instrumentations/src/cassandra-driver.js +25 -20
  28. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +9 -23
  29. package/packages/datadog-instrumentations/src/cucumber.js +48 -33
  30. package/packages/datadog-instrumentations/src/elasticsearch.js +1 -5
  31. package/packages/datadog-instrumentations/src/electron.js +6 -6
  32. package/packages/datadog-instrumentations/src/fastify.js +13 -4
  33. package/packages/datadog-instrumentations/src/fs.js +2 -2
  34. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +9 -5
  35. package/packages/datadog-instrumentations/src/grpc/client.js +2 -0
  36. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +2 -2
  37. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +1 -2
  38. package/packages/datadog-instrumentations/src/helpers/instrument.js +1 -1
  39. package/packages/datadog-instrumentations/src/helpers/register.js +36 -1
  40. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -0
  41. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +87 -6
  42. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +5 -6
  43. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +48 -31
  44. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +64 -11
  45. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  46. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +42 -4
  47. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +7 -10
  48. package/packages/datadog-instrumentations/src/ioredis.js +7 -5
  49. package/packages/datadog-instrumentations/src/iovalkey.js +7 -5
  50. package/packages/datadog-instrumentations/src/jest.js +122 -11
  51. package/packages/datadog-instrumentations/src/knex.js +9 -5
  52. package/packages/datadog-instrumentations/src/mariadb-bundle.js +3 -3
  53. package/packages/datadog-instrumentations/src/mocha/common.js +20 -8
  54. package/packages/datadog-instrumentations/src/mocha/main.js +140 -43
  55. package/packages/datadog-instrumentations/src/mocha/utils.js +127 -38
  56. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +61 -34
  57. package/packages/datadog-instrumentations/src/mocha/worker.js +7 -5
  58. package/packages/datadog-instrumentations/src/mongodb-core.js +3 -3
  59. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  60. package/packages/datadog-instrumentations/src/nova-sonic/audio.js +173 -0
  61. package/packages/datadog-instrumentations/src/nova-sonic/index.js +209 -0
  62. package/packages/datadog-instrumentations/src/nova-sonic/session.js +483 -0
  63. package/packages/datadog-instrumentations/src/openai-realtime/index.js +18 -6
  64. package/packages/datadog-instrumentations/src/openai-realtime/session.js +1 -1
  65. package/packages/datadog-instrumentations/src/openai.js +7 -0
  66. package/packages/datadog-instrumentations/src/pino.js +5 -6
  67. package/packages/datadog-instrumentations/src/playwright.js +3 -0
  68. package/packages/datadog-instrumentations/src/redis.js +27 -13
  69. package/packages/datadog-instrumentations/src/router.js +30 -0
  70. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +2 -6
  71. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -5
  72. package/packages/datadog-instrumentations/src/vitest-main.js +9 -17
  73. package/packages/datadog-instrumentations/src/vitest-util.js +2 -2
  74. package/packages/datadog-instrumentations/src/vitest-worker.js +1 -2
  75. package/packages/datadog-instrumentations/src/vitest.js +2 -2
  76. package/packages/datadog-instrumentations/src/webdriverio.js +118 -21
  77. package/packages/datadog-instrumentations/src/ws.js +1 -4
  78. package/packages/datadog-plugin-amqplib/src/client.js +1 -2
  79. package/packages/datadog-plugin-avsc/src/schema_iterator.js +1 -5
  80. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -2
  81. package/packages/datadog-plugin-aws-sdk/src/base.js +5 -8
  82. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +18 -0
  83. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/sonic.js +45 -0
  84. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  85. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +2 -5
  86. package/packages/datadog-plugin-azure-durable-functions/src/index.js +5 -4
  87. package/packages/datadog-plugin-azure-functions/src/index.js +5 -3
  88. package/packages/datadog-plugin-claude-agent-sdk/src/index.js +0 -2
  89. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -5
  90. package/packages/datadog-plugin-cypress/src/index.js +6 -6
  91. package/packages/datadog-plugin-cypress/src/source-map-utils.js +1 -2
  92. package/packages/datadog-plugin-cypress/src/support.js +1 -4
  93. package/packages/datadog-plugin-express/src/code_origin.js +1 -2
  94. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -2
  95. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +1 -2
  96. package/packages/datadog-plugin-graphql/src/execute.js +5 -5
  97. package/packages/datadog-plugin-grpc/src/util.js +1 -2
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +1 -2
  99. package/packages/datadog-plugin-mocha/src/index.js +223 -39
  100. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +453 -0
  101. package/packages/datadog-plugin-mongodb-core/src/query.js +8 -5
  102. package/packages/datadog-plugin-next/src/index.js +1 -2
  103. package/packages/datadog-plugin-openai/src/tracing.js +5 -2
  104. package/packages/datadog-plugin-openai-agents/src/integration.js +6 -2
  105. package/packages/datadog-plugin-openai-agents/src/processor.js +2 -4
  106. package/packages/datadog-plugin-postgres/src/index.js +24 -0
  107. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +1 -5
  108. package/packages/datadog-plugin-rhea/src/consumer.js +1 -4
  109. package/packages/datadog-plugin-vitest/src/index.js +5 -2
  110. package/packages/datadog-shimmer/src/shimmer.js +5 -4
  111. package/packages/datadog-webpack/index.js +55 -43
  112. package/packages/datadog-webpack/src/loader.js +56 -8
  113. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -4
  114. package/packages/dd-trace/src/aiguard/messages/utils.js +1 -2
  115. package/packages/dd-trace/src/aiguard/redaction.js +8 -2
  116. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +3 -2
  117. package/packages/dd-trace/src/appsec/api_security/sampler.js +2 -6
  118. package/packages/dd-trace/src/appsec/downstream_requests.js +1 -9
  119. package/packages/dd-trace/src/appsec/handlers/http-request.js +3 -9
  120. package/packages/dd-trace/src/appsec/handlers/http-response.js +7 -2
  121. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +4 -3
  122. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +1 -4
  123. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +7 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -3
  125. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -3
  126. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +1 -6
  127. package/packages/dd-trace/src/appsec/lambda.js +1 -3
  128. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +37 -27
  129. package/packages/dd-trace/src/appsec/rasp/lfi.js +9 -5
  130. package/packages/dd-trace/src/appsec/reporter.js +21 -1
  131. package/packages/dd-trace/src/appsec/stack_trace.js +1 -2
  132. package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +3 -6
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +1 -1
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +13 -7
  135. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +1 -2
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +2 -8
  137. package/packages/dd-trace/src/ci-visibility/resolve-runfile-path.js +54 -0
  138. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +5 -8
  139. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +2 -53
  140. package/packages/dd-trace/src/config/config-types.d.ts +2 -2
  141. package/packages/dd-trace/src/config/defaults.js +5 -5
  142. package/packages/dd-trace/src/config/generated-config-types.d.ts +8 -0
  143. package/packages/dd-trace/src/config/helper.js +34 -3
  144. package/packages/dd-trace/src/config/index.js +10 -9
  145. package/packages/dd-trace/src/config/normalize-service.js +1 -1
  146. package/packages/dd-trace/src/config/parsers.js +2 -2
  147. package/packages/dd-trace/src/config/supported-configurations.json +35 -0
  148. package/packages/dd-trace/src/datastreams/processor.js +3 -2
  149. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +1 -4
  150. package/packages/dd-trace/src/debugger/constants.js +1 -0
  151. package/packages/dd-trace/src/debugger/devtools_client/index.js +5 -3
  152. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +17 -12
  153. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -2
  154. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -0
  155. package/packages/dd-trace/src/debugger/devtools_client/worker-error.js +21 -0
  156. package/packages/dd-trace/src/debugger/guardrail-metrics.js +2 -2
  157. package/packages/dd-trace/src/debugger/index.js +24 -2
  158. package/packages/dd-trace/src/debugger/probe_sampler.js +108 -11
  159. package/packages/dd-trace/src/dogstatsd.js +1 -1
  160. package/packages/dd-trace/src/encode/tags-processors.js +2 -2
  161. package/packages/dd-trace/src/exporters/common/request.js +2 -5
  162. package/packages/dd-trace/src/exporters/common/url.js +1 -1
  163. package/packages/dd-trace/src/id.js +15 -5
  164. package/packages/dd-trace/src/lambda/handler.js +179 -19
  165. package/packages/dd-trace/src/lambda/index.js +15 -1
  166. package/packages/dd-trace/src/lambda/runtime/patch.js +34 -7
  167. package/packages/dd-trace/src/llmobs/agent-manifest.js +296 -0
  168. package/packages/dd-trace/src/llmobs/audio-utils.js +3 -2
  169. package/packages/dd-trace/src/llmobs/constants/tags.js +13 -0
  170. package/packages/dd-trace/src/llmobs/experiments/experiment.js +1 -1
  171. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +15 -0
  172. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +1 -2
  173. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +1 -3
  174. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  175. package/packages/dd-trace/src/llmobs/plugins/nova-sonic.js +92 -0
  176. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +56 -2
  177. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +38 -0
  178. package/packages/dd-trace/src/llmobs/prompts/manager.js +5 -7
  179. package/packages/dd-trace/src/llmobs/prompts/prompt.js +1 -1
  180. package/packages/dd-trace/src/llmobs/sdk.js +23 -6
  181. package/packages/dd-trace/src/llmobs/span_processor.js +18 -8
  182. package/packages/dd-trace/src/llmobs/tagger.js +56 -3
  183. package/packages/dd-trace/src/llmobs/util.js +1 -2
  184. package/packages/dd-trace/src/llmobs/writers/base.js +7 -6
  185. package/packages/dd-trace/src/llmobs/writers/util.js +3 -1
  186. package/packages/dd-trace/src/log/log.js +1 -5
  187. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +30 -1
  188. package/packages/dd-trace/src/openfeature/configuration_source.js +17 -2
  189. package/packages/dd-trace/src/openfeature/debug-logging-hook.js +25 -0
  190. package/packages/dd-trace/src/openfeature/evaluation-telemetry-opt-out-hook.js +51 -0
  191. package/packages/dd-trace/src/openfeature/flagging_provider.js +59 -3
  192. package/packages/dd-trace/src/openfeature/remote_config.js +9 -0
  193. package/packages/dd-trace/src/openfeature/writers/util.js +43 -4
  194. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +1 -1
  195. package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +1 -1
  196. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -2
  197. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +1 -1
  198. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +1 -1
  199. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -23
  200. package/packages/dd-trace/src/opentracing/propagation/text_map.js +53 -13
  201. package/packages/dd-trace/src/otel-sampling.js +4 -5
  202. package/packages/dd-trace/src/otel-thread-ctx.js +81 -15
  203. package/packages/dd-trace/src/plugins/database.js +7 -16
  204. package/packages/dd-trace/src/plugins/util/git.js +9 -25
  205. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -5
  206. package/packages/dd-trace/src/plugins/util/test.js +48 -4
  207. package/packages/dd-trace/src/priority_sampler.js +1 -2
  208. package/packages/dd-trace/src/profiling/profiler.js +4 -6
  209. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -10
  210. package/packages/dd-trace/src/proxy.js +3 -2
  211. package/packages/dd-trace/src/ritm.js +4 -2
  212. package/packages/dd-trace/src/serverless.js +10 -0
  213. package/packages/dd-trace/src/span_processor.js +13 -17
  214. package/packages/dd-trace/src/storage-channels.js +112 -46
  215. package/packages/dd-trace/src/telemetry/endpoints.js +1 -3
  216. package/packages/dd-trace/src/util.js +29 -0
  217. package/packages/dd-trace/src/web-tags-cache.js +6 -2
  218. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  219. package/vendor/dist/@datadog/openfeature-node-server/rules-based/LICENSE +201 -0
  220. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.d.ts +925 -0
  221. package/vendor/dist/@datadog/openfeature-node-server/rules-based/index.js +1 -0
  222. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  223. package/vendor/dist/protobufjs/index.js +1 -1
  224. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  225. package/vendor/dist/shell-quote/index.js +1 -1
@@ -1,6 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { AsyncResource } = require('node:async_hooks')
4
+ const { realpathSync } = require('node:fs')
5
+ const { resolve, sep } = require('node:path')
6
+ const { fileURLToPath } = require('node:url')
4
7
 
5
8
  const { createCoverageMap } = require('../../../../vendor/dist/istanbul-lib-coverage')
6
9
  const satisfies = require('../../../../vendor/dist/semifies')
@@ -16,6 +19,7 @@ const { EMPTY_EFD_RETRY_POLICY } = require('../../../dd-trace/src/ci-visibility/
16
19
  const { writeCoverageBackfillToCache } = require('../../../dd-trace/src/ci-visibility/test-optimization-cache')
17
20
  const log = require('../../../dd-trace/src/log')
18
21
  const { getEnvironmentVariable } = require('../../../dd-trace/src/config/helper')
22
+ const { getCallSites } = require('../../../dd-trace/src/plugins/util/stacktrace')
19
23
  const {
20
24
  getTestSuitePath,
21
25
  MOCHA_WORKER_TELEMETRY_PAYLOAD_CODE,
@@ -88,6 +92,9 @@ const runnerRecoveryStates = new WeakMap()
88
92
  const runnersWithPendingCoverageReset = new WeakSet()
89
93
  const parallelRunners = new WeakSet()
90
94
  const wrappedRunnerEmitPrototypes = new WeakSet()
95
+ const rootSuitesWithHookFiles = new WeakSet()
96
+ const ROOT_HOOK_NAMES = ['_beforeAll', '_afterAll', '_beforeEach', '_afterEach']
97
+ const ROOT_SUITE_ARRAY_NAMES = ['tests', 'suites', '_onlyTests', '_onlySuites', ...ROOT_HOOK_NAMES]
91
98
  let hasWarnedDeprecatedMochaVersion = false
92
99
 
93
100
  const unskippableSuites = []
@@ -169,6 +176,40 @@ function haveRootTestsFinished (rootTests) {
169
176
  return true
170
177
  }
171
178
 
179
+ /**
180
+ * @param {{ file: string }} test
181
+ */
182
+ function startRootSuite (test) {
183
+ const ctx = testFileToSuiteCtx.get(test.file)
184
+ if (ctx?._pendingRootStart) {
185
+ ctx._pendingRootStart = false
186
+ testSuiteStartCh.runStores(ctx, () => {})
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {string} file
192
+ * @param {'pass' | 'fail' | 'skip'} status
193
+ * @param {object} [runner] Defers coverage reset until this runner's suite-end reporters finish.
194
+ */
195
+ function finishRootSuite (file, status, runner) {
196
+ const ctx = testFileToSuiteCtx.get(file)
197
+ if (!ctx) {
198
+ log.warn('No ctx found for suite', file)
199
+ return
200
+ }
201
+
202
+ if (global.__coverage__) {
203
+ const coverageFiles = getCoveredFilesFromCoverage(global.__coverage__)
204
+ testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
205
+ mergeCoverage(global.__coverage__, originalCoverageMap)
206
+ if (runner) runnersWithPendingCoverageReset.add(runner)
207
+ else resetCoverage(global.__coverage__)
208
+ }
209
+
210
+ testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
211
+ }
212
+
172
213
  function getSuitePath (suite) {
173
214
  return getTestSuitePath(suite.file, process.cwd())
174
215
  }
@@ -931,6 +972,23 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
931
972
  return
932
973
  }
933
974
 
975
+ if (runner._opts?.cleanReferencesAfterRun === false) {
976
+ const rootSuite = runner.suite
977
+ const originalArrays = ROOT_SUITE_ARRAY_NAMES.map(name => rootSuite[name])
978
+ // Restore definitions before completion callbacks can start another run, including with the plugin disabled.
979
+ runner.prependOnceListener('end', () => {
980
+ for (let index = 0; index < ROOT_SUITE_ARRAY_NAMES.length; index++) {
981
+ rootSuite[ROOT_SUITE_ARRAY_NAMES[index]] = originalArrays[index]
982
+ }
983
+ })
984
+ }
985
+
986
+ // Root hooks must be selected before .only() removes other files from the test tree.
987
+ for (const name of ROOT_HOOK_NAMES) {
988
+ runner.suite[name] = runner.suite[name].filter(hook => !hook.file ||
989
+ !suitesToSkip.includes(getSuitePath(hook)) || unskippableSuites.includes(hook.file))
990
+ }
991
+
934
992
  // We remove the suites that we skip through ITR
935
993
  // Mocha normally applies exclusivity after this asynchronous configuration step.
936
994
  if (typeof runner.suite.hasOnly === 'function') {
@@ -940,17 +998,28 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
940
998
  }
941
999
  const numTestsToRun = runner.grepTotal(runner.suite)
942
1000
  const filteredSuites = getFilteredSuites(runner.suite.suites)
1001
+ // Root tests share a Mocha suite across files, but TIA still selects them by file.
1002
+ const filteredRootTests = getFilteredSuites(runner.suite.tests)
943
1003
  const { suitesToRun, suitesToSkipForRun } = filteredSuites
944
1004
 
945
- isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length
1005
+ isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length ||
1006
+ filteredRootTests.suitesToRun.length !== runner.suite.tests.length
946
1007
 
947
1008
  log.debug('%d out of %d suites are going to run.', suitesToRun.length, runner.suite.suites.length)
948
1009
 
949
1010
  runner.suite.suites = suitesToRun
1011
+ runner.suite.tests = filteredRootTests.suitesToRun
1012
+ // Mocha reapplies exclusivity when the delayed runner resumes.
1013
+ if (runner.suite._onlyTests.length) {
1014
+ runner.suite._onlyTests = runner.suite.tests
1015
+ }
1016
+ if (runner.suite._onlySuites.length) {
1017
+ runner.suite._onlySuites = runner.suite._onlySuites.filter(suite => suitesToRun.includes(suite))
1018
+ }
950
1019
  areAllSuitesSkipped = isSuitesSkipped && numTestsToRun > 0 && runner.grepTotal(runner.suite) === 0
951
1020
 
952
- skippedSuites = [...filteredSuites.skippedSuites]
953
- suitesToSkip = suitesToSkipForRun
1021
+ skippedSuites = [...new Set([...filteredSuites.skippedSuites, ...filteredRootTests.skippedSuites])]
1022
+ suitesToSkip = [...new Set([...suitesToSkipForRun, ...filteredRootTests.suitesToSkipForRun])]
954
1023
  skippedSuitesCoverage = getSkippedSuitesCoverageForRun()
955
1024
  writeCoverageBackfillToCache(skippedSuitesCoverage, getCoverageRootDir())
956
1025
 
@@ -1095,11 +1164,51 @@ function runMochaRunner (run, runner, args) {
1095
1164
  /**
1096
1165
  * @param {Function} Mocha
1097
1166
  * @param {string} frameworkVersion
1167
+ * @param {import('./common').MochaHookMeta} hookMeta
1098
1168
  * @returns {Function}
1099
1169
  */
1100
- function wrapMochaRun (Mocha, frameworkVersion) {
1170
+ function wrapMochaRun (Mocha, frameworkVersion, hookMeta) {
1101
1171
  warnDeprecatedMochaVersion(frameworkVersion)
1102
1172
 
1173
+ // Shimmer is required to attach listeners to the root suite created by each Mocha instance.
1174
+ shimmer.wrap(Mocha.prototype, 'ui', ui => function (...args) {
1175
+ const result = ui.apply(this, args)
1176
+ const suite = this.suite
1177
+ if (!rootSuitesWithHookFiles.has(suite)) {
1178
+ rootSuitesWithHookFiles.add(suite)
1179
+ let file
1180
+ suite.prependListener('pre-require', (context, testFile) => {
1181
+ file = testFile
1182
+ })
1183
+ suite.on('post-require', () => {
1184
+ file = undefined
1185
+ })
1186
+ // Hook objects are created at runtime; inspect the caller while registration is still on the stack.
1187
+ shimmer.wrap(suite, '_createHook', createHook => function createHookWithFile (...args) {
1188
+ const hook = createHook.apply(this, args)
1189
+ if (file && !hook.file) {
1190
+ try {
1191
+ const { moduleBaseDir } = hookMeta
1192
+ const mochaPathPrefix = moduleBaseDir && moduleBaseDir + sep
1193
+ for (const site of getCallSites(createHookWithFile)) {
1194
+ const source = site.getFileName()
1195
+ if (!source || source.startsWith('node:')) continue
1196
+ const sourceFile = source.startsWith('file:') ? fileURLToPath(source) : source
1197
+ if (mochaPathPrefix && sourceFile.startsWith(mochaPathPrefix)) continue
1198
+ // Helpers register shared setup; only direct declarations belong to the loading test file.
1199
+ if (sourceFile === file || realpathSync(sourceFile) === realpathSync(file)) hook.file = file
1200
+ break
1201
+ }
1202
+ } catch (error) {
1203
+ log.debug('Error capturing Mocha root hook source: %s', error)
1204
+ }
1205
+ }
1206
+ return hook
1207
+ })
1208
+ }
1209
+ return result
1210
+ })
1211
+
1103
1212
  // Shimmer is required because run must return its Runner while execution is paused and resumed after configuration.
1104
1213
  shimmer.wrap(Mocha.prototype, 'run', run => function (...args) {
1105
1214
  // Workers do not need to request any data, just run the tests
@@ -1123,7 +1232,7 @@ function wrapMochaRun (Mocha, frameworkVersion) {
1123
1232
  this.files.forEach((path) => {
1124
1233
  const isUnskippable = isMarkedAsUnskippable({ path })
1125
1234
  if (isUnskippable) {
1126
- unskippableSuites.push(path)
1235
+ unskippableSuites.push(resolve(path))
1127
1236
  }
1128
1237
  })
1129
1238
 
@@ -1200,8 +1309,7 @@ addHook({
1200
1309
  filePattern: String.raw`lib/runner\.(?:c?js)$`,
1201
1310
  }, function (runnerPackage, frameworkVersion) {
1202
1311
  const Runner = runnerPackage.Runner ?? runnerPackage.default ?? runnerPackage
1203
- if (typeof Runner !== 'function') return
1204
- if (patched.has(Runner)) return
1312
+ if (typeof Runner !== 'function' || patched.has(Runner)) return
1205
1313
 
1206
1314
  patched.add(Runner)
1207
1315
  wrapRunnerEmit(Runner)
@@ -1226,12 +1334,12 @@ addHook({
1226
1334
  runnerFrameworkErrors.delete(this)
1227
1335
  }
1228
1336
 
1229
- const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
1337
+ let { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
1230
1338
  // Root-level tests (direct children of root, no describe wrapper) keyed by file.
1231
1339
  // Populated during the root 'suite' event so the normal finish path can include them
1232
1340
  // in mixed-file status calculation.
1233
1341
  const rootTestsByFile = new Map()
1234
- // Counts how many original tests per pure-root file still need their final attempt.
1342
+ // Counts how many selected tests per pure-root file still need their final attempt.
1235
1343
  // Hits zero when the last test's lifecycle completes, triggering the suite finish.
1236
1344
  const rootPendingCountByFile = new Map()
1237
1345
  const rootFinalizationPendingCountByFile = new Map()
@@ -1304,33 +1412,18 @@ addHook({
1304
1412
  }
1305
1413
  rootPendingCountByFile.delete(file)
1306
1414
 
1307
- const ctx = testFileToSuiteCtx.get(file)
1308
- if (!ctx) {
1309
- log.warn('No ctx found for suite', file)
1310
- return
1311
- }
1312
-
1313
1415
  const rootTests = rootTestsByFile.get(file) || []
1314
- const status = getRootSuiteStatus(rootTests)
1315
-
1316
- if (global.__coverage__) {
1317
- const coverageFiles = getCoveredFilesFromCoverage(global.__coverage__)
1318
- testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
1319
- mergeCoverage(global.__coverage__, originalCoverageMap)
1320
- resetCoverage(global.__coverage__)
1321
- }
1322
-
1323
- testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
1416
+ finishRootSuite(file, getRootSuiteStatus(rootTests))
1324
1417
  }
1325
1418
 
1326
- function finishRootSuiteFallbackForFile (file) {
1419
+ function finishRootSuiteFallbackForFile (file, runner) {
1327
1420
  const ctx = testFileToSuiteCtx.get(file)
1328
1421
  if (!ctx || !rootPendingCountByFile.has(file)) return
1329
1422
 
1330
1423
  const rootTests = rootTestsByFile.get(file) || []
1331
1424
  const status = haveRootTestsFinished(rootTests) ? getRootSuiteStatus(rootTests) : 'fail'
1332
1425
  rootPendingCountByFile.delete(file)
1333
- testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
1426
+ finishRootSuite(file, status, runner)
1334
1427
  }
1335
1428
 
1336
1429
  function finishRootSuiteAfterFinalAttempt (test) {
@@ -1361,13 +1454,7 @@ addHook({
1361
1454
  this.prependListener('test', getOnTestHandler(true))
1362
1455
  // Reporters are registered before Runner#run, so prepend this after the test handler
1363
1456
  // to start the suite first and preserve the test event if a reporter throws.
1364
- this.prependListener('test', function (test) {
1365
- const ctx = testFileToSuiteCtx.get(test.file)
1366
- if (ctx?._pendingRootStart) {
1367
- ctx._pendingRootStart = false
1368
- testSuiteStartCh.runStores(ctx, () => {})
1369
- }
1370
- })
1457
+ this.prependListener('test', startRootSuite)
1371
1458
 
1372
1459
  // Reporters are registered before this run wrapper. Prepend terminal handlers
1373
1460
  // so a reporter error cannot strand a test span that Mocha already completed.
@@ -1407,8 +1494,16 @@ addHook({
1407
1494
  this.prependListener('fail', getOnFailHandler(true, config))
1408
1495
 
1409
1496
  this.prependListener('pending', getOnPendingHandler())
1497
+ // Static skips emit 'pending' without 'test', so their suite must start here too.
1498
+ this.prependListener('pending', startRootSuite)
1410
1499
 
1411
1500
  this.prependListener('suite', function (suite) {
1501
+ if (suite.root) {
1502
+ // TIA and .only() have now selected the suites that can finish during this run.
1503
+ const selectedSuites = getSuitesByTestFile(suite)
1504
+ suitesByTestFile = selectedSuites.suitesByTestFile
1505
+ numSuitesByTestFile = selectedSuites.numSuitesByTestFile
1506
+ }
1412
1507
  if (suite.root || !suite.tests.length) {
1413
1508
  // This branch can be triggered when we have top level it(...) inside test files.
1414
1509
  // In that case, they all (even if they are from different files) are going to be
@@ -1418,9 +1513,14 @@ addHook({
1418
1513
  // below. Since 'suite' fires for root first, in the mixed case the ctx is created
1419
1514
  // here and the describe-based handler finds it already set.
1420
1515
  if (suite.root && suite.tests.length > 0) {
1516
+ const { _grep: grep, _invert: invert } = this
1517
+ const lastIndex = grep.lastIndex
1518
+ const selectedTests = suite.tests.filter(test => grep.test(test.fullTitle()) !== !!invert)
1519
+ // Inspecting a stateful grep must not change which tests Mocha subsequently selects.
1520
+ if (grep.lastIndex !== lastIndex) grep.lastIndex = lastIndex
1421
1521
  const files = new Set(suite.tests.map(test => test.file).filter(Boolean))
1422
1522
  for (const file of files) {
1423
- const testsForFile = suite.tests.filter(t => t.file === file)
1523
+ const testsForFile = selectedTests.filter(t => t.file === file)
1424
1524
  rootTestsByFile.set(file, testsForFile)
1425
1525
  // Only track the countdown for pure root-level files.
1426
1526
  // Mixed files are finished by the normal 'suite end' path.
@@ -1461,10 +1561,10 @@ addHook({
1461
1561
  this.prependListener('suite end', function (suite) {
1462
1562
  if (suite.root) {
1463
1563
  // Normal case: pure root-level files are finished by the 'test end' / 'hook end'
1464
- // listeners via finishRootSuiteForFile. Two edge cases remain here:
1564
+ // listeners via finishRootSuiteForFile. Fallback cases remain here:
1465
1565
  //
1466
- // 1. All-pending: no 'test' event fired, _pendingRootStart is still true.
1467
- // Start and immediately finish with 'skip'.
1566
+ // 1. Unstarted file: no 'test' or 'pending' event fired (e.g. all tests
1567
+ // were filtered out). Start and immediately finish with 'skip'.
1468
1568
  //
1469
1569
  // 2. Aborted mid-run (e.g. a beforeEach hook failure): Mocha skips remaining
1470
1570
  // tests and jumps straight to 'suite end'. rootPendingCountByFile still has
@@ -1491,7 +1591,7 @@ addHook({
1491
1591
  continue
1492
1592
  }
1493
1593
 
1494
- finishRootSuiteFallbackForFile(test.file)
1594
+ finishRootSuiteFallbackForFile(test.file, this)
1495
1595
  }
1496
1596
  }
1497
1597
  return
@@ -1584,10 +1684,7 @@ addHook({
1584
1684
  file: 'src/WorkerHandler.js',
1585
1685
  }, (workerHandlerPackage) => {
1586
1686
  shimmer.wrap(workerHandlerPackage.prototype, 'exec', exec => function (_, path) {
1587
- if (!testFinishCh.hasSubscribers) {
1588
- return exec.apply(this, arguments)
1589
- }
1590
- if (!path?.length) {
1687
+ if (!testFinishCh.hasSubscribers || !path?.length) {
1591
1688
  return exec.apply(this, arguments)
1592
1689
  }
1593
1690
  const [testSuiteAbsolutePath] = path
@@ -26,6 +26,7 @@ const shimmer = require('../../../datadog-shimmer')
26
26
  const testStartCh = channel('ci:mocha:test:start')
27
27
  const testFinishCh = channel('ci:mocha:test:finish')
28
28
  const testDiWaitCh = channel('ci:mocha:test:di:wait')
29
+ const testAfterEachFinishCh = channel('ci:mocha:after-each:finish')
29
30
  // after a test has failed, we'll publish to this channel
30
31
  const testRetryCh = channel('ci:mocha:test:retry')
31
32
  const errorCh = channel('ci:mocha:test:error')
@@ -54,6 +55,7 @@ const testsQuarantined = new Set()
54
55
  const testsStatuses = new Map()
55
56
  const efdRetryCountByTestFullName = new Map()
56
57
  const efdSlowAbortedTests = new Set()
58
+ const suiteHookFailureSuites = new Set()
57
59
  let dynamicAtrRetryCountByTest = new WeakMap()
58
60
  const attemptToFixExecutions = new Map()
59
61
  const isMochaWorker = !!getEnvironmentVariable('MOCHA_WORKER_ID')
@@ -343,6 +345,7 @@ function resetRunState (rootSuite) {
343
345
  testsStatuses.clear()
344
346
  efdRetryCountByTestFullName.clear()
345
347
  efdSlowAbortedTests.clear()
348
+ suiteHookFailureSuites.clear()
346
349
  dynamicAtrRetryCountByTest = new WeakMap()
347
350
  attemptToFixExecutions.clear()
348
351
  loggedAttemptToFixTests.clear()
@@ -379,7 +382,9 @@ function resetRunState (rootSuite) {
379
382
  delete test._ddTestFinishPublished
380
383
  delete test._ddIsFinalAttempt
381
384
  delete test._ddHookFailed
385
+ delete test._ddSuppressSyntheticTestLifecycle
382
386
  delete test._ddPendingRetry
387
+ delete test._ddPendingHookFailureFinish
383
388
  delete test._ddReporterStartFailed
384
389
  delete test._ddReporterTerminalFailed
385
390
  originalTests.push(test)
@@ -439,6 +444,21 @@ function recordTestAttempt (testsByFullName, test) {
439
444
  }
440
445
  }
441
446
 
447
+ function recordRetryTrackedAttempt (test) {
448
+ const {
449
+ _ddIsAttemptToFix: isAttemptToFix,
450
+ _ddIsModified: isModified,
451
+ _ddIsNew: isNew,
452
+ } = test
453
+
454
+ if (isNew) {
455
+ recordTestAttempt(newTests, test)
456
+ }
457
+ if (!isAttemptToFix && (isNew || isModified)) {
458
+ recordTestAttempt(efdTests, test)
459
+ }
460
+ }
461
+
442
462
  function getTestStatus (test) {
443
463
  if (test.isPending()) {
444
464
  return 'skip'
@@ -476,6 +496,15 @@ function startTestFinish (test) {
476
496
  return true
477
497
  }
478
498
 
499
+ function hasHandledHookFailure (test) {
500
+ return test._ddHookFailed && (test._ddTestFinishPublished || test._ddPendingHookFailureFinish)
501
+ }
502
+
503
+ function suppressSyntheticTestLifecycle (test) {
504
+ test._ddSuppressSyntheticTestLifecycle = true
505
+ test._ddTestFinishPublished = true
506
+ }
507
+
479
508
  /**
480
509
  * Copies Test Management metadata from Mocha's original runnable to its native retry clone.
481
510
  * @param {{
@@ -571,6 +600,14 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
571
600
  }
572
601
  }
573
602
 
603
+ const failedTestReplayPromise = (isTestHook || this.type === 'test') &&
604
+ test._retriedTest?._ddFailedTestReplayPromise
605
+ if (failedTestReplayPromise) {
606
+ delete test._retriedTest._ddFailedTestReplayPromise
607
+ const runRunnable = () => run.apply(this, args)
608
+ return failedTestReplayPromise.then(runRunnable, runRunnable)
609
+ }
610
+
574
611
  return run.apply(this, args)
575
612
  })
576
613
  return RunnablePackage
@@ -578,6 +615,8 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
578
615
 
579
616
  function getOnTestHandler (isMain) {
580
617
  return function (test) {
618
+ if (test._ddSuppressSyntheticTestLifecycle || hasHandledHookFailure(test)) return
619
+
581
620
  const testStartLine = testToStartLine.get(test)
582
621
 
583
622
  // This may be a retry. If this is the case, `test.fn` is already wrapped,
@@ -586,6 +625,12 @@ function getOnTestHandler (isMain) {
586
625
 
587
626
  inheritDatadogPropertiesFromRetriedTest(test)
588
627
 
628
+ if (isInFailedSuite(test.parent)) {
629
+ recordRetryTrackedAttempt(test)
630
+ suppressSyntheticTestLifecycle(test)
631
+ return
632
+ }
633
+
589
634
  const {
590
635
  file: testSuiteAbsolutePath,
591
636
  title,
@@ -636,13 +681,8 @@ function getOnTestHandler (isMain) {
636
681
  loggedAttemptToFixTests
637
682
  )
638
683
  }
639
- // We want to store the result of the new tests
640
- if (isNew) {
641
- recordTestAttempt(newTests, test)
642
- }
643
- if (!isAttemptToFix && (isNew || isModified)) {
644
- recordTestAttempt(efdTests, test)
645
- }
684
+ // Keep retry accounting aligned with Mocha's runner failure counts.
685
+ recordRetryTrackedAttempt(test)
646
686
 
647
687
  if (!isAttemptToFix && isDisabled) {
648
688
  if (!originalPendingByTest.has(test)) {
@@ -657,6 +697,14 @@ function getOnTestHandler (isMain) {
657
697
  }
658
698
  }
659
699
 
700
+ function isInFailedSuite (suite) {
701
+ while (suite) {
702
+ if (suiteHookFailureSuites.has(suite)) return true
703
+ suite = suite.parent
704
+ }
705
+ return false
706
+ }
707
+
660
708
  function getFinalStatus ({
661
709
  status,
662
710
  hasFailedAllRetries,
@@ -842,7 +890,7 @@ function getTestFinishInfo (test, status, config, error) {
842
890
 
843
891
  function getOnTestEndHandler (config, finalAttemptHandlers) {
844
892
  return async function (test) {
845
- if (test._ddShouldSkipEfdRetry) {
893
+ if (test._ddShouldSkipEfdRetry || test._ddSuppressSyntheticTestLifecycle || hasHandledHookFailure(test)) {
846
894
  return
847
895
  }
848
896
  const shouldWaitForHitProbe = test._retriedTest?._ddShouldWaitForHitProbe
@@ -851,6 +899,7 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
851
899
  const shouldFinishTest = ctx && (
852
900
  test._ddReporterStartFailed ||
853
901
  test._ddReporterTerminalFailed ||
902
+ !test.fn ||
854
903
  !getAfterEachHooks(test).length ||
855
904
  (test._ddIsDisabled && !test._ddIsAttemptToFix)
856
905
  )
@@ -858,8 +907,8 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
858
907
  let testFinishInfo
859
908
  let isFinalAttempt = false
860
909
 
861
- // If there are afterEach to be run, we don't finish the test yet.
862
- // Disabled tests (marked pending by us) are finished immediately without waiting for afterEach hooks.
910
+ // If there are afterEach hooks to run, we don't finish the test yet.
911
+ // Static skips have no function and never execute hooks. Disabled tests also finish immediately.
863
912
  // In older mocha versions, pending tests don't run afterEach hooks, so we can't rely on
864
913
  // getOnHookEndHandler to finish the test. This mirrors Jest's approach where the skip handler
865
914
  // directly sets finalStatus without waiting for hooks
@@ -880,24 +929,33 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
880
929
  finalAttemptHandlers?.onStart?.(test)
881
930
  }
882
931
 
883
- if (shouldWaitForHitProbe) {
884
- await waitForHitProbe()
885
- }
932
+ const finish = () => {
933
+ if (shouldFinishTest) {
934
+ test._ddTestFinishPublished = true
935
+ testFinishCh.publish({
936
+ status,
937
+ hasBeenRetried: isMochaRetry(test),
938
+ isLastRetry: getIsLastRetry(test),
939
+ ...testFinishInfo,
940
+ ...ctx.currentStore,
941
+ })
942
+ }
886
943
 
887
- if (shouldFinishTest) {
888
- test._ddTestFinishPublished = true
889
- testFinishCh.publish({
890
- status,
891
- hasBeenRetried: isMochaRetry(test),
892
- isLastRetry: getIsLastRetry(test),
893
- ...testFinishInfo,
894
- ...ctx.currentStore,
895
- })
944
+ if (isFinalAttempt) {
945
+ finalAttemptHandlers?.onFinish?.(test)
946
+ }
896
947
  }
897
948
 
898
- if (isFinalAttempt) {
899
- finalAttemptHandlers?.onFinish?.(test)
949
+ if (shouldWaitForHitProbe) {
950
+ const waitForHitProbePromise = waitForHitProbe()
951
+ if (shouldFinishTest && config.isTestFailureVideosEnabled && !test.isPending()) {
952
+ // Mocha does not await test-end listeners. Its hookUp continuation must wait even without teardown hooks.
953
+ test._ddDeferredHookEnd = { waitForHitProbePromise, publishTestFinish: finish }
954
+ return
955
+ }
956
+ await waitForHitProbePromise
900
957
  }
958
+ finish()
901
959
  }
902
960
  }
903
961
 
@@ -978,17 +1036,30 @@ function finishDeferredHookEnd (test) {
978
1036
  * @returns {unknown}
979
1037
  */
980
1038
  function runFailedTestReplayHookUpCallback (fn, test, failedTestReplayPromise, hookThis, args) {
1039
+ if (test._ddPendingHookFailureFinish) {
1040
+ // Mocha restarts teardown at the failed suite's parent. Only the last hookUp completes the attempt.
1041
+ const failedSuite = /** @type {import('mocha').Suite|undefined} */ (args[1])
1042
+ if (args[0] && failedSuite?.parent) return fn.apply(hookThis, /** @type {unknown[]} */ (args))
1043
+ const finish = test._ddPendingHookFailureFinish
1044
+ delete test._ddPendingHookFailureFinish
1045
+ finish()
1046
+ }
981
1047
  const pendingRetry = test._ddPendingRetry
982
1048
  if (pendingRetry) {
983
1049
  delete test._ddPendingRetry
984
1050
  failedTestReplayPromise = pendingRetry()
985
1051
  }
1052
+ const continueAfterCapture = () => {
1053
+ const context = { onDone: () => fn.apply(hookThis, args), isWaiting: false }
1054
+ testAfterEachFinishCh.publish(context)
1055
+ return context.isWaiting ? undefined : context.onDone()
1056
+ }
986
1057
  const continueAfterProbe = () => {
987
1058
  const deferredHookEndPromise = finishDeferredHookEnd(test)
988
1059
  if (deferredHookEndPromise) {
989
- return deferredHookEndPromise.then(() => fn.apply(hookThis, args), () => fn.apply(hookThis, args))
1060
+ return deferredHookEndPromise.then(continueAfterCapture, continueAfterCapture)
990
1061
  }
991
- return fn.apply(hookThis, args)
1062
+ return continueAfterCapture()
992
1063
  }
993
1064
 
994
1065
  if (failedTestReplayPromise) {
@@ -1037,15 +1108,23 @@ function getOnFailHandler (isMain, config) {
1037
1108
  const testFile = testOrHook.file
1038
1109
  let test = testOrHook
1039
1110
  const isHook = testOrHook.type === 'hook'
1111
+ const isBeforeEachHook = isHook && testOrHook.parent?._beforeEach?.includes(testOrHook)
1112
+ const isAfterEachHook = isHook && testOrHook.parent?._afterEach?.includes(testOrHook)
1113
+ const isTestHook = isBeforeEachHook || isAfterEachHook
1114
+ if (isHook && !isTestHook && testOrHook.parent) {
1115
+ suiteHookFailureSuites.add(testOrHook.parent)
1116
+ }
1040
1117
  if (isHook && testOrHook.ctx) {
1041
1118
  test = testOrHook.ctx.currentTest
1042
1119
  }
1043
1120
  let testContext
1044
1121
  if (test) {
1122
+ if (!isHook && (test._ddSuppressSyntheticTestLifecycle || hasHandledHookFailure(test))) return
1123
+
1045
1124
  testContext = getTestContext(test)
1046
1125
  }
1047
1126
  if (testContext) {
1048
- if (isHook && startTestFinish(test)) {
1127
+ if (isTestHook && startTestFinish(test)) {
1049
1128
  delete test._ddPendingRetry
1050
1129
  const hookError = new Error(`${testOrHook.fullTitle()}: ${err.message}`, { cause: err })
1051
1130
  hookError.name = err.name
@@ -1054,17 +1133,27 @@ function getOnFailHandler (isMain, config) {
1054
1133
  errorCh.runStores(testContext, () => {})
1055
1134
  // Mocha marks the hook failed, so record the test failure before computing final metadata.
1056
1135
  test._ddHookFailed = true
1057
- const testFinishInfo = getTestFinishInfo(test, 'fail', config, hookError)
1058
- if (testFinishInfo.finalStatus !== undefined) {
1059
- test._ddIsFinalAttempt = true
1136
+ if (isBeforeEachHook && testOrHook.parent) {
1137
+ suiteHookFailureSuites.add(testOrHook.parent)
1138
+ }
1139
+ const finish = () => {
1140
+ const testFinishInfo = getTestFinishInfo(test, 'fail', config, testContext.err)
1141
+ if (testFinishInfo.finalStatus !== undefined) {
1142
+ test._ddIsFinalAttempt = true
1143
+ }
1144
+ test._ddTestFinishPublished = true
1145
+ testFinishCh.publish({
1146
+ status: 'fail',
1147
+ hasBeenRetried: isMochaRetry(test),
1148
+ ...testFinishInfo,
1149
+ ...testContext.currentStore,
1150
+ })
1151
+ }
1152
+ if (config.isTestFailureVideosEnabled) {
1153
+ test._ddPendingHookFailureFinish = finish
1154
+ } else {
1155
+ finish()
1060
1156
  }
1061
- test._ddTestFinishPublished = true
1062
- testFinishCh.publish({
1063
- status: 'fail',
1064
- hasBeenRetried: isMochaRetry(test),
1065
- ...testFinishInfo,
1066
- ...testContext.currentStore,
1067
- })
1068
1157
  } else {
1069
1158
  testContext.err = err
1070
1159
  errorCh.runStores(testContext, () => {})
@@ -1105,7 +1194,7 @@ function getOnTestRetryHandler (config) {
1105
1194
  test._retries = dynamicCount
1106
1195
  }
1107
1196
 
1108
- if (config.isFlakyTestRetriesEnabled && getAfterEachHooks(test).length) {
1197
+ if ((config.isFlakyTestRetriesEnabled || config.isTestFailureVideosEnabled) && getAfterEachHooks(test).length) {
1109
1198
  // Mocha queues retries before afterEach; a hook failure can still cancel the retry.
1110
1199
  test._ddPendingRetry = () => publishTestRetry(test, err, config)
1111
1200
  return