Skip to content

test(node:fs): speed up and fix the AbortSignal leak detector - #36206

Open
robobun wants to merge 8 commits into
mainfrom
farm/ac263b55/speedup-fs-abortsignal-leak-test
Open

robobun wants to merge 8 commits into
mainfrom
farm/ac263b55/speedup-fs-abortsignal-leak-test

Conversation

@robobun

@robobun robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

Cuts the wall time of test/js/node/fs/abort-signal-leak-read-write-file.test.ts by ~18x on release lanes, makes it pass under bun bd test instead of timing out, and turns the heapStats AbortSignal check into an assertion that can actually fail on the regression it guards.

Why

The existing fixture ran 100,000 iterations in a spawned subprocess and asserted two things: heapStats().objectTypeCounts.AbortSignal <= 10 and rss < 200 MB. While reviewing this I reintroduced the leak locally (removed the pending_activity_unref() from ReadFile/WriteFile Drop in src/runtime/node/node_fs.rs) and ran the fixture: the AbortSignal count stayed at 3. A JSAbortSignal wrapper is only pinned by hasPendingActivity() when the signal is not aborted and has an abort listener (JSAbortSignalCustom.cpp), and every signal in the old fixture is aborted by the time GC runs, so the object-count assertion could never have failed on this leak. The RSS check at 100,000 iterations was the only live detector, and only by a marginal amount.

The file was also timing out under bun bd test locally (the subprocess alone cost ~4 min under debug+ASAN against the 5s default), and was quarantined on ASAN CI for the same reason.

How

  • Run the leak check in-process instead of via a spawned fixture. The only reason for the subprocess was RSS isolation, and no RSS bound at a reasonable iteration count distinguishes the regression from healthy without risking flake, so the subprocess is dropped along with the RSS cap.
  • Add a detector path: a never-aborted signal with a no-op abort listener passed to both readFile and writeFile. With pending_activity_unref() removed from ReadFile::drop, the in-process test reports 102 live wrappers at 100 iterations vs ~2 with the fix in place, so the heapStats threshold of 10 is now a real detector.
  • Keep the three original behaviour paths (pre-aborted readFile, pre-aborted writeFile, abort-during-writeFile) as single assertions that verify the rejection is an AbortError; the old fixture swallowed all errors with empty catch blocks.
  • Scale the detector loop by isDebug || isASAN (100 vs 2,000) so debug+ASAN completes under the default test timeout without a per-test override.
  • Remove the stale [ ASAN ] quarantine from test/expectations.txt.

Verification

Commented out signal.pending_activity_unref() in ReadFile::drop and rebuilt:

Expected: <= 10
Received: 102
(fail) fs.promises readFile/writeFile does not leak AbortSignal

With a clean src/:

(pass) fs.promises readFile/writeFile does not leak AbortSignal

Timings

build before after
linux x64 release 2267 ms ~130 ms
windows x64 release 4785 ms ~260 ms
windows aarch64 CI ~17 s ~1 s (extrapolated)
linux x64 debug+ASAN timeout (fixture ~252 s) 0.7 - 2.6 s

[stamp-90s] gate passed · iteration 3 · 3 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/node/fs/abort-signal-leak-read-write-file.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/node/fs/abort-signal-leak-read-write-file.test.ts
bun test v1.4.0 (862e37c36)

test/js/node/fs/abort-signal-leak-read-write-file.test.ts:
(pass) fs.promises readFile/writeFile does not leak AbortSignal [520.73ms]

 1 pass
 0 fail
 3 expect() calls
Ran 1 test across 1 file. [3.12s]
Exit: 0
diff hotspot
test/expectations.txt                              |  1 -
 .../abort-signal-leak-read-write-file-fixture.ts   | 41 ------------------
 .../fs/abort-signal-leak-read-write-file.test.ts   | 48 ++++++++++++++++++++--
 3 files changed, 45 insertions(+), 45 deletions(-)

gate history · 5 passed · 1 rejected · iteration 3

evidence per changed file
file                                                      reads  edits  tests
test/expectations.txt                                         2      1      0
…js/node/fs/abort-signal-leak-read-write-file-fixture.ts      6      6      0
…st/js/node/fs/abort-signal-leak-read-write-file.test.ts      6      6      0

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

AbortSignal leak test

Layer / File(s) Summary
Structured fixture scenarios
test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts
The fixture tests pre-aborted, delayed-abort, and never-aborted signal scenarios, counts unexpected errors, and validates post-GC AbortSignal object counts.
Spawned fixture validation
test/js/node/fs/abort-signal-leak-read-write-file.test.ts, test/expectations.txt
The parent test validates fixture output, stderr, and exit status, while the obsolete ASAN leak expectation is removed.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: speeding up and fixing the AbortSignal leak detector.
Description check ✅ Passed It covers the required What and verification details, though it uses custom headings instead of the template wording.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:04 AM PT - Jul 29th, 2026

❌ @robobun, your commit 862e37c has 3 failures in Build #84634 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36206

That installs a local version of the PR into your bun-36206 executable, so you can run:

bun-36206 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — test-only speedup with strictly tighter assertions; the RSS-threshold nit below is a tidy-up, not a blocker (the heapStats AbortSignal count remains the load-bearing detector with ~1500× headroom).

Extended reasoning...

Overview

Test-only change to test/js/node/fs/abort-signal-leak-read-write-file{.test,-fixture}.ts. Drops the fixture loop from 100k → 5k iterations, replaces empty catch {} blocks with an explicit AbortError check, swaps .toRun() for Bun.spawn with concurrent stdout/stderr/exited draining and structured JSON assertions, and adds a 30s per-test timeout so debug+ASAN completes instead of silently timing out at 5s.

Security risks

None. No production code touched; the fixture writes only to a tmpdirSync() path and spawns nothing external.

Level of scrutiny

Low-to-medium. This is a leak-test refactor with no runtime code changes. The key question is whether the reduced iteration count still catches the #16788 regression — it does: 5k iterations × 3 signals would surface ~15,000 live AbortSignal objects against a threshold of 10, so the heapStats check fails loudly. The one weakened assertion is the trailing RSS cap (see inline nit), which was already a secondary backstop.

Other factors

  • The rewrite is strictly stricter than before: previously all rejections were swallowed; now a non-AbortError outcome fails the test and is surfaced in the JSON summary.
  • Follows repo test conventions: bunEnv/bunExe, await using proc, Promise.all over stdout/stderr/exited, stderr/stdout asserted before exitCode.
  • JSON.parse(stdout) is safe here — the fixture emits exactly one console.log(JSON.stringify(...)) line and bunEnv quiets debug logs.
  • PR description includes measured before/after timings across four build configurations, indicating the author actually ran it.

Comment thread test/js/node/fs/abort-signal-leak-read-write-file-fixture.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the vacuous RSS cap I flagged earlier was dropped in 1f20728, and the remaining heapStats AbortSignal-count check still has ~600× headroom against the #16788 regression at 2,000 iterations.

What was reviewed:

  • Confirmed all three original code paths (pre-aborted readFile, pre-aborted writeFile, abort-during-writeFile) are preserved.
  • The .then(() => nonAbortErrors++, check) vs .catch(check) split correctly allows the race-y third case to resolve successfully without flagging.
  • Spawn wrapper drains stdout/stderr concurrently and asserts output before exit code per harness conventions; dead isASAN import removed.
Extended reasoning...

Overview

Test-only change to test/js/node/fs/abort-signal-leak-read-write-file{.test,-fixture}.ts. The fixture's iteration count drops 100k → 2k, empty catch blocks become AbortError assertions, and the test wrapper switches from .toRun() to an explicit Bun.spawn that parses a JSON summary and asserts on stderr / AbortSignal count / non-abort-error count before the exit code. The absolute RSS cap was removed in the follow-up commit after my earlier note that it could no longer trip at the reduced iteration count.

Security risks

None. Pure test-infrastructure change with no runtime code touched.

Level of scrutiny

Low-to-moderate. The main REVIEW.md concern for this class of change is "never silently weaken an existing test". I checked each removed assertion:

  • The 100k → 2k reduction: the load-bearing detector is heapStats().objectTypeCounts.AbortSignal > 10. If #16788 regresses, 2,000 iterations × 3 signals ≈ 6,000 live signals vs. threshold 10 — still fails decisively. The extra 98k iterations added no detection headroom.
  • The dropped RSS cap: at 2k iterations the leak's RSS delta (~11 MB release / ~20 MB ASAN, per the author's measurement) sits inside normal jitter, so the cap was already vacuous. Dropping it with a stated reason satisfies the "every deletion needs a stated reason" rule; RSS stays in the JSON output for diagnostics.
  • The empty catch {} blocks were strengthened into a nonAbortErrors counter that fails the test if any rejection isn't an AbortError, and the pre-aborted cases now also fail if the promise unexpectedly resolves.

