Skip to content

feat: first commit flaky#61746

Open
vespa7 wants to merge 4 commits intonodejs:mainfrom
vespa7:test-runner/feat/flaky-test-flag
Open

feat: first commit flaky#61746
vespa7 wants to merge 4 commits intonodejs:mainfrom
vespa7:test-runner/feat/flaky-test-flag

Conversation

@vespa7
Copy link
Copy Markdown
Contributor

@vespa7 vespa7 commented Feb 8, 2026

@nodejs-github-bot
Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/test_runner

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Feb 8, 2026
@JakobJingleheimer JakobJingleheimer self-requested a review February 9, 2026 18:15
Copy link
Copy Markdown
Member

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far. I expected to see a loop somewhere; did you merely not get to that yet (since this is a draft) or did I miss some magic somewhere?

PS Sorry this took me a while to get to.

Comment on lines +74 to +92
@@ -87,6 +87,9 @@ function formatTestReport(type, data, showErrorDetails = true, prefix = '', inde
}
} else if (expectFailure !== undefined) {
title += ` # EXPECTED FAILURE`;
} else if (flaky !== undefined && flaky > 0) {
const retryText = flaky === 1 ? 're-try' : 're-tries';
title += ` # FLAKY ${flaky} ${retryText}`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same (RE var name)

let { fn, name, parent } = options;
const { concurrency, entryFile, expectFailure, loc, only, timeout, todo, skip, signal, plan } = options;

const { concurrency, entryFile, expectFailure, flaky, loc, only, timeout, todo, skip, signal, plan } = options;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is getting really long

Suggested change
const { concurrency, entryFile, expectFailure, flaky, loc, only, timeout, todo, skip, signal, plan } = options;
const {
entryFile,
expectFailure,
flaky,
loc,
only,
plan,
signal,
skip,
timeout,
todo,
concurrency,
} = options;

Comment thread package-lock.json Outdated
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should not be committed?

Comment on lines +68 to +84
@@ -78,6 +78,10 @@ function reportTest(nesting, testNumber, status, name, skip, todo, expectFailure
line += ` # TODO${typeof todo === 'string' && todo.length ? ` ${tapEscape(todo)}` : ''}`;
} else if (expectFailure !== undefined) {
line += ' # EXPECTED FAILURE';
//should we use flaky >=0 here? for always printing 0 retries
} else if (flaky !== undefined && flaky > 0) {
const retryText = flaky === 1 ? 're-try' : 're-tries';
line += ` # FLAKY ${flaky} ${retryText}`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the feature proposal, the output cites the number of re-tries it took to succeed. So I think the name for flaky should be changed to something like flakyRetriedCount to disambiguate it with the maximum number of re-tries to attempt.

# FLAKY 42 re-tries

@Han5991
Copy link
Copy Markdown
Contributor

Han5991 commented Mar 22, 2026

Hi @vespa7, are you still planning to continue this PR?

If not, I’d like to pick this up and open a follow-up PR based on the same idea. If that works for you, I’ll make sure to credit your original work.

@vespa7
Copy link
Copy Markdown
Contributor Author

vespa7 commented Mar 22, 2026

Hi @vespa7, are you still planning to continue this PR?

If not, I’d like to pick this up and open a follow-up PR based on the same idea. If that works for you, I’ll make sure to credit your original work.

Yes I will publish the pr tomorrow

Copy link
Copy Markdown
Member

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

I agree the formatting changes are improvements, but they're not germane to this PR, and they make it rather difficult to see the real changes, so please undo them 😰

Comment thread doc/api/test.md Outdated
* `testNumber` {number} The ordinal number of the test.
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
* `flakyRetriedCount` {number|undefined} The number of retries taken for a
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little wordy to me. Maybe just retryCount?

@Han5991
Copy link
Copy Markdown
Contributor

Han5991 commented Apr 13, 2026

@vespa7 Are you still working on it? If you don't mind, I'd like to work on it!

@vespa7
Copy link
Copy Markdown
Contributor Author

vespa7 commented Apr 13, 2026

@vespa7 Are you still working on it? If you don't mind, I'd like to work on it!

Yes, still working on it

@vespa7 vespa7 marked this pull request as ready for review April 13, 2026 10:18
Copilot AI review requested due to automatic review settings April 22, 2026 16:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds initial support for “flaky” tests in the Node.js test runner, enabling automatic retries and exposing retry info in reporter output and docs.

Changes:

  • Implement flaky option parsing/inheritance and retry execution in internal/test_runner/test.js.
  • Propagate/display flaky retry metadata (retryCount) across TestsStream and multiple reporters (TAP, dot, JUnit, human-readable utils).
  • Add TAP output fixtures/snapshots and document the new API surface (*.flaky() shorthands + options/events updates).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/test-runner/test-output-flaky.mjs Adds snapshot-based test to validate flaky reporter output.
test/fixtures/test-runner/output/flaky.js Fixture exercising flaky retries, suite inheritance, and invalid inputs.
test/fixtures/test-runner/output/flaky.snapshot Expected TAP output including flaky directives and summary counter.
lib/internal/test_runner/test.js Core implementation: parse flaky, run retry loop, report retry directive, add flaky summary line.
lib/internal/test_runner/tests_stream.js Adds getFlaky() directive payload (retryCount).
lib/internal/test_runner/utils.js Increments new harness.counters.flaky counter for flaky tests.
lib/internal/test_runner/harness.js Initializes/resets counters.flaky; exposes .flaky shorthand on test/suite factories.
lib/internal/test_runner/reporter/utils.js Adds # FLAKY … annotation in formatted output when retryCount > 0.
lib/internal/test_runner/reporter/tap.js Emits # FLAKY … in TAP lines when retryCount > 0.
lib/internal/test_runner/reporter/junit.js Adds a <property name="flaky" …> when retryCount > 0.
lib/internal/test_runner/reporter/dot.js Renders a distinct marker for flaky-pass tests (based on retryCount).
doc/api/test.md Documents flaky option + shorthands + retryCount event field.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1365 to 1367
}

this.pass();
await afterEach();
await after();
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retries currently re-run only the test body; beforeEach is executed once before the loop and afterEach is executed once after the loop (and is additionally wrapped in runOnce). If the intended semantics of “re-run the test” include per-attempt hooks/cleanup, the hooks need to run inside the retry loop (and afterEach should not be runOnce), otherwise state can leak across attempts.

Copilot uses AI. Check for mistakes.
Comment thread doc/api/test.md
* `todo` {string|boolean|undefined} Present if [`context.todo`][] is called
* `skip` {string|boolean|undefined} Present if [`context.skip`][] is called
* `retryCount` {number|undefined} The number of retries taken for a
flaky test. Present when a test is marked as flaky.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say retryCount is present on 'test:fail' whenever a test is marked as flaky, but the current implementation only adds retryCount via the getFlaky() directive when a test passes after retries (see Test#getReportDetails(), which only sets the flaky directive when this.passed). Either update the docs to match the actual emitted data, or include retryCount in fail events as well (likely with the number of retries attempted).

Suggested change
flaky test. Present when a test is marked as flaky.
flaky test. Present only when flaky metadata is included in the event.

Copilot uses AI. Check for mistakes.
Comment thread doc/api/test.md
Comment thread doc/api/test.md
Comment thread doc/api/test.md
Comment thread doc/api/test.md
ArrayPrototypePush(promises, stopPromise);
try {
const runArgs = ArrayPrototypeSlice(args);
ArrayPrototypeUnshift(runArgs, this.fn, ctx);
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within the flaky retry loop the test is invoked via ArrayPrototypeUnshift(runArgs, this.fn, ctx), which bypasses the testFn wrapper created earlier (used to preserve testChannel.start.runStores(...) / AsyncLocalStorage context). Retried executions should call the same wrapped function that non-flaky execution would, otherwise diagnostics_channel subscribers see different behavior and context propagation can break.

Suggested change
ArrayPrototypeUnshift(runArgs, this.fn, ctx);
ArrayPrototypeUnshift(runArgs, testFn, ctx);

Copilot uses AI. Check for mistakes.
Comment on lines +1356 to +1360
this.pass();
break;
} catch (attemptErr) {
if (attempt < maxAttempts) {
this.retriesTaken = attempt;
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry loop only retries when the test body throws/rejects (caught by catch (attemptErr)), but many failures are recorded via this.fail(...) without throwing (e.g. t.fail(), callback+promise misuse, plan failures, etc.). In those cases this.pass() returns early with this.error !== null and the code still breaks, so flaky tests won't actually retry. After running an attempt, the loop should detect !this.passed/this.error !== null and continue (or throw) when attempt < maxAttempts.

Copilot uses AI. Check for mistakes.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 56.69291% with 55 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.60%. Comparing base (3a53447) to head (b9544b1).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/test_runner/test.js 73.17% 19 Missing and 3 partials ⚠️
lib/internal/test_runner/reporter/junit.js 0.00% 20 Missing ⚠️
lib/internal/test_runner/reporter/tap.js 50.00% 2 Missing and 1 partial ⚠️
lib/internal/test_runner/reporter/utils.js 25.00% 2 Missing and 1 partial ⚠️
lib/internal/test_runner/utils.js 25.00% 3 Missing ⚠️
lib/internal/test_runner/reporter/dot.js 60.00% 2 Missing ⚠️
lib/internal/test_runner/tests_stream.js 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61746      +/-   ##
==========================================
- Coverage   89.60%   89.60%   -0.01%     
==========================================
  Files         706      706              
  Lines      219179   219262      +83     
  Branches    41993    42005      +12     
==========================================
+ Hits       196400   196468      +68     
- Misses      14675    14678       +3     
- Partials     8104     8116      +12     
Files with missing lines Coverage Δ
lib/internal/test_runner/harness.js 86.17% <100.00%> (+0.02%) ⬆️
lib/internal/test_runner/reporter/dot.js 95.45% <60.00%> (-4.55%) ⬇️
lib/internal/test_runner/tests_stream.js 87.43% <50.00%> (-0.81%) ⬇️
lib/internal/test_runner/reporter/tap.js 93.58% <50.00%> (-1.30%) ⬇️
lib/internal/test_runner/reporter/utils.js 91.50% <25.00%> (-3.64%) ⬇️
lib/internal/test_runner/utils.js 64.32% <25.00%> (+0.73%) ⬆️
lib/internal/test_runner/reporter/junit.js 84.15% <0.00%> (-10.33%) ⬇️
lib/internal/test_runner/test.js 95.62% <73.17%> (-1.28%) ⬇️

... and 35 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JakobJingleheimer JakobJingleheimer added the test_runner Issues and PRs related to the test runner subsystem. label Apr 22, 2026
@JakobJingleheimer
Copy link
Copy Markdown
Member

@vespa7 heads up: the first commit must follow a specific format, something like test_runner: support retrying tests marked "flaky" (the PR title should also be updated).

const retryText = flaky === 1 ? 're-try' : 're-tries';
line += ` # FLAKY ${flaky} ${retryText}`;
line += ` # EXPECTED FAILURE${typeof expectFailure === 'string' ? ` ${tapEscape(expectFailure)}` : ''}`;
} else if (retryCount !== undefined && retryCount > 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think you don't need the retryCount !== undefined because undefined > 0 → false

Suggested change
} else if (retryCount !== undefined && retryCount > 0) {
} else if (retryCount > 0) {

} else if (flaky !== undefined && flaky > 0) {
const retryText = flaky === 1 ? 're-try' : 're-tries';
title += ` # FLAKY ${flaky} ${retryText}`;
} else if (retryCount !== undefined && retryCount > 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Suggested change
} else if (retryCount !== undefined && retryCount > 0) {
} else if (retryCount > 0) {

@avivkeller
Copy link
Copy Markdown
Member

@vespa7 Can you update your commit + PR title to actually explain the change? Additionally, please add a proper description

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. test_runner Issues and PRs related to the test runner subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants