Skip to content

inspector: share TestReporter test ID counter between live and retroactive paths - #36522

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/593de83d/testreporter-id-collision
Aug 1, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/593de83d/testreporter-id-collision

Conversation

@robobun

@robobun robobun commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Adopts the fix from #34221 by @hughescr.

Problem

TestReporter test/describe IDs came from two independent counters:

  • the live-registration path (ScopeFunctions::call, which assigns an ID as each describe()/test() is collected) used a function-local static AtomicI32 that only advances while the agent is enabled
  • the retroactive path (retroactively_report_discovered_tests, which walks the already-discovered scope tree when TestReporter.enable arrives after collection has started) seeded a fresh local max_id at 0 on every call

When enable lands mid-collection (some scopes already discovered, others still being registered live) both paths run in the same process and each hands out its own 1..N range, so two unrelated tests can be reported under the same ID. Inspector clients keying state by ID (correlating found/start/end events) silently conflate them.

Fix

Move the counter to a next_test_id: i32 field on TestReporterAgent (matching HTTPServerAgent::next_server_id / ErasedAgentSlot::sequence):

  • ScopeFunctions::call increments it directly through its &mut Debugger borrow
  • retroactively_report_discovered_tests takes the counter by value and returns the advanced value through the RuntimeHooks hook; test_reporter_agent_enable writes the return back into the field

Both paths now draw from the same monotone counter. Single JS thread, so no synchronization needed.

Verification

New test in test/cli/inspect/test-reporter.test.ts creates a genuine mid-collection window by pausing inside an async describe() callback (suite A's sync callback has already registered 2 tests; suite B's test() call has not yet run). TestReporter.enable is sent in that window so suite A + suite B's describe go through the retroactive walk and test B1 goes through the live path in the same run. The test asserts every found ID across both paths is unique via a raw non-deduped list (the Map-keyed session bookkeeping would coalesce a collision).

USE_SYSTEM_BUN=1 bun test test/cli/inspect/test-reporter.test.ts -t "mid-collection"
# (fail) ... timed out after 5000ms  (Map never reaches 5 distinct keys)

bun bd test test/cli/inspect/test-reporter.test.ts
#  2 pass, 0 fail

Supersedes #34221 (same fix, adopted with co-author credit).


[review] gate passed · iteration 0 · 5 files touched

fails on main (without fix)
ASAN without fix: 1 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/inspect/test-reporter.test.ts
bun test v1.4.0 (532af4234)

test/cli/inspect/test-reporter.test.ts:
(pass) TestReporter inspector protocol > retroactively reports tests when TestReporter.enable is called after tests are discovered [1618.23ms]
killed 1 dangling process
(fail) TestReporter inspector protocol > assigns unique test IDs when TestReporter.enable lands mid-collection [5001.59ms]
  ^ this test timed out after 5000ms.

 1 pass
 1 fail
 7 expect() calls
Ran 2 tests across 1 file. [8.88s]
error: script "bd" exited with code 1
__F:1:S:0

release without fix: 1 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/cli/inspect/test-reporter.test.ts:
(pass) TestReporter inspector protocol > retroactively reports tests when TestReporter.enable is called after tests are discovered [58.29ms]
killed 1 dangling process
(fail) TestReporter inspector protocol > assigns unique test IDs when TestReporter.enable lands mid-collection [5000.27ms]
  ^ this test timed out after 5000ms.

 1 pass
 1 fail
 7 expect() calls
Ran 2 tests across 1 file. [5.26s]
__F:1:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/inspect/test-reporter.test.ts
bun test v1.4.0 (532af4234)

test/cli/inspect/test-reporter.test.ts:
(pass) TestReporter inspector protocol > retroactively reports tests when TestReporter.enable is called after tests are discovered [1671.03ms]
(pass) TestReporter inspector protocol > assigns unique test IDs when TestReporter.enable lands mid-collection [1602.82ms]

 2 pass
 0 fail
 19 expect() calls
