bun test: detect obsolete snapshots and fix -u "added" mislabel

Closed
RO
2mo ago
+408-26

What

bun test now reports snapshot keys that exist in a .snap file but were never matched during the run, and bun test -u stops mislabeling rewritten entries as "added".

Fixes #12114

Problem

$ bun test           # .snap file has a stale `obsolete-gone 1` key
 1 pass
 0 fail
 1 snapshots, 1 expect() calls     # no signal that a stale key exists

$ bun test -u
snapshots: +1 added                # nothing was added; one entry was rewritten, one removed

Jest prints "1 snapshot obsolete" on the first run and "1 snapshot removed" on the second. Bun had no way to represent "in the file but never matched": Snapshots carried only added/passed/failed, and under -u the file was opened with O_TRUNC so every entry looked brand new. The early truncate also left the .snap file empty for anything that read it during afterAll() (#12114).

Fix

src/runtime/test_runner/snapshot.rs

  • Snapshots gains obsolete, removed counters and an unchecked_keys: StringHashMap<()> populated by parse_file and drained on each match in get_or_put.
  • Under -u, get_snapshot_file reads and parses the old file before clearing the buffer so a key that already existed counts as passed rather than added; write_snapshot_file truncates after write instead of relying on O_TRUNC at open. A malformed old file is ignored under -u so the existing "replace unparseable file" behaviour is preserved.
  • mark_snapshots_as_checked_for_test removes every {testName} N key for a given test (Jest's keyToTestName rule) so skipped/todo/filtered tests are not counted as obsolete.

src/runtime/test_runner/Execution.rs

  • on_sequence_completed calls the new hook for Skip / Todo / SkippedBecauseLabel results, building the name identically to Expect::get_snapshot_name.

src/runtime/cli/test_command.rs

  • Summary prints N removed / N obsolete (with a bun test -u hint). The obsolete line is suppressed under --only since non-only tests never reach the reporter there.

Verification

$ bun test
snapshots: 1 passed, 1 obsolete
To remove obsolete snapshots, run bun test -u

$ bun test -u
snapshots: 1 passed, 1 removed

New coverage in test/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts: plain-run obsolete count, -u removed vs added, test.skip and -t not producing false positives, a truly new key still reporting added, and the #12114 repro (.snap file readable in afterAll under -u). All six fail on current main and pass with this change. Existing snapshot-tests/, ci-restrictions.test.ts, and bun_test.test.ts pass unchanged.


[review] gate passed · iteration 3 · 6 files touched

fails on main (without fix)
ASAN without fix: 9 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (a024016cd)

test/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts:
31 |         "// Bun Snapshot v1, https://bun.sh/docs/test/snapshots\n\n" +
32 |         "exports[`foo 1`] = `1`;\n\n" +
33 |         "exports[`foo 2`] = `2`;\n",
34 |     });
35 |     const { stderr, exitCode } = await run(dir);
36 |     expect(stderr).toContain("1 obsolete");
                        ^
error: expect(received).toContain(expected)

Expected to contain: "1 obsolete"
Received: "\nsnap.test.ts:\n(pass) foo [3.09ms]\n\n 1 pass\n 0 fail\n 1 snapshots, 1 expect() calls\nRan 1 test across 1 file. [395.00ms]\n"

      at <anonymous> (/workspace/bun/test/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts:36:20)
(fail) obsolete 
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (d9ac81530)

test/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts:
(pass) obsolete snapshot detection > passing test with fewer toMatchSnapshot calls than keys on disk reports obsolete [17.65ms]
(pass) obsolete snapshot detection > -u labels dropped entries as removed, not added [20.44ms]
(pass) obsolete snapshot detection > reports obsolete snapshots without -u [21.28ms]
(pass) obsolete snapshot detection > skipped test's snapshot is not counted obsolete (skip before first match) [18.43ms]
(pass) obsolete snapshot detection > -u counts a skipped test's dropped entry in removed [18.30ms]
(pass) obsolete snapshot detection > in-source test.only() suppresses obsolete [17.42ms]
(pass) obsolete snapshot detection > skipped test's snapshot is not counted obsolete (skip after first match) [20.23ms]
(pass) obsolete snapshot detection > beforeAll failure suppresses obsolete for jumped-over tests [16.77ms]
(pass) obsolete snapshot detection > skipped test with a hinted snapshot is not counted obsolete [15.67ms]
(pass) obsolete snapshot detection > -t filtered test's snapshot is not counted obsolete [14.25ms]
(pass) obsolete snapshot detection >
... (truncated)
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/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (a024016cd)

test/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts:
(pass) obsolete snapshot detection > passing test with fewer toMatchSnapshot calls than keys on disk reports obsolete [486.97ms]
(pass) obsolete snapshot detection > reports obsolete snapshots without -u [474.96ms]
(pass) obsolete snapshot detection > skipped test's snapshot is not counted obsolete (skip after first match) [489.76ms]
(pass) obsolete snapshot detection > skipped test's snapshot is not counted obsolete (skip before first match) [507.33ms]
(pass) obsolete snapshot detection > -u labels dropped entries as removed, not added [543.50ms]
(pass) obsolete snapshot detection > -u counts a skipped test's dropped entry in removed [4
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 675ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 243 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
   Compiling bun_core v0.0.0 (/workspace/bun/src/bun_core)
   Compiling bun_errno v0.0.0 (/workspace/bun/src/errno)

... (truncated)
diff hotspot
src/runtime/cli/test_command.rs                    |  45 ++++-
 src/runtime/test_runner/Execution.rs               |  22 +++
 src/runtime/test_runner/bun_test.rs                |   4 +-
 src/runtime/test_runner/expect.rs                  |  14 +-
 src/runtime/test_runner/snapshot.rs                | 137 +++++++++++--
 .../test/snapshot-tests/obsolete-snapshots.test.ts | 212 +++++++++++++++++++++
 6 files changed, 408 insertions(+), 26 deletions(-)

gate history · 7 passed · 0 rejected · iteration 3

evidence per changed file
file                                                      reads  edits  tests
src/runtime/cli/test_command.rs                               8     17      0
src/runtime/test_runner/Execution.rs                         11     14      0
src/runtime/test_runner/bun_test.rs                           2      1      0
src/runtime/test_runner/expect.rs                             3      1      0
src/runtime/test_runner/snapshot.rs                          13     26      0
…t/js/bun/test/snapshot-tests/obsolete-snapshots.test.ts      1     10      0