dd-trace 5.103.0 → 5.104.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 (90) hide show
  1. package/index.d.ts +25 -3
  2. package/package.json +4 -3
  3. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
  4. package/packages/datadog-instrumentations/src/cassandra-driver.js +5 -2
  5. package/packages/datadog-instrumentations/src/cucumber.js +103 -30
  6. package/packages/datadog-instrumentations/src/elasticsearch.js +4 -4
  7. package/packages/datadog-instrumentations/src/graphql.js +0 -5
  8. package/packages/datadog-instrumentations/src/grpc/client.js +48 -32
  9. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +1 -1
  10. package/packages/datadog-instrumentations/src/helpers/kafka.js +17 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +3 -2
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +19 -5
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +14 -13
  14. package/packages/datadog-instrumentations/src/http/client.js +2 -2
  15. package/packages/datadog-instrumentations/src/ioredis.js +3 -3
  16. package/packages/datadog-instrumentations/src/jest.js +33 -36
  17. package/packages/datadog-instrumentations/src/kafkajs.js +25 -6
  18. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  19. package/packages/datadog-instrumentations/src/memcached.js +2 -1
  20. package/packages/datadog-instrumentations/src/mocha/main.js +272 -91
  21. package/packages/datadog-instrumentations/src/mocha/utils.js +48 -8
  22. package/packages/datadog-instrumentations/src/mongodb-core.js +1 -1
  23. package/packages/datadog-instrumentations/src/mongoose.js +10 -12
  24. package/packages/datadog-instrumentations/src/mysql.js +2 -2
  25. package/packages/datadog-instrumentations/src/mysql2.js +1 -1
  26. package/packages/datadog-instrumentations/src/pg.js +1 -1
  27. package/packages/datadog-instrumentations/src/playwright.js +22 -5
  28. package/packages/datadog-instrumentations/src/router.js +4 -2
  29. package/packages/datadog-instrumentations/src/vitest.js +246 -149
  30. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -19
  31. package/packages/datadog-plugin-elasticsearch/src/index.js +28 -8
  32. package/packages/datadog-plugin-graphql/src/utils.js +4 -1
  33. package/packages/datadog-plugin-kafkajs/src/producer.js +32 -0
  34. package/packages/datadog-plugin-mongodb-core/src/index.js +54 -19
  35. package/packages/datadog-plugin-redis/src/index.js +37 -2
  36. package/packages/datadog-plugin-undici/src/index.js +19 -0
  37. package/packages/datadog-plugin-vitest/src/index.js +19 -7
  38. package/packages/datadog-shimmer/src/shimmer.js +35 -0
  39. package/packages/dd-trace/src/appsec/blocking.js +2 -2
  40. package/packages/dd-trace/src/appsec/index.js +10 -3
  41. package/packages/dd-trace/src/appsec/reporter.js +19 -5
  42. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -1
  43. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +5 -3
  44. package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -0
  45. package/packages/dd-trace/src/config/supported-configurations.json +9 -0
  46. package/packages/dd-trace/src/crashtracking/crashtracker.js +15 -3
  47. package/packages/dd-trace/src/datastreams/context.js +4 -2
  48. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +26 -19
  49. package/packages/dd-trace/src/exporters/common/agents.js +3 -1
  50. package/packages/dd-trace/src/exporters/common/request.js +3 -1
  51. package/packages/dd-trace/src/id.js +17 -4
  52. package/packages/dd-trace/src/lambda/handler.js +2 -4
  53. package/packages/dd-trace/src/llmobs/sdk.js +10 -0
  54. package/packages/dd-trace/src/log/writer.js +3 -1
  55. package/packages/dd-trace/src/noop/span.js +3 -1
  56. package/packages/dd-trace/src/openfeature/writers/exposures.js +51 -20
  57. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
  58. package/packages/dd-trace/src/plugins/apollo.js +3 -1
  59. package/packages/dd-trace/src/plugins/ci_plugin.js +3 -13
  60. package/packages/dd-trace/src/plugins/log_plugin.js +3 -1
  61. package/packages/dd-trace/src/plugins/tracing.js +5 -3
  62. package/packages/dd-trace/src/plugins/util/git.js +3 -1
  63. package/packages/dd-trace/src/plugins/util/test.js +82 -0
  64. package/packages/dd-trace/src/plugins/util/web.js +11 -0
  65. package/packages/dd-trace/src/scope.js +7 -5
  66. package/packages/dd-trace/src/service-naming/extra-services.js +14 -0
  67. package/vendor/dist/opentracing/LICENSE +0 -201
  68. package/vendor/dist/opentracing/binary_carrier.d.ts +0 -11
  69. package/vendor/dist/opentracing/constants.d.ts +0 -61
  70. package/vendor/dist/opentracing/examples/demo/demo.d.ts +0 -2
  71. package/vendor/dist/opentracing/ext/tags.d.ts +0 -90
  72. package/vendor/dist/opentracing/functions.d.ts +0 -20
  73. package/vendor/dist/opentracing/global_tracer.d.ts +0 -14
  74. package/vendor/dist/opentracing/index.d.ts +0 -12
  75. package/vendor/dist/opentracing/index.js +0 -1
  76. package/vendor/dist/opentracing/mock_tracer/index.d.ts +0 -5
  77. package/vendor/dist/opentracing/mock_tracer/mock_context.d.ts +0 -13
  78. package/vendor/dist/opentracing/mock_tracer/mock_report.d.ts +0 -16
  79. package/vendor/dist/opentracing/mock_tracer/mock_span.d.ts +0 -50
  80. package/vendor/dist/opentracing/mock_tracer/mock_tracer.d.ts +0 -26
  81. package/vendor/dist/opentracing/noop.d.ts +0 -8
  82. package/vendor/dist/opentracing/reference.d.ts +0 -33
  83. package/vendor/dist/opentracing/span.d.ts +0 -147
  84. package/vendor/dist/opentracing/span_context.d.ts +0 -26
  85. package/vendor/dist/opentracing/test/api_compatibility.d.ts +0 -16
  86. package/vendor/dist/opentracing/test/mocktracer_implemenation.d.ts +0 -3
  87. package/vendor/dist/opentracing/test/noop_implementation.d.ts +0 -4
  88. package/vendor/dist/opentracing/test/opentracing_api.d.ts +0 -3
  89. package/vendor/dist/opentracing/test/unittest.d.ts +0 -2
  90. package/vendor/dist/opentracing/tracer.d.ts +0 -127
