Conversation
…ing test get_snapshot_name built the key from the active execution entry, which during a per-test hook is the hook itself: hooks have no name, so every snapshot taken in beforeEach, afterEach, an afterEach registered inside a test, or onTestFinished was keyed "<describe> (unnamed): <hint> N" with the describe chain of the hook's declaration (none for hooks added inside a test). All of a describe's hooks shared that key, so adding or reordering a test renumbered every later hook snapshot. Resolve the sequence's test_entry instead, so the key uses the name and describe chain of the test the hook runs for, as jest does. beforeAll and afterAll run in sequences without a test and keep their existing key.
|
Warning Review limit reached
Next review available in: 32 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Status: ready for review at bc3d385. CI (build 97523): 177 of 179 jobs passed, including the Linux, ASAN, Alpine and Windows runs of the new tests. The only two non-green jobs are the macOS 14 aarch64 test lanes, which never got an agent (one expired, one still queued); the previous build, 97156, was starved of the same lanes. The annotations on both builds are retried flaky tests unrelated to the test runner. Nothing in the diff is platform-specific. The expired job needs a retry from someone with write access to Buildkite; I am not going to push retrigger commits against a starved queue. Reproduced with bun 1.4.0-canary.1: |
There was a problem hiding this comment.
Beyond the inline nit, I also checked the raw-pointer round-trip in get_snapshot_name (capture NonNull from entry(), then re-borrow buntest for sequence()) — it mirrors the existing pattern at jest.rs:609-614, and the unwrap_or(active_entry) fallback preserves the old key when sequence() yields no test_entry (beforeAll/afterAll), so the concurrent-group error path and the describe-chain walk are unchanged.
Extended reasoning...
Verified the new unsafe deref follows the same split-borrow idiom already used for entry()/sequence() in jest.rs's uncaught-exception handler, and that test_entry: Option<NonNull<ExecutionEntry>> is Copy so no borrow outlives the sequence() call. The fallback to active_entry means every case where sequence() or test_entry is None degrades to exactly the pre-PR behavior. Not approving because this is a user-facing snapshot-key change with an unsafe block in the test runner — worth a maintainer glance.
Move the test-or-hook lookup next to entry(), sharing the validated sequence lookup, so get_snapshot_name only swaps the accessor.
There was a problem hiding this comment.
I re-reviewed after 79ef2eb and 5f1f700 and found no bugs — the earlier stdout-pipe nit is fixed and the running_sequence() extraction preserves entry()'s validation exactly. Because this re-keys existing hook .snap entries (a user-visible behavior change, even if it restores pre-#22534/Jest semantics) and adds new unsafe derefs in the test-runner core, a human sign-off would still be worthwhile.
Checked: running_sequence() is a pure extraction of the old entry() body (same phase check + get_current_and_valid_execution_sequence); snapshot_entry()'s test_entry.or(active_entry) fallback is total because the validator only returns Some when active_entry matches a non-null recorded entry; get_snapshot_name only reads base.name/base.parent, so the &ExecutionEntry return type suffices; the sole other .entry() caller in jest.rs is untouched.
Extended reasoning...
Overview
Three files: bun_test.rs extracts a private RefDataValue::running_sequence() from the existing entry() body and adds a sibling snapshot_entry() that returns sequence.test_entry.or(sequence.active_entry); expect.rs swaps .entry() for .snapshot_entry() at the one call site in get_snapshot_name; the test file adds a two-test describe covering top-level/outer/inner beforeEach, beforeAll/afterAll, in-test afterEach, onTestFinished, and an inline snapshot in beforeEach, plus a stability test (adding a test doesn't renumber existing hook snapshots).
Security risks
None. No untrusted input, no auth/crypto/permissions surface. The new unsafe blocks deref NonNull<ExecutionEntry> fields under the same invariant the pre-existing entry() and every test_entry deref in Execution.rs already relies on (intrusive nodes valid while the sequence is live, lifetime tied to &'a mut BunTest).
Level of scrutiny
Moderate-to-high. The Rust delta is small and pattern-matching, but (a) it lives in the test runner's execution core with unsafe pointer derefs, and (b) it changes the observable snapshot key for any user who currently calls toMatchSnapshot from a per-test hook — their next run outside CI writes new keys and leaves the old ones obsolete, and their next CI run reports new snapshots. The PR description argues this well (regression from #22534, matches Jest's currentTestName and vitest, keys are unstable today), but that's a product call a maintainer should ratify.
Other factors
Prior feedback addressed: my stdout: "pipe" nit (79ef2eb → "ignore") and comment-cop's paragraph-comment flag (5f1f700 moved the lookup into snapshot_entry() so get_snapshot_name is a one-token change). I confirmed get_current_and_valid_execution_sequence requires entry_data: Some and matches active_entry against a non-null recorded pointer, so running_sequence() returning Some implies active_entry.is_some() — the .or(active_entry) fallback for beforeAll/afterAll never yields None after ?. The refactored entry() derives &mut ExecutionEntry from a NonNull field of an immutably-borrowed &ExecutionSequence; the entry is a separate heap box (in DescribeScope::entries/hook vecs/cloned_hook_entries), so no aliasing. Tests follow harness conventions (tempDir, bunEnv, test.concurrent, exact-array assertions, stderr before exitCode). No CODEOWNERS on the touched paths.
|
Updated 4:56 AM PT - Aug 15th, 2026
❌ @robobun, your commit bc3d385 has some failures in 🧪 To try this PR locally: bunx bun-pr 38820That installs a local version of the PR into your bun-38820 --bun |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs (the earlier stdout: "pipe" nit was addressed in 79ef2eb). Because it changes user-facing snapshot key generation — existing .snap files with hook snapshots need a one-time bun test -u after upgrading — a human sign-off on that compat trade-off would be worthwhile.
What was reviewed:
running_sequence()refactor preservesentry()behavior exactly;snapshot_entry()returnsNoneon the same paths, so the concurrent-group error inget_snapshot_nameis unchanged.test_entryderef safety matches the existingactive_entrypattern; both point into sequences validated byget_current_and_valid_execution_sequence.- The describe-chain walk in
get_snapshot_namenow uses the test'sbase.parent, so outer-declared hooks get the inner describe prefix — checked against the fixture's expected keys.
Extended reasoning...
Overview
The PR changes which ExecutionEntry is used to build snapshot keys when toMatchSnapshot() is called from a per-test hook. In bun_test.rs, RefDataValue::entry() is refactored onto a new private running_sequence() helper (behavior-preserving), and a new snapshot_entry() sibling returns the sequence's test_entry (falling back to active_entry for beforeAll/afterAll). expect.rs swaps one accessor call in get_snapshot_name. Three subprocess tests are added to bun-snapshots.test.ts covering the full key layout, the shared-counter behavior, and the stability property (adding a test does not renumber existing hook snapshots).
Security risks
None. This is test-runner-internal snapshot naming; no untrusted input, no I/O beyond the existing .snap file writes.
Level of scrutiny
Medium-high. The Rust diff is small (~20 lines) and mechanically parallels existing code — the new test_entry deref uses the same intrusive-node safety justification as the adjacent active_entry deref, and running_sequence() is a pure extraction with the same guards (RefDataValue::Execution match, Phase::Execution check, get_current_and_valid_execution_sequence). The tests are hermetic (tempDir, bunEnv, CI: "false"), use test.concurrent, drain stderr concurrently with proc.exited, and assert exact .snap contents plus pass/fail counts before exit code.
However, this is a user-visible behavior change: any project taking snapshots inside beforeEach/afterEach/onTestFinished will see those snapshots re-keyed and need bun test -u. The PR body documents this thoroughly and shows the change aligns with Jest and vitest, but whether that trade-off is acceptable (and whether it needs a changelog note) is a maintainer call.
Other factors
- The one nit from my earlier review (
stdout: "pipe"unread) was fixed in 79ef2eb; the comment-cop flag onexpect.rswas addressed by moving the lookup intosnapshot_entry()in 5f1f700. All inline threads are resolved. - CI on 5f1f700 was 177/179 green with unrelated flakes per the status comment; bc3d385 only adds a third test.
- I checked that
snapshot_entry()returning&ExecutionEntry(vsentry()'s&mut) is fine —get_snapshot_nameonly readsbase.nameand walksbase.parent. - The
.or(active_entry)fallback keeps beforeAll/afterAll on their existing(unnamed)key rather than throwing, which the PR body justifies (Jest has nocurrentTestNamethere either, and throwing would break files that work today).
Problem
toMatchSnapshot()/toThrowErrorMatchingSnapshot()called frombeforeEach,afterEach, anafterEachregistered inside a test, oronTestFinishedis written asexports[`d (unnamed): hint 1`]instead ofexports[`d t1: hint 1`](repro and the full before/after key lists are in the details block below).outer (unnamed): ...) even when the test lives in an inner describe, and hooks registered from inside a test body get no prefix at all ((unnamed): hint 1).Expect::get_snapshot_name(src/runtime/test_runner/expect.rs:626) builds the key from the execution entry that was active whenexpect()was called. While a hook runs that is the hook's own entry, which has no name, and whose parent is the scope the hook was declared in (none for hooks added during a test). Before the test runner rewrite (Rewrite test/describe, add test.concurrent #22534) the key came from the pending test; Jest keys these snapshots bycurrentTestName, which is set for the whole test including its hooks.Fix
RefDataValue::snapshot_entry()(bun_test.rs, next toentry(), both built on a sharedrunning_sequence()that keeps the existing validation) returns the sequence'stest_entry, falling back to the active entry when the sequence has no test.get_snapshot_namecalls it instead ofentry()and is otherwise unchanged, so the concurrent-group error and the describe-chain walk are as before.ExecutionSequenceis exactly one test plus the beforeEach/afterEach/onTestFinished entries that run for it (Order.rsgenerate_order_test, andgeneric_hook_implinbun_test.rsfor hooks added while the test runs), sotest_entryis the test a hook is running for, and its name and describe chain are what the test body's own snapshots are already keyed by. The result matches Jest (and vitest, apart from its>separator) for every case in the new test, including an inline snapshot inbeforeEachtaking number 1 of the test's unhinted counter.beforeAll/afterAllrun in sequences without atest_entry(generate_all_order) and stay under their currentd (unnamed): hintkey, now counted among themselves only. Jest has no test name there either (currentTestNameis undefined, or still the previous test's), that key is stable once the per-test hooks stop sharing it, and throwing (what bun did before the rewrite) would break files that work today.bun test -uafter upgrading. Every such snapshot (file or inline, with or without hint) now counts against the test it runs for, which changes three kinds of existing.snapkeys:<describe> (unnamed)...to the test's keys;(unnamed)entry to grep for;(unnamed)key but are renumbered when a per-test hook in the same describe used the same hint.-urewrites the file with only the current keys. Each shape was checked with a.snapwritten by bun 1.4.0 and then run with this branch (details block).test/js/bun/test/snapshot-tests/bun-snapshots.test.ts, newsnapshots taken in hooksblock, three tests that all fail on the released bun with the(unnamed)keys and pass with this change:beforeEach,beforeAll,afterEach, anafterEachregistered inside the test,onTestFinished,afterAlland an inline snapshot inbeforeEach, asserting the full.snapcontents;d (unnamed): setup 1/2for beforeAll/afterAll,d t1: setup 1/2/3for beforeEach/body/afterEach);afterEachsnapshots and checks the run still passes with the existing entries untouched.test/js/bun/test/bun_test.test.ts,test/js/bun/test/ci-restrictions.test.ts,test/cli/test/rerun-each.test.ts(retry/repeat counter reset),test/js/bun/test/test-test.test.ts,test/js/bun/test/expect.test.js, and the rest oftest/js/bun/test/snapshot-tests/(itserror snapshotscase needsFORCE_COLOR=1, as on main; CI sets it).Background
: hintif a hint was given, then a counter. The counter is per key and is incremented by every file or inline snapshot taken under that key during the run (and reset when a test is retried or repeated), so a snapshot charged to the wrong key also shifts the numbering of the others.ExecutionEntry: one callback the runner executes, a test or a hook. Hooks have no name.base.parentis the describe scope the entry was declared in; hooks registered while a test is running have none.ExecutionSequence: what the runner executes as one unit. For a test it is the test's beforeEach entries, the test, and its afterEach/onTestFinished entries, withtest_entrypointing at the test; each beforeAll/afterAll hook is a sequence of its own with notest_entry.active_entryis the entry currently running.expect()records the runner state it was called in (RefDataValue).RefDataValue::entry()returns the recorded entry if it is still the one running, and nothing in a concurrent group, where the runner cannot tell which sequence anexpect()belongs to (hence the existing "not supported in concurrent tests" error, which this keeps).jest.rsalready compares the active entry with the sequence'stest_entrythe same way to tell a hook failure from a test failure.Repro and keys before/after
CI=false bun testwith bun 1.4.0-canary.1 writes:With this change:
vitest 4.1.9 on the same file (with
vitestimports) writes the same set of keys with>as the separator, includingouter > inner > t1 2for the unhinted body snapshot.Upgrade path: .snap written by bun 1.4.0, then run with this branch
bun 1.4.0 writes:
This branch against that file, without
-u: the beforeEach fails (d t1: setup 1now receives"be", stored"body"), so the body is skipped; the afterEach'sd t1: setup 2is appended (outside CI); the afterAll fails (d (unnamed): setup 2now receives"aa", stored"be"). Summary:snapshots: 1 passed, 1 added, 2 failed, and the five old entries stay in the file.After
bun test -uon this branch:A
beforeEachwith onlytoMatchInlineSnapshot()behaves like thebeforeEachabove for numbering purposes: the body's unhintedtoMatchSnapshot()entries move fromd t1 1tod t1 2, with no(unnamed)entry in the old file.