Skip to content

bun test: accept what Bun 1.4.4 stored for a Response, a BuildArtifact, expect.any() and a node:vm global - #47809

Open
robobun wants to merge 8 commits into
mainfrom
robobun/c8c329e8/older-bun-vm-global-snapshot
Open

robobun wants to merge 8 commits into
mainfrom
robobun/c8c329e8/older-bun-vm-global-snapshot

Conversation

@robobun

@robobun robobun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A snapshot that Bun 1.4.4 wrote fails on main for four values. The diffs: - }Response {} / + }, - }BuildArtifact {} / + }, - Any<Function> / + Any<URL>, - NodeVMGlobalProxy {} / + JSGlobalProxy { "a": 1 }.
  • bun test: Vitest compatibility #44833 changed these prints and gave them no older form in did_older_bun_print (src/runtime/test_runner/pretty_format.rs:1570, :1604, :2882, :2392).

Fix

  • The older print writes a Response and a BuildArtifact again as an object, as 1.4.4 did.
  • It writes Function for a native class in Any<Name> and [class Name], where the saved text has it. A node:vm global gets NodeVMGlobalProxy the same way.
  • Verified: test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts (9 tests fail on main), with a fixture that 1.4.4 wrote.
  • Self-reviewed: 28 concerns raised, 27 addressed. Rejected: docs and an issue for test.each keys, which bun test: Vitest compatibility #44833 changed on purpose.

Background

  • ExpectedText compares each write of the older print with the saved text, and lets the formatter read the rest.
  • The older print runs only if the first print cleared same_as_older_bun, as it now does.
  • A native class: an InternalFunction whose class name is not Function, like URL.
  • Considered no same_as_older_bun gate (fixes 0 of 27) and a copy of the 1.4.4 printer (+73 KB).

Downsides

  • A failing snapshot with one of these values, or a class value like Map, gets one more print. +768 B text, +7 to +22 instructions per matching snapshot.
  • Still fail: keys from test.each titles that bun test: Vitest compatibility #44833 changed, a Response of a typed Blob, property matchers on a Response.
  • Some Response texts of 1.1.42 and older pass on main and fail here: the bar is what 1.4.4 accepts.
Notes

Before merge

Where this comes from

  • There is no issue and no user report. The failing texts come from notes of my own earlier PRs: inspect: print a node:vm context global from its own property slots, sorted or not #47217 (the node:vm global), bun:test: print Headers entries and the href of a URL in failure messages #47611 and bun:test: store the href of a URL in a snapshot instead of URL {} #48020 (the Response).
  • One public project is known to store such a text. The first entry of tests/routes/__snapshots__/public.test.ts.snap in MyHouse-OS/API (d6970bd72c) ends with }Response {}. Of 559 public .snap files in Bun's format that I read, it is the only one. None holds one of the other three texts.
  • With that file and a stand-in test (a Response of 970 bytes in place of the app of the project), 1.4.4 passes 2 of 2, main fails 1 with - }Response {} / + }, and this PR passes 2 of 2. The file stays as it is. The test of the project itself was not run.
  • The numbers below are coverage of values that I chose. They are not a measure of how many projects are affected.
  • bun test: Vitest compatibility #44833 is in no release yet. Its description says: "When a snapshot doesn't match, Bun also compares it against what earlier versions printed. If that matches, the snapshot passes and is left alone until --update-snapshots rewrites it." It lists two exceptions. Under "Older bugs" it says: "expect.any(URL) was printed as Any<Function>. A Response in a diff was followed by Response {}." Neither is among the exceptions.
  • The same PR gave [class Function] its older form after a report of this kind (comment). Any<Function> comes from the same change of JSC__JSValue__getClassName.
  • Who is affected on the first release with bun test: Vitest compatibility #44833: a CI job that installs the latest Bun fails on text that 1.4.4 wrote, while a developer on 1.4.4 passes. --update-snapshots writes text that 1.4.4 then rejects, so no text passes on both until every runner has the new version.

Repro

import { test, expect } from "bun:test";
test("a Response alone", () => { expect(new Response("a", { status: 201 })).toMatchSnapshot(); });
test("a Response in an object", () => { expect({ id: 1, response: new Response("a", { status: 201 }) }).toMatchSnapshot(); });
test("any URL", () => { expect({ a: expect.any(URL) }).toMatchSnapshot(); });
test("any Buffer", () => { expect({ a: expect.any(Buffer) }).toMatchSnapshot(); });

Write the .snap with a release. Then run CI=true bun test with a build of main and of this PR.

.snap written by main This PR
1.1.43, 1.2.0, 1.3.0, 1.4.0, 1.4.3, 1.4.4 0 pass, 4 fail 4 pass
1.1.0, 1.1.42 (no Response {}) 2 pass, 2 fail 4 pass
main 4 pass 4 pass

For the node:vm global:

import { expect, test } from "bun:test";
import vm from "node:vm";
test("context global with data", () => {
  expect(vm.runInContext("this", vm.createContext({ a: 1 }))).toMatchSnapshot();
});
test("empty context global", () => {
  expect(vm.runInContext("this", vm.createContext({}))).toMatchSnapshot();
});
test("nested context global", () => {
  expect({ holder: vm.runInContext("this", vm.createContext({ a: 1, get g() { return 2; } })) }).toMatchSnapshot();
});
test("DONT_CONTEXTIFY global", () => {
  expect(Object.assign(vm.createContext(vm.constants.DONT_CONTEXTIFY), { a: 1 })).toMatchSnapshot();
});
.snap written by main This PR
Bun 1.4.4 (NodeVMGlobalProxy {} four times) 0 pass, 4 fail 4 pass
Bun 1.4.3 (JSGlobalProxy {} three times, then NodeVMSpecialSandbox { "a": 1 }) 3 pass, 1 fail 3 pass, 1 fail

The bar

  • An older form is what the last release before the change of a print wrote. For all four values that release is 1.4.4. A text that 1.4.4 accepts still passes, with the exceptions under "Not in this PR".
  • A text that only a version before 1.4.4 accepts can fail. Two such texts were accepted by an earlier head of this PR and are not now: a Response with no second print (1.1.42 and older), and JSGlobalProxy {} for a node:vm global whose prototype has a tag (1.4.0 to 1.4.3, the jsdom window). 1.4.4 rejects both.
  • main itself uses this bar for [class Function]: it refuses [class Stats], which 1.2.0 wrote.

A Response and a BuildArtifact

  • 1.1.43 to 1.4.4 printed the value with write_format, and then printed the same value as an object. In 1.4.4 these two branches of the Tag::Private arm had no return, so the last statement of the arm ran. bun test: Vitest compatibility #44833 added the return.
  • The second print is the object form of the value: Response {}, Sub {} for a subclass, and the own properties if there are any (}Response \nResponse {\n "extra": 1,\n} at the top level).
  • The older print runs the same statement again (Formatter::print_again_as_object), so each of these forms comes out as the release wrote it.
  • 1.1.42 and older did not print the value a second time. main passes their text where something else starts the older print, for example an accessor next to the Response. This PR does not: 11 such entries in my corpus pass on main and fail here. 1.1.43 to 1.4.4 reject all 11.

The name of a class

Any<Name> and [class Name] print JSC__JSValue__getClassName. #44833 changed that function for an InternalFunction, and gave [class Name] an older form with the constant Function. Any<Name> had none.

Class 1.1.42, 1.2.0 1.3.0 to 1.4.4 main Accepted here
URL, Buffer, Headers, File, Event and 21 more of 63 probed Function Function its name both
fs.Stats, fs.Dirent its name Function its name both
Bun.Cookie, Bun.CookieMap not there its name its name both
Bun.ArrayBufferSink, FileSink, a perf_hooks histogram Function Function its name its name
Number, Response, Map, a class in a script its name its name its name its name
  • The rule (Formatter::class_name): in the older print, for an InternalFunction whose class info name is not Function, write Function if the saved text continues with Function> or [class Function]. In every other case write the live name.
  • The constant Function on main fails [class Cookie], which 1.4.4 wrote, where an accessor is next to it. It passes here.
  • The class info name of Bun.ArrayBufferSink is Function on main, so no check of the live value tells it from Map.
  • Any<Function> for Bun.Cookie passes here and no release wrote it. [class Function] for it passes on main already.

The global of a node:vm context

Value 1.4.0 to 1.4.3 1.4.4 Live name on main Accepted here
vm.runInContext("this", vm.createContext({ a: 1 })) JSGlobalProxy {} NodeVMGlobalProxy {} JSGlobalProxy both
A jsdom 25 window with runScripts. jsdom gives the global the prototype Window.prototype, whose tag is Window JSGlobalProxy {} Window {} Window Window {}
A sandbox with its own Symbol.toStringTag: "Tagged" Tagged {} Tagged {} Tagged Tagged {}
this of a DONT_CONTEXTIFY context JSGlobalProxy {} NodeVMGlobalProxy {} JSGlobalProxy both
The global of the main realm JSGlobalProxy {} JSGlobalProxy {} JSGlobalProxy JSGlobalProxy {}
  • The rule (ExpectedText::names_it_node_vm_global_proxy): for a JSGlobalProxy whose target is a NodeVMGlobalObject and whose live name is JSGlobalProxy, the older print writes NodeVMGlobalProxy if the saved text continues with NodeVMGlobalProxy {}.
  • JSGlobalProxy {} passes for a plain context on main already: the live name is the same and the older print lists no properties.
  • A sandbox whose own tag is the string JSGlobalProxy accepts NodeVMGlobalProxy {}, which 1.4.4 did not write for it. An earlier head refused that with a C++ lookup of 18 lines. It is gone with the branch for 1.4.0 to 1.4.3.

Snapshots that release binaries wrote

test/js/bun/test/snapshot-tests/formats/bun-1.4.4/older-forms.fixture.js and its .snap are in this PR. Bun 1.4.4 wrote both the file entries and the inline snapshots: 13 tests, 32 snapshots. 1.4.4 passes 13, main passes 0, this PR passes 13 and leaves both files as they are. --update-snapshots rewrites them once.

The other sets are not in the repo. Each number is the count of entries that pass with CI=true. Every .snap file stays byte-identical. main is the canary build of b1305dd.

Set main This PR
189 values written by 1.4.4 (web classes, matchers, mocks, errors, collections, node:vm) 162 187
The repro above, written by 8 releases from 1.1.0 to 1.4.4 4 of 32 32
A BuildArtifact alone and in an object, written by 1.4.3 and by 1.4.4 0 of 4 4
8 texts (a Response alone, in an object and a Map, a subclass, an own property, property matchers, expect.any() of six classes), written by 1.2.0, 1.4.3 and 1.4.4 0 of 24 24
expect.any() of 63 constructors, written by 1.4.3 and by 1.4.4 74 of 126 126
{ a: expect.any(C), c: C, get g() } for 9 classes (7 before Bun.Cookie), written by 6 releases from 1.1.42 to 1.4.4 12 of 50 44
11 texts with property matchers, resolves and a thrown object, written by 1.4.4 1 8
15 texts written by 1.1.0, 13 of them with a Response and no second print 13 5
  • Over 43 sets (the rows above and more probes), 393 of 622 entries pass on main and 583 here.
  • 11 entries pass on main and fail here. All are in the last row and in one more text of 1.1.42: a Response with no second print, next to an accessor, a class value or a non-enumerable property.
  • The 2 of 189 that stay: a Blob and a File. Their type changed on main (text/plain;charset=utf-8 to text/plain), which is a change of the value.
  • The 6 of 50: Bun.ArrayBufferSink.
  • The 3 of 11: a property matcher that lands on the Response itself.

For the node:vm global, measured at 8d91cb7 with files that 1.4.4 wrote. The rule for the 1.4.4 name is the same since then.

Set main This PR
19 shapes from real use: node:vm as documented, jsdom 25.0.1 windows, happy-dom 17.0.3 7 18
114 shapes made to probe the name (prototypes, tags, constructors, DONT_CONTEXTIFY) 32 86
40 ways to hold the global (array, Map, Set, class field, resolves, a hint, property matchers on another key) 5 37
  • The entry from real use that still fails: a hand-made window whose prototype is a class with no Symbol.toStringTag. 1.4.4 wrote FakeWindow {}.
  • Of the 114, the 28 that still fail are names that 1.4.4 took from a prototype a script set, where the name of the constructor is not the tag (Foo {}, Array {}, Error {}).
  • In the 40, the entries left are expect.soft (the releases saved no entry) and property matchers on the global.

What the older print accepts, as on main

  • An entry that passes through the older print counts as a pass, and a Bun-format file does not record the version that wrote it. So an older form has no end date.
  • The older print also accepts the older form of the value as it is now. A stored Any<Function> from expect.any(Function) still passes after the matcher becomes expect.any(URL).
  • An accepted entry keeps its older text. When the value really changes later, the diff of that failure also lists the lines that only differ in form, as for [native code] on main.