Other factors

  • My prior inline comment is resolved by 1f20728.
  • Harness conventions are followed: bunEnv/bunExe, Promise.all over both pipes and proc.exited, stderr/stdout asserted before exit code, per-test 30s timeout instead of a file-wide default.
  • The abort-during-writeFile case correctly uses .catch(check) (not .then(fail, check)) since the write may legitimately complete before nextTick fires the abort — avoids a flake the stricter form would introduce.
  • No production code, no CODEOWNER paths, no outstanding reviewer comments.

@robobun robobun changed the title test(node:fs): speed up and tighten the AbortSignal leak test test(node:fs): speed up and fix the AbortSignal leak detector Jul 28, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the earlier RSS-cap nit was addressed in 1f20728/38b72e21 and the detector is now demonstrably live.

What was reviewed:

  • New never-aborted-signal-with-listener path against JSAbortSignalCustom.cpp semantics — it's the only shape that hasPendingActivity() pins, so the heapStats threshold can now fail.
  • Abort-later writeFile path uses .catch(mustAbort) (not .then(bump, mustAbort)), so a write that races ahead of nextTick abort correctly counts as success rather than a non-abort error.
  • Spawn wrapper drains stdout/stderr/exited concurrently and asserts stderr/counts before exit code; bunEnv keeps debug logging off so stderr === "" holds under bun bd.
Extended reasoning...

Overview

Test-only change to test/js/node/fs/abort-signal-leak-read-write-file{.test,-fixture}.ts. Cuts the fixture from 100k iterations to 200 (debug/ASAN) / 2,000 (release), adds a never-aborted signal + abort listener path so the heapStats AbortSignal count can actually trip on a missing pending_activity_unref(), replaces empty catch {} blocks with an AbortError-name check, drops the now-unfalsifiable RSS cap (keeping RSS in the diagnostic JSON), and swaps .toRun() for an explicit Bun.spawn that parses the JSON summary and asserts stderr / counts / exit code in the recommended order.

Security risks

None. Test-only; no production code touched, no network, no untrusted input.

Level of scrutiny

Low-to-moderate. The risk with leak-test edits is silently weakening coverage. Here the opposite happened: the PR description shows the old heapStats assertion could not fail on the #16788 regression (every signal was aborted by GC time, so hasPendingActivity() never pinned the wrapper), and the new never-aborted-with-listener path was empirically verified to report ~200 live wrappers vs the threshold of 10 when pending_activity_unref() is removed. That satisfies REVIEW.md's "confirm deleting each load-bearing clause of your fix breaks at least one test" for the detector itself.

Other factors

  • My earlier inline comment about the vacuous RSS cap was addressed (dropped, with measured justification that no RSS bound distinguishes leak from healthy at this iteration count).
  • Checked the abort-later path for a false-positive: .catch(mustAbort) (rather than .then(onFulfilled, mustAbort)) means a write that completes before process.nextTick fires is treated as success, not a non-abort error — correct.
  • Iteration scaling on isDebug || isASAN keeps the fixture under the default 5s test timeout (~3.2s reported for debug+ASAN) without a per-test override.
  • ?? 0 on objectTypeCounts.AbortSignal avoids undefined > 10 silently passing if the type key is absent.

robobun added 3 commits July 28, 2026 21:00
The fixture ran 100,000 iterations, which cost ~17s on the Windows
aarch64 lane and several minutes under debug+ASAN (so the test was
silently timing out under 'bun bd test' locally).

The primary assertion is the heapStats AbortSignal object count, which
would report ~15,000 live signals at 5,000 iterations if the #16788
regression ever came back, versus the threshold of 10. Cut the loop to
5,000, keep all three code paths (pre-aborted readFile/writeFile and
abort-during-writeFile), and additionally assert that every rejection
is actually an AbortError rather than swallowing all errors.

Replace the opaque .toRun() with an explicit Bun.spawn that surfaces
the live-signal count, RSS, and any unexpected stderr on failure.
At the reduced iteration count the #16788 leak would only add tens of MB
of RSS, so the 200/700 MB absolute cap could no longer fail on the bug
it was written for. The heapStats AbortSignal count is the load-bearing
detector; keep RSS in the JSON summary for diagnostics only.