Ran 2 tests across 1 file. [5.40s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 692ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] cc obj/packages/bun-usockets/src/context.c.o
[2/6] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 239 extern-C blocks audited
[2/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install)
�[1m�[92m   Compiling�[0m bun_jsc v0.0.0 (/workspace/bun/src/jsc)
�[1m�[92m   Compiling�[0m bun_ast_jsc v0.0.0 (/workspace/bun/src/ast_jsc)
�[1m�[92m   Compiling�[0m bun_js_parser_jsc v0.0.0 (/workspace/bun/src/js_parser_jsc)
�[1m�[92m   Compiling�[0m bun_semver_jsc v0.0.0 (/workspace/bun/src/semver_jsc)
�[1m�[92m   Compiling�[0m bun_sys_jsc v0.0.0 (/workspace/bun/src/sys_jsc)
�[1m�[92m   Compiling�[0m bun_sql_jsc v0.0.0 (/workspace/bun/src/sql_jsc)
�[1m�[92m   Compiling�[0m bun_sourcemap_jsc v0.0.0 (/workspace/bun/src/sourcemap_jsc)
�[1m�[92m   Compiling�[0m bun_css_jsc
... (truncated)
diff hotspot
src/jsc/Debugger.rs                       |  10 ++-
 src/jsc/VirtualMachine.rs                 |   8 +-
 src/runtime/jsc_hooks.rs                  |  24 ++---
 src/runtime/test_runner/ScopeFunctions.rs |   5 +-
 test/cli/inspect/test-reporter.test.ts    | 145 ++++++++++++++++++++++++++++++
 5 files changed, 174 insertions(+), 18 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                       reads  edits  tests
src/jsc/Debugger.rs                            4      4      0
src/jsc/VirtualMachine.rs                      2      4      0
src/runtime/jsc_hooks.rs                       4      5      0
src/runtime/test_runner/ScopeFunctions.rs      4      4      0
test/cli/inspect/test-reporter.test.ts         3      3      0

…ctive paths

The live-registration path in ScopeFunctions::call used a function-local
static AtomicI32; the retroactive path in retroactively_report_discovered_tests
seeded a fresh local at 0 on every call. When TestReporter.enable arrives
mid-collection both paths run in the same process and hand out overlapping
1..N ranges, so two unrelated tests can be reported under the same ID.

Move the counter to Debugger::next_test_id_for_debugger. ScopeFunctions::call
increments it directly; the retroactive hook takes the current value and
returns the advanced counter, which test_reporter_agent_enable writes back.

Co-authored-by: Craig R. Hughes <craig.git@rungie.com>
@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Test reporter ID allocation

Layer / File(s) Summary
Reporter counter contract
src/jsc/Debugger.rs, src/jsc/VirtualMachine.rs
TestReporterAgent stores next_test_id. The runtime hook receives and returns the counter.
Retroactive reporting
src/runtime/jsc_hooks.rs
Retroactive reporting preserves the incoming counter when inactive and advances it when tests are reported.
Live registration and regression coverage
src/runtime/test_runner/ScopeFunctions.rs, test/cli/inspect/test-reporter.test.ts
Live registration uses the reporter-owned counter. The regression test validates unique IDs across retroactive and live events.

Possibly related PRs

  • oven-sh/bun#34221: Implements the same shared test-ID counter flow and regression coverage.

Suggested reviewers: jarred-sumner, cirospaciari

🚥 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 clearly summarizes the main change: sharing the TestReporter test ID counter between live and retroactive reporting paths.
Description check ✅ Passed The description explains the problem, fix, affected paths, regression test, and verification results in sufficient detail.

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

@robobun

robobun commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced and verified locally:

  • USE_SYSTEM_BUN=1 bun test test/cli/inspect/test-reporter.test.ts -t mid-collection times out (ID collision leaves the found-tests Map at 4 keys)
  • bun bd test test/cli/inspect/test-reporter.test.ts passes both tests

After review the counter now lives on TestReporterAgent::next_test_id (matching HTTPServerAgent::next_server_id / ErasedAgentSlot::sequence) rather than as a loose Debugger field.

CI: test/cli/inspect/test-reporter.test.ts passed on every lane in builds #86035 and #86096. Remaining failures are unrelated to this change: two worker-termination SIGABRTs on debian x64-asan (worker-transfer-terminate-stress.test.ts, test-worker-message-port-transfer-terminate.js; pre-existing main breaks, reported separately) plus assorted parallel-batch flakes that passed alone. Ready for review.

@robobun

robobun commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:42 AM PT - Jul 31st, 2026

❌ @robobun, your commit 532af42 has 1 failures in Build #86096 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36522

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

bun-36522 --bun

Comment thread src/jsc/Debugger.rs Resolved
Comment thread src/jsc/Debugger.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved
@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. TestReporter inspector events are silently, permanently lost if the process exits before the debugger thread finishes delivering them #34218 - Fixes the TestReporter ID collision layer described in this issue's prior art (PR inspector: drain queued frontend messages before exit; fix TestReporter ID collision #29307); the two independent counters that produced duplicate test IDs when TestReporter.enable arrives mid-collection are now unified into a single monotone counter on the Debugger struct

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #34218

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. inspector: fix TestReporter ID collision between retroactive and live registration #34221 - Same fix for TestReporter test ID collisions between live and retroactive registration paths, modifying the same 5 files with the same shared-counter approach

🤖 Generated with Claude Code

Comment thread test/cli/inspect/test-reporter.test.ts Resolved
Address review: colocate the counter with the agent handle to match
HTTPServerAgent.next_server_id / ErasedAgentSlot.sequence. Trim redundant
comments. Drain proc.stdout alongside stderr in the mid-collection test.

Co-authored-by: Craig R. Hughes <craig.git@rungie.com>
Comment thread src/jsc/Debugger.rs Resolved
Comment thread src/jsc/VirtualMachine.rs Resolved
Comment thread src/runtime/jsc_hooks.rs Resolved

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/Debugger.rs`:
- Around line 810-815: Rename TestReporterAgent.next_test_id to last_test_id and
centralize pre-increment allocation in a public alloc_test_id method that
returns the newly assigned ID, starting at 1. Update both ScopeFunctions and the
retroactive jsc_hooks path to use this method while preserving the existing
parameter/return threading and ID behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c4b9c916-dcac-4635-a875-335167e6349a

📥 Commits

Reviewing files that changed from the base of the PR and between 529adec and 532af42.

📒 Files selected for processing (5)
  • src/jsc/Debugger.rs
  • src/jsc/VirtualMachine.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/test_runner/ScopeFunctions.rs
  • test/cli/inspect/test-reporter.test.ts

Comment thread src/jsc/Debugger.rs Resolved

@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 stdout-drain nit from the earlier pass is addressed, and the shared-counter fix looks correct.

What was reviewed:

  • Both ID-assignment paths now draw from TestReporterAgent::next_test_id; all early returns in retroactively_report_discovered_tests thread next_test_id back unchanged, and retroactively_report_scope only increments for scopes with test_id_for_debugger == 0, so a re-enable can't double-assign.
  • Moving from a process-global static AtomicI32 to a per-Debugger field is the right placement (per-VM state on the VM); single-JS-thread so plain i32 is fine.
  • New test genuinely interleaves the two paths (async describe checkpoint → retroactive walk → live registration) and asserts uniqueness on a raw non-deduped ID list, so a collision can't hide behind the Map.

The three unresolved comment-cop notes are firing on ordinary 2-line doc comments, not workaround justifications.

Extended reasoning...

Overview

This PR fixes a TestReporter inspector-protocol bug where test/describe IDs could collide when TestReporter.enable arrives mid-collection. Two independent counters (a function-local static AtomicI32 in ScopeFunctions::call for live registration, and a fresh let mut max_id = 0 in retroactively_report_discovered_tests) each handed out 1..N, so tests reported via different paths in the same run could share an ID. The fix unifies them into a single next_test_id: i32 field on TestReporterAgent, threaded by value/return through the RuntimeHooks boundary so the low-tier crate can seed and re-store it without a forward-dep.

Files touched: src/jsc/Debugger.rs (+field, write-back on enable), src/jsc/VirtualMachine.rs (hook signature), src/runtime/jsc_hooks.rs (thread the counter through, all early returns preserve it), src/runtime/test_runner/ScopeFunctions.rs (drop the static, mutate the field via &mut Debugger), plus a new regression test in test/cli/inspect/test-reporter.test.ts.

Security risks

None. This is inspector/debugger metadata (integer IDs correlating found/start/end events for IDE tooling). No user-controlled input reaches the changed code paths; no auth, crypto, filesystem, or network surface.

Level of scrutiny

Moderate. ~30 lines of native change across four files with a clear, mechanical shape: replace two counters with one, thread it across a crate boundary. The state relocation (process-global static → per-Debugger field) is an architectural improvement per REVIEW.md ("per-VM state goes on VirtualMachine, never process globals"). I traced retroactively_report_scope to confirm it pre-increments *max_id before use and skips scopes that already carry an ID, so seeding from a nonzero next_test_id cannot double-assign or regress the existing all-retroactive test.

Other factors

  • My prior review flagged the new test piping stdout without draining it; commit 532af423 addressed that (Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited])), and the inline comment is now resolved.
  • robobun confirmed the test fails under USE_SYSTEM_BUN=1 and passes on the debug build — satisfies the "prove the test fails for the right reason" bar.
  • The three still-open comment-cop bot comments target the 2–3-line doc comments on next_test_id and the hook signature. Those are ordinary field/function docs, not workaround justifications, so I'm treating them as bot false positives.
  • The new test follows the sibling test's harness pattern (unix-socket inspector, gate files, Console-domain checkpoint) and asserts on a raw rawFoundIds array specifically so a duplicate ID surfaces as Set.size < length rather than being coalesced by the session's Map.

@Jarred-Sumner
Jarred-Sumner merged commit 93c4666 into main Aug 1, 2026
54 of 55 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/593de83d/testreporter-id-collision branch August 1, 2026 07:08
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.

2 participants