Not in this PR

  • The key of an entry whose title comes from test.each with %o or %p of an object. bun test: Vitest compatibility #44833 changed such titles (each {"a":1} to each { a: 1 }), so the entry is not found. With CI=true the test fails with "Snapshot creation is disabled", and otherwise a new entry is written beside the old one. The older print is a print of the value, and it is never reached. bun test: Vitest compatibility #44833 lists the new titles under "Behaviour changes for existing users".
  • A Blob or a File whose type a script set, and a Response that holds one. blob.type is another value on main. The failure also shows - }Response {}, because the message compares the saved text with the new print. A row in the test pins that this text fails.
  • A property matcher that lands on a Response, a Headers or another native object itself (expect(response).toMatchSnapshot({ status: expect.any(Number) })). Older versions wrote the matcher onto the object, and the second print listed it. main does not write onto the object. The Headers case fails on main with no Response in it.
  • Any<Function> and [class Function] for Bun.ArrayBufferSink, FileSink and a perf_hooks histogram (see the table).
  • Texts that 1.4.4 rejects too: a Response with no second print (1.1.42 and older), JSGlobalProxy {} for a context global whose prototype has a tag (1.4.0 to 1.4.3), [class freemem] for a function of node:os (1.4.3), a String object with a character above U+00FF (1.4.0 to 1.4.3).
  • The name that 1.4.4 gave a context global from a prototype that a script set, if that prototype has no tag (Foo {}). The first version of this PR computed it in C++ (+6,656 B). No tool is known to make that shape.
  • Property matchers on the context global itself, and NodeVMSpecialSandbox { ... } of 1.4.3 for vm.constants.DONT_CONTEXTIFY. The test pins that these texts fail.
  • A bound on the first print of a context global. For a jsdom window that print grows with the document: 64 ms for <p>hi</p>, 3,548 ms with 16 more <div>, on a release build.
  • In a Vitest format file a context global prints as [Object].
  • A docs line for the keys. The docs sentence that this PR changes is about values.

Other pull requests

Self-review

28 concerns. What changed after it:

  • The bar is stated and the two branches below it are gone: the second print that was tried and taken back for texts of 1.1.42 and older, and the JSGlobalProxy {} name for 1.4.0 to 1.4.3 with its C++ lookup. That answers the concerns about the mechanism and the tests of both (7).
  • A fixture that 1.4.4 wrote is in the repo, in place of most hand-typed texts. The hand-typed rows that stay are for a test of vitest and for texts that must fail (2).
  • One method for the two sites that print a value again (1).
  • This description: the reach, the contract of the older print, what still fails, the corrected account of the other pull requests (15).
  • The note on Print Bun's snapshot format with the console.log formatter #46729 with the port for each order, built and tested (3).
  • Rejected, from one concern: a docs line and an issue for the keys of test.each titles. bun test: Vitest compatibility #44833 lists the new titles as a behaviour change, so they are its decision. The other part of that concern is done: the "Still fail" line under Downsides.
  • Not done: a version in the header of a snapshot file, which the review keeps out of this PR too. The test of the one public project was not run, only the stand-in under "Where this comes from".
  • The questions for a maintainer are in the comment that "Before merge" links.

Measurements

main b1305dd against this PR (3f9db1d), linux x64. The diff of src/ is the same at the head 406c068.

  • release binary text: +768 B (llvm-size, 140,289,934 to 140,290,702. pretty_format symbols +957 B by llvm-nm). The stripped binary is 140,478,752 B in both.
  • instructions per matching snapshot: empty objects 27,512 to 27,529 (+17.5), Response 27,979 to 27,986 (+6.5), expect.any 16,589 to 16,610 (+21.6), class values 15,810 to 15,789 (-20.7). User mode, perf_event_open, BUN_JSC_useJIT=0, 200,000 matching toMatchInlineSnapshot calls, median of 7 interleaved rounds. The noise floor (two series of the main binary) is 16.4, 1.3, 0.1 and 0.1.
  • matching path, 40 snapshots that each hold a Response, a BuildArtifact, expect.any(URL), expect.any(Number) and the class values URL and Map (debug builds, lldb breakpoint hit counts): did_older_bun_print 0 (0 expected), names_it_function 0, names_it_node_vm_global_proxy 0, getClassInfoName 0 vs 80 on main, getClassName 240 vs 240 on main.
  • allocations per matching snapshot with a Response and expect.any(URL): 39.0 and 39.2 on main (two rounds), 38.6 here (one round). Calls of the mi_malloc family and of WTF::fastMalloc on release builds, 400 snapshots minus 200.
  • print passes (main / this PR), counted by a Proxy ownKeys trap in the first key of the value, for an inline snapshot: match 1/1, older text 2 and fails / 2 and passes, wrong text with a Response 2/3, wrong text with expect.any(URL) or the class Map 2/3, wrong text with expect.any(Number) 2/2, wrong text with property matchers 4/4, plain value 2/2. For a file: match 1/1, older text 1/2, wrong text with a Response 1/2, plain value 1/1. The second print on main for an inline snapshot is the print in Jest's format.
  • Bun.inspect(expect.any(URL)) x1000: getClassInfoName 0 vs 0 on main, getClassName 1003 vs 1003 on main. The text is Any<URL> on both.
  • 1.4.4-written corpus: 2 of 189 fail (27 on main). BuildArtifact 2 of 2 pass. The 1.4.4 fixture in the repo: 13 of 13 pass (0 on main). Files unchanged.
  • size_of::<Formatter>(): 304 B (main 304 B). ExpectedText: 24 B (24 B).
  • test/bundler/compile-aot-size.test.ts skips all 39 tests on local builds of main and of this PR.