2,000 iterations keeps the object-count headroom at ~600x while leaving
the debug+ASAN subprocess comfortably under the per-test timeout.
Empirically verified that the existing heapStats AbortSignal count was
never able to fail on the #16788 regression: JSAbortSignal wrappers are
only pinned by hasPendingActivity() when the signal is not aborted AND
has an abort listener, and every signal in the old fixture was aborted
by the time GC ran.

Add a never-aborted-signal-with-listener path to the loop. With
pending_activity_unref() removed from ReadFile/WriteFile Drop, this
reports ~200 live wrappers at 200 iterations vs ~3 with the fix in
place, so the object-count assertion is now a real detector.

Scale ITERATIONS by isDebug/isASAN (200 vs 2,000) instead of carrying a
per-test timeout, matching the pattern used by other leak tests.
@robobun
robobun force-pushed the farm/ac263b55/speedup-fs-abortsignal-leak-test branch from 38b72e2 to 4090c23 Compare July 28, 2026 21:00

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — test-only change that makes the detector actually fail on the regression it guards, and the earlier RSS-cap feedback was addressed. The inline note about the stale expectations.txt ASAN quarantine is worth cleaning up (otherwise the isASAN branch never runs in CI), but it's not a regression from today and doesn't block.

Extended reasoning...

Overview

Two test files: the abort-signal-leak-read-write-file fixture and its wrapper test. The fixture drops from 100k to 200/2,000 iterations (branched on isDebug || isASAN), adds a fourth loop path (never-aborted signal + listener) so heapStats().objectTypeCounts.AbortSignal can actually exceed the threshold when pending_activity_unref() is missing, replaces empty catch {} blocks with an AbortError name check, and drops the now-vacuous RSS cap (per my earlier comment, which the author addressed with measurements). The wrapper test replaces .toRun() with an explicit Bun.spawn that drains stdout/stderr concurrently, parses the JSON summary, and asserts stderr/counts before exitCode.

Security risks

None. Test-only; no production code, no untrusted input, no network.

Level of scrutiny

Low-to-moderate. It's test infrastructure, but leak tests are easy to make vacuous. The key claims check out: the author verified the new detector fires (203 vs 3) by reintroducing the leak; the abort-during-write path is race-tolerant (.catch(mustAbort) only fires on rejection, so a fast write that completes before nextTick doesn't spuriously bump nonAbortErrors); the never-aborted detector path can't reject, so no error handling is needed there; bunEnv keeps stderr clean for the toBe("") assertion.

Other factors

  • The one open finding is a nit: test/expectations.txt:88 still quarantines this file on ASAN with a comment naming the old test title. Worst case the ASAN lane keeps skipping the file exactly as it does today — nothing regresses.
  • Harness conventions followed: Promise.all on stdout/stderr/exited, bunEnv/bunExe, exit code asserted last, iteration count branched on isDebug/isASAN, no per-test timeout override.
  • Previous review round's feedback (RSS cap) was resolved with a reasoned reply and code change.

Comment thread test/js/node/fs/abort-signal-leak-read-write-file.test.ts
The entry was added when the RSS check tripped under ASAN's elevated
baseline. That check is gone and the fixture now runs ~3s under
debug+ASAN, so there is no reason to keep skipping the file there.
@robobun
robobun requested a review from Jarred-Sumner as a code owner July 28, 2026 21:15

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — both earlier nits (the vacuous RSS cap and the stale ASAN quarantine) are addressed, and the detector now demonstrably fails on the reintroduced leak.

What was reviewed:

  • Verified the new never-aborted-signal-with-listener path is what makes heapStats pin the wrapper per the hasPendingActivity contract — the old fixture's all-aborted signals could never trip it.
  • Checked the abort-during-writeFile path's .catch(mustAbort) correctly tolerates the write completing before nextTick fires (4-byte write), so nonAbortErrors won't false-positive there.
  • Confirmed bunEnv/bunExe, concurrent pipe drain, and exit-code-last ordering match harness conventions; JSON.parse(stdout) is safe since the fixture writes exactly one line.
Extended reasoning...

Overview

