@@ -744,7 +744,10 @@ class Test extends AsyncResource {
744744 this . expectedAssertions = plan ;
745745 this . cancelled = false ;
746746 if ( flaky === undefined || flaky === null ) {
747- this . flakyRetries = this . parent ?. flakyRetries ?? 0 ;
747+ // Inherit flaky only from a suite parent: a suite propagates it to its
748+ // test-cases, but a test must not propagate it to its own subtests.
749+ this . flakyRetries = this . parent ?. reportedType === 'suite' ?
750+ ( this . parent . flakyRetries ?? 0 ) : 0 ;
748751 } else if ( typeof flaky === 'boolean' ) {
749752 this . flakyRetries = flaky ? kDefaultFlakyRetries : 0 ;
750753 } else if ( typeof flaky === 'number' ) {
@@ -1379,6 +1382,9 @@ class Test extends AsyncResource {
13791382 this . error = null ;
13801383 this . passed = false ;
13811384 this . cancelled = false ;
1385+ // Restore any mocks applied by the previous attempt so the retry starts
1386+ // clean (the MockTracker is otherwise only reset in postRun).
1387+ this . mock ?. reset ( ) ;
13821388 ArrayPrototypeSplice ( this . diagnostics , baseDiagnosticsCount ) ;
13831389 // Reset assertion bookkeeping; re-arm the plan if one was requested via
13841390 // the `plan` option so that `t.plan(N)` does not accumulate across
@@ -1405,9 +1411,13 @@ class Test extends AsyncResource {
14051411 // is resolved by the snapshot manager; it is intentionally not reset from
14061412 // here because doing so would corrupt sibling tests in the same file. The
14071413 // final attempt's snapshots win (last-write-wins).
1408- // A fresh abort controller per attempt prevents an abort from a previous
1409- // attempt's timeout/cancel from being observed as an external abort by
1410- // the retry decision below.
1414+ // Abort the previous attempt's controller so any signal-bound work it
1415+ // started (e.g. a timer awaiting t.signal) is torn down instead of
1416+ // leaking. The retry decision for that attempt has already been made, so
1417+ // this cannot be misread as an external abort. A fresh controller then
1418+ // backs the next attempt, preventing the previous abort from being
1419+ // observed as an external abort by the retry decision below.
1420+ this . abortController . abort ( ) ;
14111421 this . abortController = new AbortController ( ) ;
14121422 this . signal = this . abortController . signal ;
14131423 afterEach = runOnce ( async ( ) => {
@@ -1493,14 +1503,6 @@ class Test extends AsyncResource {
14931503 await afterEach ( ) ;
14941504 await after ( ) ;
14951505 } catch ( err ) {
1496- // Publish to the tracing channel's error stream only on the final
1497- // attempt; intermediate retries of a flaky test are silent. For a
1498- // non-flaky test (maxAttempts === 1) this always fires, as before.
1499- const willRetry = attempt < maxAttempts &&
1500- ! ( this . signal . aborted || this . outerSignal ?. aborted ) ;
1501- if ( ! willRetry && channelContext !== null && testChannel . error . hasSubscribers ) {
1502- publishError ( err ) ;
1503- }
15041506 if ( isTestFailureError ( err ) ) {
15051507 if ( err . failureType === kTestTimeoutFailure && this . flakyRetries === 0 ) {
15061508 // Non-flaky timeouts are classified as cancelled, unchanged.
@@ -1516,23 +1518,49 @@ class Test extends AsyncResource {
15161518 } else {
15171519 this . fail ( new ERR_TEST_FAILURE ( err , kTestCodeFailure ) ) ;
15181520 }
1521+ // The pass/fail outcome is now known (e.g. an expectFailure throw is a
1522+ // pass, so it is not retried). Publish to the tracing channel's error
1523+ // stream and run the test-level `after` hook only on the final attempt;
1524+ // intermediate retries of a flaky test are silent and keep `after` for
1525+ // the end. For a non-flaky test (maxAttempts === 1) the error always
1526+ // publishes, as before.
1527+ const willRetry = attempt < maxAttempts &&
1528+ ! this . passed &&
1529+ ! this . expectFailure &&
1530+ ! ( this . signal . aborted || this . outerSignal ?. aborted ) ;
1531+ if ( ! willRetry && channelContext !== null && testChannel . error . hasSubscribers ) {
1532+ publishError ( err ) ;
1533+ }
15191534 try { await afterEach ( ) ; } catch { /* test is already failing, let's ignore the error */ }
1520- try { await after ( ) ; } catch { /* Ignore error. */ }
1535+ if ( ! willRetry ) {
1536+ try { await after ( ) ; } catch { /* Ignore error. */ }
1537+ }
15211538 } finally {
15221539 stopPromise ?. [ SymbolDispose ] ( ) ;
15231540 }
15241541
1542+ // Publish the tracing channel `end` once per attempt so each `start`
1543+ // (published when the body runs) is paired, keeping spans balanced even
1544+ // when a flaky test is retried.
1545+ if ( channelContext !== null && testChannel . end . hasSubscribers ) {
1546+ publishEnd ( ) ;
1547+ }
1548+
15251549 // Decide whether to retry based on the resulting state, not on whether the
15261550 // attempt threw: a non-throwing failure via this.fail() must also be
1527- // retried. An external abort (user signal, parent, or process cancellation)
1528- // is intentional and stops retrying immediately. What remains -
1529- // timeouts and ordinary failures - is retried.
1530- if ( this . passed ) {
1551+ // retried, and a failure that surfaces only through a subtest (promoted to
1552+ // the parent later, in postRun) is detected here so the parent still
1553+ // retries. An external abort and an expectFailure verdict stop retrying
1554+ // immediately.
1555+ const subtestsFailed = this . flakyRetries > 0 &&
1556+ ArrayPrototypeSome ( this . subtests , ( subtest ) => ! subtest . passed && ! subtest . isTodo ) ;
1557+ if ( this . passed && ! subtestsFailed ) {
15311558 break ;
15321559 }
15331560 const shouldRetry =
15341561 attempt < maxAttempts &&
1535- ! this . passed &&
1562+ ( ! this . passed || subtestsFailed ) &&
1563+ ! this . expectFailure &&
15361564 ! ( this . signal . aborted || this . outerSignal ?. aborted ) ;
15371565 if ( ! shouldRetry ) {
15381566 break ;
@@ -1546,11 +1574,6 @@ class Test extends AsyncResource {
15461574 this . abortController . abort ( ) ;
15471575 }
15481576
1549- // Publish diagnostics_channel end event if the channel has subscribers (in both success and error cases)
1550- if ( channelContext !== null && testChannel . end . hasSubscribers ) {
1551- publishEnd ( ) ;
1552- }
1553-
15541577 if ( this . parent !== null || typeof this . hookType === 'string' ) {
15551578 // Clean up the test. Then, try to report the results and execute any
15561579 // tests that were pending due to available concurrency.
0 commit comments