Tests

  • Local, linux x64: test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts on a debug build with ASAN and FORCE_COLOR=1, as CI sets it: 189 pass, 1 skip. With the src/ of main: 180 pass and 9 fail, all from this PR.
  • CI, build 138343 at 406c068: the file passes on each of the 11 lanes that ran it. Linux x64 and aarch64 (glibc, musl, and x64 with ASAN) and macOS x64 and aarch64: 189 pass, 1 skip. Windows x64 and aarch64: 186 pass, 4 skip. The 3 more skips are tests that main skips on Windows.
  • Time of the file in CI, this PR / main (build 138109): 2.98 s / 3.26 s on debian aarch64, 3.61 s / 3.46 s on ubuntu x64, 11.20 s / 10.63 s on Windows x64, 11.62 s / 12.26 s on Windows aarch64. main has 181 tests in the file and this PR has 190.
  • In this container the tests of "the fixtures and snapshot files that another runner wrote" take 4.5 s to 7.2 s each on both builds, so some pass the 5 s limit of a local run.
  • The fixture of 1.4.4 has each accepted form: a Response alone, in an object, an array and a Map, a subclass, an own property, next to an accessor, with property matchers, from resolves, then expect.any() and [class Name] of ten classes, and the node:vm global.
  • Hand-typed rows are for a test of vitest, for a BuildArtifact (its older text is made from the text of the build under test, because it holds a hash), and for texts that must stay refused: the second print after one of two Responses only, the name of another class after it, the second print twice, another status, }Request {}, the text of 1.1.42 next to an accessor, the type that a Blob had, Any<Function> for Number and for Response, Any<Buffer> for URL, and eight texts for the node:vm global.
  • Also run: test/js/bun/test/printing/diffexample.test.ts, dom-nodes.test.ts, test/js/bun/test/expect.test.js (464 pass), and the other files in test/js/bun/test/snapshot-tests/.

no test proof · iteration 4 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

…4.3 or 1.4.4 wrote

When a snapshot does not match, bun test prints the value again the way an
older Bun did and accepts the entry if that text matches. For the global of
a node:vm context that print used the class name of the live object. Bun
1.4.4 had the class NodeVMGlobalProxy for it, so its snapshots failed. Bun
1.4.0 to 1.4.3 printed JSGlobalProxy whatever prototype a script gave the
global, so a 1.4.3 snapshot of a jsdom window failed too.

For a context global that print now takes either of the two names when the
saved text continues with it. ExpectedText shares the rest of the saved
text with the formatter for this.
@robobun

robobun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:52 AM PT - Nov 7th, 2026

❌ @robobun, your commit 406c068 has 39 failures in Build #138343 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 47809

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

bun-47809 --bun

@robobun

robobun commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

How I reproduced it: the released Bun 1.4.4 wrote snapshots for a Response, a BuildArtifact, expect.any(URL) and the global of a node:vm context (the test files are under "Repro" in the description). With CI=true, a build of main fails each of them: - }Response {} / + }, - }BuildArtifact {} / + }, - Any<Function> / + Any<URL>, - NodeVMGlobalProxy {} / + JSGlobalProxy { "a": 1 }. With this change they pass, and the .snap files stay as they are.

The fixture test/js/bun/test/snapshot-tests/formats/bun-1.4.4/older-forms.fixture.js holds snapshots that 1.4.4 wrote. On main 13 of its 13 tests fail. With this change 13 pass.

CI (build 138343, head 406c068): snapshot.test.ts passes on each of the 11 lanes that ran it. The build is red from 38 test files that this PR does not touch: 37 under test/aot/ and test/bundler/compile-aot-size.test.ts. The same 38 files fail on main (build 138109). One macOS job stopped before its tests started, because a clone of a vendored project timed out.

Next: the decisions in this comment.

PR: #47809

@github-actions github-actions Bot added the claude label Nov 3, 2026
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review

No actionable comments were generated in the recent review.

Walkthrough

The formatter now recognizes selected Bun 1.4.4 snapshot representations for class matchers, Response, BuildArtifact, and node:vm globals. New fixtures and tests cover matching, mismatches, snapshot updates, and unchanged snapshots. Documentation describes the legacy representations.

Changes

Snapshot compatibility

Layer / File(s) Summary
Legacy snapshot text comparison
src/runtime/test_runner/pretty_format.rs
ExpectedText tracks remaining saved output and divergence. The formatter recognizes historical internal-function names, extra object output for Response and BuildArtifact, and NodeVMGlobalProxy output. Asymmetric matchers obtain class names through the formatter interface.
Legacy snapshot regression tests
test/js/bun/test/snapshot-tests/formats/bun-1.4.4/*, test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts, docs/test/snapshots.mdx, .prettierignore
Fixtures and tests cover Bun 1.4.4 representations, recorded-output matches, unrecorded-output mismatches, and snapshot update behavior. Documentation and the ignore comment identify the legacy snapshot forms and writer.

Suggested reviewers: jarred-sumner

Merge Risk: Low · up to 406c0

A specially tagged VM global can incorrectly match an older snapshot. This is a narrow, acknowledged compatibility tradeoff.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title identifies the snapshot compatibility changes.
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.
Description check Passed The description explains the changes and how the author verified them. It uses different headings from the template, but provides the required information.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 9ec6032 and 406c068.

Files selected for processing: 1

  • test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

…al has no tag of its own

Bun 1.4.0 to 1.4.3 printed the Symbol.toStringTag that the sandbox or the
global itself has, and JSGlobalProxy where the tag comes from a prototype
that a script set. So a saved `JSGlobalProxy {}` no longer passes for a
global that has its own tag, as on main.

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

I reviewed the updated PR and didn't find any bugs; the follow-up commit that only substitutes JSGlobalProxy when the global has no own Symbol.toStringTag addresses the concern from my earlier pass. Because this adds a version-specific acceptance heuristic to snapshot matching (a saved NodeVMGlobalProxy {} now passes for any context global regardless of its properties), a maintainer's judgment on that trade-off would still be worthwhile.

What was reviewed:

  • ExpectedText/like_older_bun rework: rest and differs move into Cells and the Write impl moves to &ExpectedText; the differs check at the format entry and the final !differs && rest.is_empty() test are unchanged, and the write chain (FmtAdapter -> AsFmt -> &ExpectedText) is unbuffered, so rest is current when name_of_context_global reads it.
  • name_of_context_global: gated on js_type() == GlobalProxy plus get_proxy_target() class-info name, so the main-realm global and non-vm objects return None; get_proxy_target returns an empty value for non-proxies and get_class_info_name tolerates it.
  • SnapshotFormat__hasOwnToStringTag: VMInquiry slot through NodeVMGlobalObject::getOwnPropertySlot forwards only to the sandbox's own-property lookup (ProxyObject bails on VM inquiry), the possible stack-overflow throw from isSafeToForwardToContextifiedObject is covered by RELEASE_AND_RETURN plus from_js_host_call_generic on the Rust side.
  • Test: spawns a child bun test with a pre-written .snap, drains stdout/stderr/exited concurrently, pins the negative cases (tagged + JSGlobalProxy {}, property matchers, NodeVMSpecialSandbox) and asserts the .snap stays byte-identical.
Extended reasoning...

The change touches the bun test snapshot backward-compat path in src/runtime/test_runner/pretty_format.rs (about 70 lines), adds a 12-line extern "C" helper in src/jsc/bindings/SnapshotFormat.cpp that does an own-property lookup under VMInquiry, and adds a subprocess test in test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts. It touches no security-sensitive surface: no auth, crypto, network, or path handling. The bug hunt ran dry with no findings, the earlier inline finding was fixed in commit e718a85 with a test pin, and no CODEOWNER covers these files. Deferral rather than approval is because the fix is a deliberate heuristic that accepts older-version text with stated downsides in the description, which is a product decision for a maintainer, and a github-actions bot comment on the new code could not be read.

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

Additional comments: 1
src/runtime/test_runner/pretty_format.rs-3553-3555: 1

3553-3555: Minor · Potential issue · Quick win

Unverified finding
We couldn't verify this from the code. It is shown for review, not as a verified issue.

When the live name is JSGlobalProxy, this branch substitutes NodeVMGlobalProxy solely from saved-text prefix, without checking for an own Symbol.toStringTag. It may accept a name that the historical formatter would have replaced with the object's own tag.

A mismatched snapshot may pass as though it had been written by the older release.

This can occur when: A context global owns Symbol.toStringTag with value JSGlobalProxy, and its saved snapshot says NodeVMGlobalProxy {}.

Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between cfa751c and 8a5ff76.

Files selected for processing: 3
  • src/jsc/bindings/SnapshotFormat.cpp
  • src/runtime/test_runner/pretty_format.rs
  • test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Every version printed the Symbol.toStringTag that the sandbox or the global
itself has. So the check for it now guards both older names: a saved
`NodeVMGlobalProxy {}` no longer passes for a global whose own tag is the
string "JSGlobalProxy". A tag with no text does not count, as before.
@robobun

robobun commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

On the finding for pretty_format.rs 3553-3555 (a context global that owns Symbol.toStringTag with the value JSGlobalProxy): reproduced. For vm.runInContext("this", vm.createContext({ [Symbol.toStringTag]: "JSGlobalProxy" })) a saved NodeVMGlobalProxy {} fails on main and passed with 8a5ff76. Bun 1.4.4 wrote JSGlobalProxy {} for that value.

Fixed in 8d91cb7. The check for a tag of the global itself (SnapshotFormat__hasOwnStringTag) now guards both older names. A tag with no text does not count, because 1.4.4 printed NodeVMGlobalProxy {} for it. The test pins both cases (taggedAsTheClass, taggedWithNoText).

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

I re-reviewed the latest three commits and found no bugs; the sandbox-with-its-own-tag case I raised earlier is now rejected by the SnapshotFormat__hasOwnStringTag guard and pinned in notAccepted, so that thread is addressed. Since this loosens which stale snapshots bun test accepts for a context global, a human look at the acceptance rule in name_of_context_global would still be worthwhile.

What was reviewed:

  • ExpectedText refactor to Cell fields and the &ExpectedText Write impl: same strip-prefix/differs semantics, no buffering between the formatter and rest, so starts_with sees the text at the class-name position.
  • SnapshotFormat__hasOwnStringTag: VMInquiry lookup forwards through NodeVMGlobalObject::getOwnPropertySlot to the sandbox without running getters or Proxy traps; exception checked before the slot is read; accessor tags return false.
  • name_of_context_global only runs under like_older_bun, so the normal print path is unchanged; get_proxy_target is gated on js_type() == GlobalProxy.
  • Test: drains stdout/stderr/exited concurrently, cleans up via using, asserts output before exit code, and pins both accepted and rejected spellings per version.
Extended reasoning...

The change touches the bun test snapshot backward-compatibility path in src/runtime/test_runner/pretty_format.rs, adds one C++ FFI helper in src/jsc/bindings/SnapshotFormat.cpp, and extends test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts; it touches no security-sensitive surface. The earlier inline finding about a tagged sandbox accepting a saved JSGlobalProxy {} was addressed by commits e718a85 and 8d91cb7, and no third-party CHANGES_REQUESTED review is open. Deferring rather than approving because the fix is a heuristic that intentionally accepts older snapshot text for a node:vm context global regardless of its properties, which is a product decision a maintainer should weigh, and the PR is bot-authored.

…t and expect.any() of a native class

When a snapshot does not match, the value is printed again as the last
release before a print changed printed it. Three prints that #44833
changed had no older form:

- A Response and a BuildArtifact. Bun 1.1.43 to 1.4.4 printed the value,
  and then the same value again as an object (`}Response {}`).
- The class in `Any<Name>`. Bun 1.4.4 named a class that native code makes
  `Function`, as in `[class Function]`, but named a few by their own name
  (`Bun.Cookie`). Both prints now share one rule, which writes `Function`
  only where the saved text has it.

The older name of the global of a node:vm context is now the name of
1.4.4 alone. The text that 1.4.0 to 1.4.3 wrote for a global whose
prototype has a tag fails on 1.4.4 too, so its branch and the C++ helper
for it are gone.

formats/bun-1.4.4 holds a fixture whose snapshots Bun 1.4.4 wrote.
@robobun robobun changed the title bun test: accept the snapshot of a node:vm context global that Bun 1.4.3 or 1.4.4 wrote bun test: accept what Bun 1.4.4 stored for a Response, a BuildArtifact, expect.any() and a node:vm global Nov 7, 2026

@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


  • Fix comments: Fix CodeRabbit comments on this PR
Prompt to fix all issues
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. 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:
Review comments at @src/runtime/test_runner/pretty_format.rs:
- Around line 3566-3573: Update names_it_node_vm_global_proxy to reject the
legacy NodeVMGlobalProxy match when the global has a nonempty own
Symbol.toStringTag, while continuing to accept it when the tag is empty. Add a
regression test covering both tagged and empty-tag cases.

---

Failed pre-merge checks:
- Description check:
Describe the full snapshot compatibility scope, documentation changes, and
fixture. Clarify which tests ran, where they ran, and which tests remain
deferred to CI.

After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 8d91cb7 and 9ec6032.

Files ignored due to path filters: 1

  • test/js/bun/test/snapshot-tests/formats/bun-1.4.4/__snapshots__/older-forms.fixture.js.snap is excluded by !**/*.snap