Test-only PR touching three files: rewrites abort-signal-leak-read-write-file-fixture.ts to use a heapStats-based detector that can actually fail on the #16788 regression, replaces the .toRun() wrapper in the test file with an explicit Bun.spawn that parses a JSON summary and asserts on stderr / AbortSignal count / non-abort-error count before the exit code, and removes the now-obsolete [ ASAN ] quarantine from test/expectations.txt. No production code is touched.

Security risks

None. The change is confined to a leak regression test and a CI quarantine list; there is no user-facing surface, no parsing of untrusted input, and no auth/crypto/permissions code.

Level of scrutiny

Low-to-moderate. This is test infrastructure with no runtime impact. The main risks for a change like this are (a) weakening the assertion the test was guarding, or (b) introducing flake. Both were examined: the PR description includes a direct verification that reintroducing the leak (pending_activity_unref() removed) yields numAbortSignalObjects: 203 against a threshold of 10, so the detector is strictly stronger than before (the old heapStats check was shown to stay at 3 under the same regression). The RSS cap that was dropped was already vacuous at the reduced iteration count, per the earlier review thread and the author's measurements. The un-quarantine is justified by the ~3s debug+ASAN timing and the file remaining in no-validate-leaksan.txt for LSan.

Other factors

Both prior review comments from earlier runs were addressed with follow-up commits and the threads are resolved. The test follows repo conventions: bunEnv/bunExe, Promise.all on stdout/stderr/exited to avoid pipe deadlock, ITERATIONS branched on isDebug || isASAN, stdout/stderr asserted before exit code. The .then(() => nonAbortErrors++, mustAbort) shape on the pre-aborted paths correctly counts an unexpected resolution as a failure, and the .catch(mustAbort) on the abort-during-write path correctly tolerates the write winning the race with process.nextTick. The fixture still uses tmpdirSync rather than tempDir, but that predates this PR and isn't a regression.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — both earlier nits (vacuous RSS cap, stale ASAN quarantine) are addressed and the current diff is a clean test-only improvement.

What was reviewed:

  • Fixture's new never-aborted-signal-with-listener path against hasPendingActivity() semantics — it's the case that actually pins the wrapper, and the author empirically proved the threshold of 10 trips (203 live) with pending_activity_unref() removed.
  • Abort-later writeFile path uses .catch(mustAbort) without asserting rejection — correct, since a 4-byte write can legitimately complete before nextTick.
  • Test wrapper follows harness conventions (bunEnv/bunExe, concurrent pipe drain, stderr asserted before JSON.parse and exitCode); no-validate-leaksan.txt still covers LSan so dropping the expectations.txt line only removes the timeout-era skip.
Extended reasoning...

Overview

Test-only change touching three files: rewrites the fs.promises.readFile/writeFile AbortSignal leak fixture (abort-signal-leak-read-write-file-fixture.ts), replaces the .toRun() wrapper with an explicit Bun.spawn that parses a JSON summary (abort-signal-leak-read-write-file.test.ts), and removes one stale [ ASAN ] quarantine line from test/expectations.txt. No runtime code is touched.

Security risks

None. Test infrastructure only; no user-facing surface, no parsing of untrusted input, no auth/crypto/permissions.

Level of scrutiny

Low-to-medium. It's test-only, but leak-detector tests are easy to get subtly wrong (vacuous assertions, thresholds that never trip). The author did the right thing here: they reintroduced the #16788 leak locally by commenting out pending_activity_unref() in ReadFile::drop/WriteFile::drop, showed the old heapStats check couldn't fail (every signal was aborted by GC time, so hasPendingActivity() returned false regardless), and demonstrated the new never-aborted+listener path yields ~203 live wrappers vs ~3 healthy at 200 iterations. That's exactly the "prove the test fails for the right reason" bar from REVIEW.md.

Other factors

  • I left two prior nits on this PR (RSS cap now vacuous at reduced iterations; stale ASAN quarantine). Both were addressed in follow-up commits (ca4226f dropped the RSS cap with measured justification; 0ff4d9c removed the quarantine). All threads are resolved.
  • The wrapper follows CLAUDE.md's spawn pattern exactly: Promise.all([stdout.text(), stderr.text(), exited]), asserts stderr first so any fixture crash surfaces before JSON.parse chokes, and checks exitCode last.
  • The abort-during-write path deliberately does not assert rejection (only .catch(mustAbort)) because a 4-byte write can race ahead of process.nextTick — same tolerance the old fixture had, but now non-AbortError rejections are counted instead of swallowed.
  • Un-quarantining on ASAN is safe: the file remains in no-validate-leaksan.txt for LSan-at-exit, and the fixture now completes in ~3s under debug+ASAN, so the only way ASAN CI goes red is a real regression in the assertion — which is desirable.
  • Iteration scaling (isDebug || isASAN ? 200 : 2_000) matches the pattern used by other leak tests in the suite and keeps the file under the default timeout without a per-test override.