@@ -18,6 +18,7 @@ const {
18
18
  getIsFaultyEarlyFlakeDetection,
19
19
  collectTestOptimizationSummariesFromTraces,
20
20
  logTestOptimizationSummary,
21
+ getTestOptimizationRequestResults,
21
22
  } = require('../../../dd-trace/src/plugins/util/test')
22
23
 
23
24
  const {
@@ -109,6 +110,29 @@ function isTestFailed (test) {
109
110
  return false
110
111
  }
111
112
 
113
+ function getRootSuiteStatus (rootTests) {
114
+ let status = 'pass'
115
+ if (rootTests.every(t => t.isPending())) {
116
+ status = 'skip'
117
+ } else {
118
+ for (const test of rootTests) {
119
+ if (test.state === 'failed' || test.timedOut || test._ddHookFailed) {
120
+ status = 'fail'
121
+ }
122
+ }
123
+ }
124
+ return status
125
+ }
126
+
127
+ function haveRootTestsFinished (rootTests) {
128
+ for (const test of rootTests) {
129
+ if (!test.isPending() && !test.state && !test.timedOut && !test._ddHookFailed) {
130
+ return false
131
+ }
132
+ }
133
+ return true
134
+ }
135
+
112
136
  function getFilteredSuites (originalSuites) {
113
137
  return originalSuites.reduce((acc, suite) => {
114
138
  const testPath = getTestSuitePath(suite.file, process.cwd())
@@ -226,11 +250,38 @@ function getOnEndHandler (isParallel) {
226
250
  }
227
251
  }
228
252
 
253
+ function getRunStoresPromise (channelToPublishTo, ctx) {
254
+ return new Promise(resolve => {
255
+ channelToPublishTo.runStores({ ...ctx, onDone: resolve }, () => {})
256
+ })
257
+ }
258
+
259
+ function applyKnownTestsResponse ({ err, knownTests }) {
260
+ if (err) {
261
+ config.knownTests = []
262
+ config.isEarlyFlakeDetectionEnabled = false
263
+ config.isKnownTestsEnabled = false
264
+ } else {
265
+ config.knownTests = knownTests
266
+ }
267
+ }
268
+
269
+ function applyTestManagementTestsResponse ({ err, testManagementTests: receivedTestManagementTests }) {
270
+ if (err) {
271
+ config.testManagementTests = {}
272
+ config.isTestManagementTestsEnabled = false
273
+ config.testManagementAttemptToFixRetries = 0
274
+ } else {
275
+ config.testManagementTests = receivedTestManagementTests
276
+ }
277
+ }
278
+
229
279
  function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFinishRequest) {
230
280
  const ctx = {
231
281
  isParallel,
232
282
  frameworkVersion,
233
283
  }
284
+ let skippableSuitesResponse
234
285
 
235
286
  const onReceivedSkippableSuites = ({ err, skippableSuites, itrCorrelationId: responseItrCorrelationId }) => {
236
287
  if (err) {
@@ -256,6 +307,16 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
256
307
  })
257
308
  }
258
309
 
310
+ const requestSkippableSuites = () => {
311
+ if (skippableSuitesResponse) {
312
+ onReceivedSkippableSuites(skippableSuitesResponse)
313
+ return
314
+ }
315
+
316
+ ctx.onDone = onReceivedSkippableSuites
317
+ skippableSuitesCh.runStores(ctx, () => {})
318
+ }
319
+
259
320
  const onReceivedImpactedTests = ({ err, modifiedFiles: receivedModifiedFiles }) => {
260
321
  if (err) {
261
322
  config.modifiedFiles = []
@@ -264,8 +325,7 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
264
325
  config.modifiedFiles = receivedModifiedFiles
265
326
  }
266
327
  if (config.isSuitesSkippingEnabled) {
267
- ctx.onDone = onReceivedSkippableSuites
268
- skippableSuitesCh.runStores(ctx, () => {})
328
+ requestSkippableSuites()
269
329
  } else {
270
330
  mochaGlobalRunCh.runStores(ctx, () => {
271
331
  onFinishRequest()
@@ -273,44 +333,12 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
273
333
  }
274
334
  }
275
335
 
276
- const onReceivedTestManagementTests = ({ err, testManagementTests: receivedTestManagementTests }) => {
277
- if (err) {
278
- config.testManagementTests = {}
279
- config.isTestManagementTestsEnabled = false
280
- config.testManagementAttemptToFixRetries = 0
281
- } else {
282
- config.testManagementTests = receivedTestManagementTests
283
- }
336
+ const continueAfterTestRequests = () => {
284
337
  if (config.isImpactedTestsEnabled) {
285
338
  ctx.onDone = onReceivedImpactedTests
286
339
  modifiedFilesCh.runStores(ctx, () => {})
287
340
  } else if (config.isSuitesSkippingEnabled) {
288
- ctx.onDone = onReceivedSkippableSuites
289
- skippableSuitesCh.runStores(ctx, () => {})
290
- } else {
291
- mochaGlobalRunCh.runStores(ctx, () => {
292
- onFinishRequest()
293
- })
294
- }
295
- }
296
-
297
- const onReceivedKnownTests = ({ err, knownTests }) => {
298
- if (err) {
299
- config.knownTests = []
300
- config.isEarlyFlakeDetectionEnabled = false
301
- config.isKnownTestsEnabled = false
302
- } else {
303
- config.knownTests = knownTests
304
- }
305
- if (config.isTestManagementTestsEnabled) {
306
- ctx.onDone = onReceivedTestManagementTests
307
- testManagementTestsCh.runStores(ctx, () => {})
308
- } else if (config.isImpactedTestsEnabled) {
309
- ctx.onDone = onReceivedImpactedTests
310
- modifiedFilesCh.runStores(ctx, () => {})
311
- } else if (config.isSuitesSkippingEnabled) {
312
- ctx.onDone = onReceivedSkippableSuites
313
- skippableSuitesCh.runStores(ctx, () => {})
341
+ requestSkippableSuites()
314
342
  } else {
315
343
  mochaGlobalRunCh.runStores(ctx, () => {
316
344
  onFinishRequest()
@@ -336,23 +364,30 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini
336
364
  config.isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
337
365
  config.flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
338
366
 
339
- if (config.isKnownTestsEnabled) {
340
- ctx.onDone = onReceivedKnownTests
341
- knownTestsCh.runStores(ctx, () => {})
342
- } else if (config.isTestManagementTestsEnabled) {
343
- ctx.onDone = onReceivedTestManagementTests
344
- testManagementTestsCh.runStores(ctx, () => {})
345
- } else if (config.isImpactedTestsEnabled) {
346
- ctx.onDone = onReceivedImpactedTests
347
- modifiedFilesCh.runStores(ctx, () => {})
348
- } else if (config.isSuitesSkippingEnabled) {
349
- ctx.onDone = onReceivedSkippableSuites
350
- skippableSuitesCh.runStores(ctx, () => {})
351
- } else {
352
- mochaGlobalRunCh.runStores(ctx, () => {
353
- onFinishRequest()
354
- })
355
- }
367
+ getTestOptimizationRequestResults({
368
+ isKnownTestsEnabled: config.isKnownTestsEnabled,
369
+ isTestManagementTestsEnabled: config.isTestManagementTestsEnabled,
370
+ isSuitesSkippingEnabled: config.isSuitesSkippingEnabled,
371
+ getKnownTests: () => getRunStoresPromise(knownTestsCh, ctx),
372
+ getTestManagementTests: () => getRunStoresPromise(testManagementTestsCh, ctx),
373
+ getSkippableSuites: () => getRunStoresPromise(skippableSuitesCh, ctx),
374
+ }).then(requestResults => {
375
+ const {
376
+ knownTestsResponse,
377
+ testManagementTestsResponse,
378
+ skippableSuitesResponse: requestSkippableSuitesResponse,
379
+ } = requestResults
380
+
381
+ if (knownTestsResponse) {
382
+ applyKnownTestsResponse(knownTestsResponse)
383
+ }
384
+ if (testManagementTestsResponse) {
385
+ applyTestManagementTestsResponse(testManagementTestsResponse)
386
+ }
387
+ skippableSuitesResponse = requestSkippableSuitesResponse
388
+
389
+ continueAfterTestRequests()
390
+ })
356
391
  }
357
392
 
358
393
  ctx.onDone = onReceivedConfiguration
@@ -473,22 +508,166 @@ addHook({
473
508
  // Populated during the root 'suite' event so the normal finish path can include them
474
509
  // in mixed-file status calculation.
475
510
  const rootTestsByFile = new Map()
511
+ // Counts how many original tests per pure-root file still need their final attempt.
512
+ // Hits zero when the last test's lifecycle completes, triggering the suite finish.
513
+ const rootPendingCountByFile = new Map()
514
+ const rootFinalizationPendingCountByFile = new Map()
515
+ const rootFallbackPendingFiles = new Set()
516
+ const rootFinalizationPendingTests = new WeakSet()
517
+ let pendingRootFinalizations = 0
518
+ let hasEnded = false
519
+ let hasFinishedRun = false
520
+ let endRunner
521
+
522
+ function updateRootTestForFinalAttempt (test) {
523
+ if (!test._retriedTest) return
524
+
525
+ const rootTests = rootTestsByFile.get(test.file)
526
+ if (!rootTests) return
527
+
528
+ const retriedTestIndex = rootTests.indexOf(test._retriedTest)
529
+ if (retriedTestIndex !== -1) {
530
+ rootTests[retriedTestIndex] = test
531
+ }
532
+ }
533
+
534
+ function finishRunIfReady () {
535
+ if (hasFinishedRun) return
536
+ if (hasEnded && pendingRootFinalizations === 0) {
537
+ hasFinishedRun = true
538
+ onEnd.call(endRunner)
539
+ }
540
+ }
541
+
542
+ function incrementPendingRootFinalization (test) {
543
+ if (!rootPendingCountByFile.has(test.file) || rootFinalizationPendingTests.has(test)) return
544
+
545
+ rootFinalizationPendingTests.add(test)
546
+ pendingRootFinalizations++
547
+ rootFinalizationPendingCountByFile.set(
548
+ test.file,
549
+ (rootFinalizationPendingCountByFile.get(test.file) || 0) + 1
550
+ )
551
+ }
552
+
553
+ function decrementPendingRootFinalization (test) {
554
+ if (!rootFinalizationPendingTests.has(test)) return
555
+
556
+ rootFinalizationPendingTests.delete(test)
557
+ pendingRootFinalizations--
558
+
559
+ const remaining = rootFinalizationPendingCountByFile.get(test.file) - 1
560
+ if (remaining > 0) {
561
+ rootFinalizationPendingCountByFile.set(test.file, remaining)
562
+ } else {
563
+ rootFinalizationPendingCountByFile.delete(test.file)
564
+ }
565
+
566
+ if (!rootFinalizationPendingCountByFile.has(test.file) && rootFallbackPendingFiles.delete(test.file)) {
567
+ finishRootSuiteFallbackForFile(test.file)
568
+ }
569
+
570
+ finishRunIfReady()
571
+ }
572
+
573
+ function finishRootSuiteForFile (file) {
574
+ const remaining = rootPendingCountByFile.get(file) - 1
575
+ if (remaining > 0) {
576
+ rootPendingCountByFile.set(file, remaining)
577
+ return
578
+ }
579
+ rootPendingCountByFile.delete(file)
580
+
581
+ const ctx = testFileToSuiteCtx.get(file)
582
+ if (!ctx) {
583
+ log.warn('No ctx found for suite', file)
584
+ return
585
+ }
586
+
587
+ const rootTests = rootTestsByFile.get(file) || []
588
+ const status = getRootSuiteStatus(rootTests)
589
+
590
+ if (global.__coverage__) {
591
+ const coverageFiles = getCoveredFilenamesFromCoverage(global.__coverage__)
592
+ testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
593
+ mergeCoverage(global.__coverage__, originalCoverageMap)
594
+ resetCoverage(global.__coverage__)
595
+ }
596
+
597
+ testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
598
+ }
599
+
600
+ function finishRootSuiteFallbackForFile (file) {
601
+ const ctx = testFileToSuiteCtx.get(file)
602
+ if (!ctx || !rootPendingCountByFile.has(file)) return
603
+
604
+ const rootTests = rootTestsByFile.get(file) || []
605
+ const status = haveRootTestsFinished(rootTests) ? getRootSuiteStatus(rootTests) : 'fail'
606
+ rootPendingCountByFile.delete(file)
607
+ testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
608
+ }
609
+
610
+ function finishRootSuiteAfterFinalAttempt (test) {
611
+ if (!test._ddIsFinalAttempt || !rootPendingCountByFile.has(test.file)) return
612
+
613
+ updateRootTestForFinalAttempt(test)
614
+ finishRootSuiteForFile(test.file)
615
+ }
616
+
617
+ const onEnd = getOnEndHandler(false)
476
618
 
477
619
  this.once('start', getOnStartHandler(frameworkVersion))
478
620
 
479
- this.once('end', getOnEndHandler(false))
621
+ this.once('end', function () {
622
+ hasEnded = true
623
+ endRunner = this
624
+ finishRunIfReady()
625
+ })
626
+
627
+ // The job of this listener is to
628
+ // initialize the suite span tag in correct order
629
+ // (that is suiteA -> testA ... -> suiteB -> testB
630
+ // instead of suiteA -> suiteB -> testA -> ... -> testB)
631
+ // when the suite has tests that are in the top level
632
+ // (no describe(...))
633
+ this.on('test', function (test) {
634
+ const ctx = testFileToSuiteCtx.get(test.file)
635
+ if (ctx?._pendingRootStart) {
636
+ ctx._pendingRootStart = false
637
+ testSuiteStartCh.runStores(ctx, () => {})
638
+ }
639
+ })
480
640
 
481
641
  this.on('test', getOnTestHandler(true))
482
642
 
483
- this.on('test end', getOnTestEndHandler(config))
643
+ this.on('test end', getOnTestEndHandler(config, {
644
+ onStart: incrementPendingRootFinalization,
645
+ onFinish: function (test) {
646
+ finishRootSuiteAfterFinalAttempt(test)
647
+ decrementPendingRootFinalization(test)
648
+ },
649
+ }))
484
650
 
485
651
  this.on('retry', getOnTestRetryHandler(config))
486
652
 
487
653
  // If the hook passes, 'hook end' will be emitted. Otherwise, 'fail' will be emitted
488
654
  this.on('hook end', getOnHookEndHandler(config))
489
655
 
656
+ this.on('hook end', function (hook) {
657
+ const test = hook.ctx?.currentTest
658
+ if (!test) return
659
+ finishRootSuiteAfterFinalAttempt(test)
660
+ })
661
+
490
662
  this.on('fail', getOnFailHandler(true, config))
491
663
 
664
+ this.on('fail', function (testOrHook) {
665
+ if (testOrHook.type !== 'hook') return
666
+ const test = testOrHook.ctx?.currentTest
667
+ if (!test) return
668
+ finishRootSuiteAfterFinalAttempt(test)
669
+ })
670
+
492
671
  this.on('pending', getOnPendingHandler())
493
672
 
494
673
  this.on('suite', function (suite) {
@@ -503,7 +682,13 @@ addHook({
503
682
  if (suite.root && suite.tests.length > 0) {
504
683
  const files = new Set(suite.tests.map(test => test.file).filter(Boolean))
505
684
  for (const file of files) {
506
- rootTestsByFile.set(file, suite.tests.filter(t => t.file === file))
685
+ const testsForFile = suite.tests.filter(t => t.file === file)
686
+ rootTestsByFile.set(file, testsForFile)
687
+ // Only track the countdown for pure root-level files.
688
+ // Mixed files are finished by the normal 'suite end' path.
689
+ if (!suitesByTestFile[file]) {
690
+ rootPendingCountByFile.set(file, testsForFile.length)
691
+ }
507
692
  if (testFileToSuiteCtx.get(file)) continue
508
693
  const isUnskippable = unskippableSuites.includes(file)
509
694
  isForcedToRun = isUnskippable && suitesToSkip.includes(getTestSuitePath(file, process.cwd()))
@@ -512,9 +697,9 @@ addHook({
512
697
  isUnskippable,
513
698
  isForcedToRun,
514
699
  itrCorrelationId,
700
+ _pendingRootStart: true, // Now the suite start fires lazily on the first test event for this file
515
701
  }
516
702
  testFileToSuiteCtx.set(file, ctx)
517
- testSuiteStartCh.runStores(ctx, () => {})
518
703
  }
519
704
  }
520
705
  return
@@ -536,42 +721,38 @@ addHook({
536
721
 
537
722
  this.on('suite end', function (suite) {
538
723
  if (suite.root) {
539
- // Symmetric to the suite start fix
540
- const fileToTests = new Map()
724
+ // Normal case: pure root-level files are finished by the 'test end' / 'hook end'
725
+ // listeners via finishRootSuiteForFile. Two edge cases remain here:
726
+ //
727
+ // 1. All-pending: no 'test' event fired, _pendingRootStart is still true.
728
+ // Start and immediately finish with 'skip'.
729
+ //
730
+ // 2. Aborted mid-run (e.g. a beforeEach hook failure): Mocha skips remaining
731
+ // tests and jumps straight to 'suite end'. rootPendingCountByFile still has
732
+ // a nonzero count for the file because the last tests never ran. Finish it
733
+ // as failed now.
734
+ //
735
+ // 3. Async finalization lagged behind Mocha's synchronous events (e.g. DI retry
736
+ // wait): all tests have Mocha terminal state, but the final-attempt callback
737
+ // did not run before root 'suite end'. Finish from the observed test states.
738
+ const processedFiles = new Set()
541
739
  for (const test of suite.tests) {
542
- if (!test.file) continue
543
- if (!fileToTests.has(test.file)) fileToTests.set(test.file, [])
544
- fileToTests.get(test.file).push(test)
545
- }
546
- for (const [file, tests] of fileToTests) {
547
- // Mixed case: if a file appears in suitesByTestFile (pre-populated before the run),
548
- // its numSuitesByTestFile counter hits zero when its last describe-based suite ends
549
- // and the normal path below fires testSuiteFinishCh. Since root is last when
550
- // 'suite end' fires, any such file has already been handled — skipping it here
551
- // avoids duplication.
552
- if (suitesByTestFile[file]) continue
553
- let status = 'pass'
554
- if (tests.every(test => test.isPending())) {
555
- status = 'skip'
556
- } else {
557
- for (const test of tests) {
558
- if (test.state === 'failed' || test.timedOut) {
559
- status = 'fail'
560
- break
561
- }
740
+ if (!test.file || processedFiles.has(test.file)) continue
741
+ processedFiles.add(test.file)
742
+ if (suitesByTestFile[test.file]) continue // mixed: handled by normal path
743
+ const ctx = testFileToSuiteCtx.get(test.file)
744
+ if (!ctx) continue
745
+ if (ctx._pendingRootStart) {
746
+ ctx._pendingRootStart = false
747
+ testSuiteStartCh.runStores(ctx, () => {})
748
+ testSuiteFinishCh.publish({ status: 'skip', ...ctx.currentStore }, () => {})
749
+ } else if (rootPendingCountByFile.has(test.file)) {
750
+ if (rootFinalizationPendingCountByFile.has(test.file)) {
751
+ rootFallbackPendingFiles.add(test.file)
752
+ continue
562
753
  }
563
- }
564
- if (global.__coverage__) {
565
- const coverageFiles = getCoveredFilenamesFromCoverage(global.__coverage__)
566
- testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
567
- mergeCoverage(global.__coverage__, originalCoverageMap)
568
- resetCoverage(global.__coverage__)
569
- }
570
- const ctx = testFileToSuiteCtx.get(file)
571
- if (ctx) {
572
- testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
573
- } else {
574
- log.warn('No ctx found for suite', file)
754
+
755
+ finishRootSuiteFallbackForFile(test.file)
575
756
  }
576
757
  }
577
758
  return
@@ -503,13 +503,38 @@ function getTestFinishInfo (test, status, config, error) {
503
503
  }
504
504
  }
505
505
 
506
- function getOnTestEndHandler (config) {
506
+ function getOnTestEndHandler (config, finalAttemptHandlers) {
507
507
  return async function (test) {
508
508
  if (test._ddShouldSkipEfdRetry) {
509
509
  return
510
510
  }
511
511
  const ctx = getTestContext(test)
512
512
  const status = getTestStatus(test)
513
+ const shouldFinishTest = ctx && (!getAfterEachHooks(test).length || (test._ddIsDisabled && !test._ddIsAttemptToFix))
514
+ let testFinishInfo
515
+ let isFinalAttempt = false
516
+
517
+ // If there are afterEach to be run, we don't finish the test yet.
518
+ // Disabled tests (marked pending by us) are finished immediately without waiting for afterEach hooks.
519
+ // In older mocha versions, pending tests don't run afterEach hooks, so we can't rely on
520
+ // getOnHookEndHandler to finish the test. This mirrors Jest's approach where the skip handler
521
+ // directly sets finalStatus without waiting for hooks
522
+ if (!ctx && test.isPending()) {
523
+ test._ddIsFinalAttempt = true
524
+ isFinalAttempt = true
525
+ }
526
+
527
+ if (shouldFinishTest) {
528
+ testFinishInfo = getTestFinishInfo(test, status, config, ctx.err || test.err)
529
+ if (testFinishInfo.finalStatus !== undefined) {
530
+ test._ddIsFinalAttempt = true
531
+ isFinalAttempt = true
532
+ }
533
+ }
534
+
535
+ if (isFinalAttempt) {
536
+ finalAttemptHandlers?.onStart?.(test)
537
+ }
513
538
 
514
539
  // After finishing it might take a bit for the snapshot to be handled.
515
540
  // This means that tests retried with DI are BREAKPOINT_HIT_GRACE_PERIOD_MS slower at least.
@@ -521,13 +546,7 @@ function getOnTestEndHandler (config) {
521
546
  })
522
547
  }
523
548
 
524
- // If there are afterEach to be run, we don't finish the test yet.
525
- // Disabled tests (marked pending by us) are finished immediately without waiting for afterEach hooks.
526
- // In older mocha versions, pending tests don't run afterEach hooks, so we can't rely on
527
- // getOnHookEndHandler to finish the test. This mirrors Jest's approach where the skip handler
528
- // directly sets finalStatus without waiting for hooks
529
- if (ctx && (!getAfterEachHooks(test).length || (test._ddIsDisabled && !test._ddIsAttemptToFix))) {
530
- const testFinishInfo = getTestFinishInfo(test, status, config, ctx.err || test.err)
549
+ if (shouldFinishTest) {
531
550
  testFinishCh.publish({
532
551
  status,
533
552
  hasBeenRetried: isMochaRetry(test),
@@ -536,6 +555,10 @@ function getOnTestEndHandler (config) {
536
555
  ...ctx.currentStore,
537
556
  })
538
557
  }
558
+
559
+ if (isFinalAttempt) {
560
+ finalAttemptHandlers?.onFinish?.(test)
561
+ }
539
562
  }
540
563
  }
541
564
 
@@ -552,6 +575,9 @@ function getOnHookEndHandler (config) {
552
575
  // skip to avoid double-publishing
553
576
  if (ctx && (!test._ddIsDisabled || test._ddIsAttemptToFix)) {
554
577
  const testFinishInfo = getTestFinishInfo(test, status, config, ctx.err || test.err)
578
+ if (testFinishInfo.finalStatus !== undefined) {
579
+ test._ddIsFinalAttempt = true
580
+ }
555
581
  testFinishCh.publish({
556
582
  status,
557
583
  hasBeenRetried: isMochaRetry(test),
@@ -583,6 +609,20 @@ function getOnFailHandler (isMain, config) {
583
609
  testContext.err = err
584
610
  errorCh.runStores(testContext, () => {})
585
611
  const testFinishInfo = getTestFinishInfo(test, 'fail', config, err)
612
+ // ATR never retries hook failures: this.retries(N) is set in runnableWrapper
613
+ // which only runs when the test function executes — hooks bypass that path,
614
+ // so _retries stays at -1 and getIsLastRetry returns false, leaving finalStatus
615
+ // undefined. We must also mark the attempt final when no clone-based retry
616
+ // mechanism (EFD original, EFD clone, ATF) has queued further attempts.
617
+ const noCloneRetries = !test._ddIsEfdRetry &&
618
+ !((test._ddIsNew || test._ddIsModified) && config.isEarlyFlakeDetectionEnabled) &&
619
+ !test._ddIsAttemptToFix
620
+ if (testFinishInfo.finalStatus !== undefined || noCloneRetries) {
621
+ test._ddIsFinalAttempt = true
622
+ }
623
+ // test.state is never set to 'failed' for hook failures (Mocha marks the hook,
624
+ // not the test). Flag it so finishRootSuiteForFile can compute the correct status.
625
+ test._ddHookFailed = true
586
626
  testFinishCh.publish({
587
627
  status: 'fail',
588
628
  hasBeenRetried: isMochaRetry(test),
@@ -174,7 +174,7 @@ function instrument (operation, command, instance, args, server, ns, ops, option
174
174
  name,
175
175
  }
176
176
  return startCh.runStores(ctx, () => {
177
- args[index] = shimmer.wrapFunction(callback, callback => function (err, res) {
177
+ args[index] = shimmer.wrapCallback(callback, callback => function (err, res) {
178
178
  if (err) {
179
179
  ctx.error = err
180
180
  errorCh.publish(ctx)
@@ -125,21 +125,19 @@ addHook({
125
125
  const resolve = args[0]
126
126
  const reject = args[1]
127
127
 
128
- args[0] = shimmer.wrapFunction(resolve, resolve => function wrappedResolve (...args) {
129
- finishCh.publish(ctx)
130
-
131
- if (resolve) {
128
+ if (typeof resolve === 'function') {
129
+ args[0] = shimmer.wrapCallback(resolve, resolve => function wrappedResolve (...args) {
130
+ finishCh.publish(ctx)
132
131
  return resolve.apply(this, args)
133
- }
134
- })
135
-
136
- args[1] = shimmer.wrapFunction(reject, reject => function wrappedReject (...args) {
137
- finishCh.publish(ctx)
132
+ })
133
+ }
138
134
 
139
- if (reject) {
135
+ if (typeof reject === 'function') {
136
+ args[1] = shimmer.wrapCallback(reject, reject => function wrappedReject (...args) {
137
+ finishCh.publish(ctx)
140
138
  return reject.apply(this, args)
141
- }
142
- })
139
+ })
140
+ }
143
141
 
144
142
  return originalThen.apply(this, args)
145
143
  }
@@ -29,7 +29,7 @@ addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connect
29
29
 
30
30
  if (res._callback) {
31
31
  const cb = res._callback
32
- res._callback = shimmer.wrapFunction(cb, cb => function (error, result) {
32
+ res._callback = shimmer.wrapCallback(cb, cb => function (error, result) {
33
33
  if (error) {
34
34
  ctx.error = error
35
35
  errorCh.publish(ctx)
@@ -86,7 +86,7 @@ addHook({ name: 'mysql', file: 'lib/Pool.js', versions: ['>=2'] }, Pool => {
86
86
  return startPoolQueryCh.runStores(ctx, () => {
87
87
  const cb = args[args.length - 1]
88
88
  if (typeof cb === 'function') {
89
- args[args.length - 1] = shimmer.wrapFunction(cb, cb => function (...args) {
89
+ args[args.length - 1] = shimmer.wrapCallback(cb, cb => function (...args) {
90
90
  return finishPoolQueryCh.runStores(ctx, cb, this, ...args)
91
91
  })
92
92
  }
@@ -176,7 +176,7 @@ function wrapConnection (Connection, version) {
176
176
  if (typeof this.onResult === 'function') {
177
177
  const onResult = this.onResult
178
178
 
179
- this.onResult = shimmer.wrapFunction(onResult, onResult => function (error) {
179
+ this.onResult = shimmer.wrapCallback(onResult, onResult => function (error) {
180
180
  if (error) {
181
181
  ctx.error = error
182
182
  errorCh.publish(ctx)
@@ -193,7 +193,7 @@ function wrapPoolQuery (query) {
193
193
  }
194
194
 
195
195
  if (typeof cb === 'function') {
196
- args[args.length - 1] = shimmer.wrapFunction(cb, cb => function (...args) {
196
+ args[args.length - 1] = shimmer.wrapCallback(cb, cb => function (...args) {
197
197
  finish(ctx)
198
198
  return cb.apply(this, args)
199
199
  })