Files selected for processing: 5

  • .prettierignore
  • docs/test/snapshots.mdx
  • src/runtime/test_runner/pretty_format.rs
  • test/js/bun/test/snapshot-tests/formats/bun-1.4.4/older-forms.fixture.js
  • test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

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

Findings marked 🟡 are optional suggestions and need no follow-up push.

The three texts that no version wrote were checked with a bare toThrow().
They now report the first line of the failure, as the tests beside them do.

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

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator Author

Decisions for a maintainer

No user reported this. The PR rests on the precedent of [class Function] in #44833. It needs a yes or a no before a review of the code.

@Jarred-Sumner

  1. Do these texts keep passing? bun test: Vitest compatibility #44833 lists Any<Function> and the second Response {} under "Older bugs the new tests ran into". It does not list them with the snapshots that need an update by hand. bun test: Vitest compatibility #44833 gave [class Function] an older form in 40e0eaa, after a report of the same kind. If the answer is no, I close this PR.
  2. Which releases count? This PR accepts each text that Bun 1.4.4 accepts. Some texts of 1.1.42 and older pass on main and fail here. Each is a Response with no Response {} after it, next to an accessor, a class value or a non-enumerable property. 1.1.43 to 1.4.4 reject those texts too. If every release counts, I add a second older print for that form.
  3. Timing. The change is of use only in the release that first has bun test: Vitest compatibility #44833. After that release, projects have run --update-snapshots.

The known reach is small. Of 559 public .snap files in Bun's format that I read, one holds }Response {}. None holds one of the other three texts.

This PR does not change what happens to an entry of a test.each test whose title #44833 changed. The entry is not found under the new title. #44833 lists the new titles as a behaviour change.

@dylan-conway

  1. Which order with Print Bun's snapshot format with the console.log formatter #46729? That PR deletes the formatter that this PR changes. The note there has the port for each order.

@robobun

robobun commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

#48451 is under questions 1 and 3 of the comment above too.

  • It makes an inline snapshot that Bun 1.4.4 wrote pass again where the print of the value now throws: an own getter that throws, a boxed symbol, a toJSON that throws.
  • One difference from the texts in this PR: --update-snapshots cannot rewrite such a snapshot, because it has to print the value. bun test passes and bun test -u fails with the error of the value.
  • If the answer to question 1 is no, bun test: an inline snapshot an older Bun wrote still passes when printing the value throws #48451 closes too.

@robobun

robobun commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

One more case for questions 1 and 3: a getter that changes its object

This case is under questions 1 and 3 of the comment above too. It is the plain-object form of the second exception in #44833: "the new printer fills the element's lazy caches before the old form is printed".

  • What fails. A snapshot that Bun 1.4.4 wrote fails on main if an own getter of the value changes the value when it is read. An example is { id: 1, get items() { return (this._items ??= [1, 2]); } }. The diff is - "items": [native code], / + "items": [.
  • Cause. The matcher prints the value first, and that print calls the getters. The comparison with the older print runs after it, on the changed value.
  • Reach. There is no issue and no user report. I found no public snapshot that fails.
  • Without a fix. bun test -u repairs each case. The rewritten text fails on 1.4.4, so no text passes on both versions.

A draft fix is on the branch robobun/91c0240e/older-bun-snapshot-before-getters. It has no pull request. If the saved text holds [native code], it compares with the older print before the first print.

Written by Bun 1.4.4 1.4.4 main (89452d2) Draft
A fixture with 14 getter shapes (21 tests) 21 pass 0 pass 21 pass
The fixtures of #48451 (19 tests) 19 pass 0 pass 13 pass
A happy-dom element, exception 2 of #44833 (944 KB) pass fail pass

The draft is not ready to merge. It and #48451 are one rule: take the answer of the older print before the current print runs code that 1.4.4 did not run.

Open points of the draft, and its cost
  • A snapshot in the current form can hold [native code]. A getter that throws prints as that text, and so does a string such as String(Math.max). For such a snapshot the older print reads the value first. A value whose reads are not repeatable then gets another result:
    • A Proxy over a getter that counts its reads fails where main passes.
    • A value that throws on its first read only passes where main throws.
  • While a snapshot is in the older form, the print calls no getter, as on 1.4.4. The first --update-snapshots starts to call them.
  • Two cases still fail, because the saved text holds no [native code]:
    • A getter at an index key that changes its object. 1.4.4 printed null for it.
    • In a test that imports from vitest, an inline snapshot of a value that its toJSON() changes.
  • Size: +224 -46 lines in expect.rs and snapshot.rs, and +3,584 bytes of text in a release build.
  • Instructions for each snapshot call (user mode, no JIT, release builds of main and of the draft):
Snapshot main Draft
Inline, current form, object with 50 keys 143,666 144,019
Inline, current form, { a: 1 } 10,865 11,013
Inline, current form, string of 700 characters 6,744 6,777
In a .snap file, current form, object with 50 keys 135,582 135,644
The same, and another entry of the file holds [native code] 135,605 136,177
Inline, older form of { id: 1, get a() {} } 22,296 12,483
Inline, current form of an object with a getter that throws 22,546 30,591

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