robobun and others added 2 commits July 29, 2026 01:02
Spawning the fixture under debug+ASAN carried ~2.5s of subprocess
startup before the loop even ran, pushing the test past the default 5s
timeout on loaded machines. The only reason for the subprocess was RSS
isolation, and the RSS check is gone, so fold the detector into the test
file and drop the fixture. The heapStats object-count assertion works
identically in-process.
Comment thread test/js/node/fs/abort-signal-leak-read-write-file.test.ts Outdated
This file runs in the --parallel bucket on release CI where a worker
executes several test files on one VM; wrappers left over from earlier
files would count toward an absolute threshold. Use the same baseline +
expectMaxObjectTypeCount pattern as pipeTo-signal-leak.test.ts.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — test-only change; all three prior review points (RSS cap, stale ASAN quarantine, baseline-delta for parallel-bucket safety) are addressed in 862e37c.

What was reviewed:

  • Detector loop uses never-aborted signals with a listener so hasPendingActivity() actually pins leaked wrappers; author verified 102 vs ~2 with the fix reverted.
  • .rejects.toMatchObject calls are awaited; the delayed-abort race accepts either outcome; expectMaxObjectTypeCount handles GC+retry and asserts against baseline + 10.
  • tempDir + using for cleanup; iteration count branches on isDebug || isASAN; fixture file and its expectations.txt entry both removed.
Extended reasoning...

Overview

Test-only PR touching three files: rewrites test/js/node/fs/abort-signal-leak-read-write-file.test.ts to run the AbortSignal leak check in-process instead of via a spawned 100k-iteration fixture, deletes the now-unused fixture, and removes the stale [ ASAN ] quarantine from test/expectations.txt. No production code is modified.

Security risks

None. This changes only test infrastructure — no runtime, auth, crypto, or user-facing code paths.

Level of scrutiny

Low-to-moderate. The bar for a test rewrite is (a) it still detects the regression it guards, (b) it doesn't flake, and (c) it doesn't silently weaken coverage. All three are satisfied: the PR description shows the new detector fails with 102 live wrappers when pending_activity_unref() is removed from ReadFile::drop and passes (~2) with it present, so the assertion is demonstrably live. The old fixture's heapStats check was shown to be vacuous (aborted signals aren't pinned by hasPendingActivity()), so this is a strict improvement in coverage, not a weakening. The three original behaviour paths (pre-aborted read, pre-aborted write, abort-during-write) are preserved as explicit AbortError assertions rather than swallowed via empty catch.

Other factors

Three rounds of review feedback from prior runs were all incorporated: the RSS check was dropped after measurement showed it couldn't distinguish the regression at the reduced iteration count; the stale expectations.txt entry was removed so the test actually runs on ASAN CI; and the absolute heapStats threshold was converted to a delta from a captured baseline via the shared expectMaxObjectTypeCount helper, matching the sibling pipeTo-signal-leak.test.ts pattern and avoiding parallel-bucket flake. The test uses tempDir/using for hermetic cleanup, awaits every .rejects, and scales iterations by isDebug || isASAN to stay under the default timeout without a per-test override. Straightforward, well-verified, and fully test-scoped.

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

The diff here is green: abort-signal-leak-read-write-file.test.ts passed on every lane that ran, including the debian x64-asan lane it was previously quarantined on.

The red on builds 84358 and 84634 is unrelated to this test-only change:

  • Multiple build-bun jobs timed out across both builds (CI infra; a test-only diff cannot affect the native build).
  • worker-transfer-terminate-stress.test.ts hit a JSC ExceptionScope::assertNoException() assertion on x64-asan (pre-existing, tracked separately).
  • proxy-stress-protocol.test.ts hit an ECONNRESET on x64-asan (unrelated http proxy test, tracked separately).

Ready for a maintainer to merge past the unrelated red.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant