Repository navigation
bun:test: fill every placeholder in test.each titles - #44830
Jarred-Sumner wants to merge 1 commit into
Conversation
|
Updated 4:12 PM PT - Oct 8th, 2026
@Jarred-Sumner, your commit 61a3b61 is building: |
|
Folded into #44833. |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked the scanner's edge cases in format_label — a trailing % or $ at the end of the title, %<spec> with no args left, and $a. with a trailing dot — and each falls through to the literal-copy arm without slicing past the buffer; the %s-of-Buffer/URL difference from Node (toString text vs inspected) was looked at and left alone since it follows the ported hasBuiltInToString rule as written.
Extended reasoning...
The change rewrites format_label in src/runtime/test_runner/jest.rs plus tests and docs; it touches no auth, crypto or network surface. Several confirmed findings are posted inline (prototype-walk hang on a self-referential Proxy, %c consuming a value, string-matching JSC exception messages in write_json), so a human look is already signalled; this note only records the extra paths examined and ruled out.
| b"Object", b"Array", b"Date", b"RegExp", b"Boolean", b"Number", b"String", b"Symbol", b"BigInt", | ||
| ]; | ||
| let mut owner = object; | ||
| loop { | ||
| if !owner.is_object() { | ||
| return Ok(true); | ||
| } | ||
| if let Some(to_string) = owner.get_own(global, &bun_core::String::static_("toString"))? { | ||
| if !to_string.is_callable() { | ||
| return Ok(true); | ||
| } |
There was a problem hiding this comment.
🟡 (optional) A test.each row holding a Proxy whose getPrototypeOf trap returns the proxy itself makes bun test hang forever on a %s title; the base left %s literal and never touched the object. The prototype walk in has_builtin_to_string (src/runtime/test_runner/jest.rs:676-686) loops owner = owner.get_prototype(global)? with no visited check or depth cap, and get_prototype runs the trap. Fix: unwrap proxies to their target before and during the walk (get_proxy_target, as Node's hasBuiltInToString does via getProxyDetails) and cap the number of steps so any cyclic chain built through a Proxy ends the loop with true.
Why this was flagged
A test file does const p = new Proxy({}, { getPrototypeOf: () => p }); test.each([[p]])("%s", () => {}). During collection, ScopeFunctions.rs:202 calls format_label, which reaches write_placeholder at src/runtime/test_runner/jest.rs:784 with specifier s and an object, so has_builtin_to_string runs. At jest.rs:679 get_own("toString") on the proxy forwards to the empty target and returns None, then jest.rs:686 owner = owner.get_prototype(global)? invokes the trap and gets p back. JSC allows this because the trap result is only checked against the target when the target is non-extensible. The loop at jest.rs:677 has no revisit or depth bound, so it spins forever; no test timeout applies because this is synchronous collection-time native code. On the base branch %s of a non-string wrote the literal %s and never inspected the object, so the same file ran. Node's hasBuiltInToString first replaces a Proxy with its target (getProxyDetails); the port omitted that step and the repo already exposes JSValue::get_proxy_target (JSValue.rs:2626) to do it.
Verification: has_builtin_to_string at src/runtime/test_runner/jest.rs:672-696 loops owner = owner.get_prototype(global)? with no visited set, depth cap, or proxy unwrap. get_prototype (src/jsc/JSValue.rs:2354) runs the Proxy's getPrototypeOf trap, which returns p because the target is extensible, so the loop never terminates and bun test hangs. On the base, the %s arm emitted the literal %s, never touching the object.
| fn has_builtin_to_string(global: &JSGlobalObject, object: JSValue) -> JsResult<bool> { | ||
| const BUILTINS: [&[u8]; 9] = [ | ||
| b"Object", b"Array", b"Date", b"RegExp", b"Boolean", b"Number", b"String", b"Symbol", b"BigInt", | ||
| ]; | ||
| let mut owner = object; | ||
| loop { | ||
| if !owner.is_object() { | ||
| return Ok(true); | ||
| } | ||
| if let Some(to_string) = owner.get_own(global, &bun_core::String::static_("toString"))? { | ||
| if !to_string.is_callable() { | ||
| return Ok(true); | ||
| } | ||
| break; | ||
| } | ||
| owner = owner.get_prototype(global)?; | ||
| } | ||
| if owner == object { | ||
| return Ok(false); | ||
| } | ||
| let Some(constructor) = owner.get_own(global, &bun_core::String::static_("constructor"))? else { | ||
| return Ok(false); | ||
| }; | ||
| let name = constructor.get_name(global)?; | ||
| Ok(BUILTINS.iter().any(|builtin| name.eq_ascii(builtin))) |
There was a problem hiding this comment.
🟡 (optional) Users whose row values define only Symbol.toPrimitive get an inspected title (Money { v: 5 }) where Jest and Bun's own util.format print String(value) ($5). has_builtin_to_string at src/runtime/test_runner/jest.rs:679-690 only stops at a prototype with an own toString, so it walks past the user class to Object.prototype and returns true. The Node function it says it ports also stops at an own Symbol.toPrimitive and unwraps proxies first (src/js/internal/util/inspect.js:2519-2559). Fix: port the full check so an own callable Symbol.toPrimitive on the value or on the first prototype that has one ends the walk the same way toString does, and read through a Proxy to its target rather than running its getOwnPropertyDescriptor/getPrototypeOf traps. [also at: src/runtime/test_runner/jest.rs:689 - nit: a %s row value whose class defines [Symbol.toPrimitive] (and no toString) gets an inspected title like Money {} where Jest/Node print the primitive it converts to.]
Why this was flagged
Trigger: class Money { constructor(v){this.v=v} [Symbol.toPrimitive](){ return "$"+this.v } } and test.each([[new Money(5)]])("costs %s", () => {}); the value reaches write_placeholder at src/runtime/test_runner/jest.rs:772 through the %s arm, which calls has_builtin_to_string at jest.rs:775. The loop at jest.rs:679-690 finds no own toString on the instance or on Money.prototype, continues to Object.prototype, breaks there, reads constructor (Object) and returns true, so jest.rs:787 falls to write_inspected and the title is costs Money { v: 5 }. Node's hasBuiltInToString, as ported in src/js/internal/util/inspect.js:2534-2554, exits the walk at the first prototype that owns either toString or Symbol.toPrimitive; Money.prototype owns Symbol.toPrimitive, its constructor is not a builtin, so it returns false and util.format("%s", new Money(5)) in Bun itself prints costs $5, which is also what Jest prints. The same port also skips the proxy unwrapping at inspect.js:2520-2528, so for a Proxy value the loop runs the user's getOwnPropertyDescriptor and getPrototypeOf traps. On the base branch %s with an object left the literal %s in the title, so neither the mismatch nor the trap execution existed.
Verification: has_builtin_to_string at src/runtime/test_runner/jest.rs:672-695 only probes for an own toString, so a class defining only [Symbol.toPrimitive] reaches Object.prototype and returns true; write_placeholder falls to write_inspected, producing Money { v: 5 }. src/js/internal/util/inspect.js:2543-2554 also stops at Symbol.toPrimitive, so util.format yields $5. On the base branch %s left the literal %s in the title.
|
|
||
| /// Generate test label by positionally injecting parameters with printf formatting | ||
| /// The title of one `.each()` row: `%` placeholders take `function_args` in order, `$a.b` reads the first one. | ||
| pub(crate) fn format_label( |
There was a problem hiding this comment.
🟡 (optional) Projects with toMatchSnapshot() inside a test.each whose title is renamed by this change get failing CI runs after upgrading, because the snapshot key is the test name. format_label now produces a different name for every title that used %s/%d/%i/%f with a value of another type, %o (JSON to inspect), %p or $obj of an object (multi-line to one line), or $a where a is null/undefined, and snapshot.rs:183-187 refuses to create the new key under CI. Fix: state the rename as a breaking change in the docs hunk and release notes, naming --update-snapshots and the affected specifiers, so the failure is expected rather than a surprise; the CI error already names the flag. [also at: test/cli/test/bun-test.test.ts:1030 - Users with committed snapshots inside test.each tests whose titles change (a %s with a number, %o, %p or $obj of an object) get failing CI runs after upgrading.]
Why this was flagged
A Jest-migrated project has test.each([[1, 2, 3]])("%s + %s = %s", (a, b, c) => { expect(c).toMatchSnapshot(); }) with a committed .snap whose key is %s + %s = %s 1 (the base left %s literal for numbers via consume_arg's should_write false). After this change format_label (jest.rs:824) writes 1 + 2 = 3, so the snapshot name becomes 1 + 2 = 3 1. In snapshot.rs:177 the hash is not found; with CI set and no --update-snapshots, snapshot.rs:183-187 returns SnapshotCreationNotAllowedInCI and expect.rs:1191-1195 throws "Snapshot creation is disabled in CI environments unless --update-snapshots is used". The PR description acknowledges the rename; the docs hunk in docs/test/writing-tests.mdx does not say existing titles, snapshot keys, -t names or JUnit names change. The consequence is a red CI run on upgrade until snapshots are regenerated, which the base does not produce.
Verification: New format_label (src/runtime/test_runner/jest.rs:824-870) routes every %s|d|i|f|j|o|O|p|c through write_placeholder, which always writes the value. So "%s + %s = %s" with [1,2,3] now yields 1 + 2 = 3 where the base yielded %s + %s = %s. src/runtime/test_runner/snapshot.rs:177 if self.values.contains_key(&name_hash) misses for the new key, then lines 183-187 return SnapshotCreationNotAllowedInCI under CI.
| } else { | ||
| array_list.extend_from_slice(fallback); | ||
| let number = match specifier { | ||
| b's' if value.is_string() |
There was a problem hiding this comment.
🟡 (optional) Users whose row value is a boxed string (new String("x")) get the title x where Node's util.format and Jest print [String: 'x']. The %s arm at src/runtime/test_runner/jest.rs:772 tests value.is_string(), and JSValue::is_string (src/jsc/JSValue.rs:265-267) is is_string_like, which includes StringObject and DerivedStringObject. Node's %s only short-circuits on primitive strings and sends String objects through hasBuiltInToString, which inspects them. The same test at src/runtime/test_runner/jest.rs:853 prints $var boxed strings raw. Fix: use is_string_literal() at both sites so String objects fall through to has_builtin_to_string / write_inspected like every other builtin object.
Why this was flagged
A row such as [new String("x")] with the title %s enters write_placeholder at src/runtime/test_runner/jest.rs:772. value.is_string() is defined at src/jsc/JSValue.rs:265-267 as js_type().is_string_like(), and JSType::is_string_like at src/jsc/JSType.rs:742-747 matches String | StringObject | DerivedStringObject. So the boxed string takes the write_to_string branch and prints x. Node's formatWithOptions handles %s with typeof tempArg !== 'object' ... || !hasBuiltInToString(tempArg); a String object is typeof 'object', its toString lives on String.prototype whose constructor name String is in the builtin list, so Node (and Jest, which delegates to util.format) inspects it as [String: 'x']. The $var branch at src/runtime/test_runner/jest.rs:853 has the same is_string() test and prints boxed strings raw. The base branch left %s literal for any non-primitive, so no title ever claimed a boxed string was a plain one. Remedy: switch both guards to is_string_literal().
Verification: The %s arm at src/runtime/test_runner/jest.rs:772 is b's' if value.is_string() || ... and JSValue::is_string (src/jsc/JSValue.rs:265-267) is self.is_cell() && self.js_type().is_string_like(), matching String | StringObject | DerivedStringObject. So a String wrapper object short-circuits into write_to_string (line 777) and prints x. Jest titles read [String: 'x'] while Bun now emits x.
| return write_to_string(global, value, title); | ||
| } | ||
| b'j' => return write_json(global, value, title), | ||
| b'c' => return Ok(()), |
There was a problem hiding this comment.
🟡 (optional) Users with a literal %c in a title and no other placeholder (for example 50%coverage over rows [[0.5], [0.9]]) get 50overage and lose the c, where Jest leaves 50%coverage untouched; on the base the % was dropped but no value consumed. The dispatch at src/runtime/test_runner/jest.rs:841 lists c as a value-consuming specifier and write_placeholder at src/runtime/test_runner/jest.rs:780 prints nothing for it. Jest's formatTitle only calls util.format when the title matches /%[sdifjoOp]/, so a title with only %c is never formatted. Fix: treat %c as a literal unless the title also contains a supported value placeholder, or drop %c from the specifier set so the % is printed as written, and document whichever rule is chosen.
Why this was flagged
A title such as 50%coverage or 100%correct with array rows reaches format_label at src/runtime/test_runner/jest.rs:841. The %-arm matches c, args.next() succeeds, write_placeholder at src/runtime/test_runner/jest.rs:780 returns without writing, and the rest = &after[1..] at :867 skips the c. The title is 50overage and the first row value is consumed. In Jest (jest-each table/array.ts) SUPPORTED_PLACEHOLDERS is /%[sdifjoOp]/g; formatTitle returns the title unchanged when no supported placeholder exists, so Jest prints 50%coverage, and only when another placeholder is present does util.format consume the %c. On the base branch the _ arm dropped the % and printed 50coverage without consuming a value, so a following %s in a longer title shifted nothing. Remedy: only consume %c when the title contains a [sdifjoOp] placeholder (Jest's rule), or print %c literally.
Verification: A title whose only % specifier is a literal %c. In src/runtime/test_runner/jest.rs the dispatch arm (line ~841) takes args.next(), write_placeholder's b'c' => return Ok(()) (line ~780) writes nothing, and rest = &after[1..]; (line ~867) skips the c. So 50%coverage becomes 50overage. On the base, the _ arm dropped only the %, giving 50coverage. Jest prints 50%coverage.
| if let Some(error) = thrown.to_error() | ||
| && let Some(message) = error.get(global, "message")? | ||
| { | ||
| let message = message.to_bun_string(global)?; | ||
| if message.eq_ascii(b"JSON.stringify cannot serialize cyclic structures.") { | ||
| title.extend_from_slice(b"[Circular]"); | ||
| return Ok(()); | ||
| } | ||
| if message.eq_ascii(b"JSON.stringify cannot serialize BigInt.") { |
There was a problem hiding this comment.
🟡 (optional) Users of %j get [Circular] or inspect output decided by comparing a thrown error's message to two hard-coded JSC sentences, so any object whose toJSON throws that text is silently reported as circular, and a future JSC rewording makes %j of a real cycle throw and fail the whole file again. write_json at src/runtime/test_runner/jest.rs:714-722 matches only message, not the error's type or identity, and never checks that the thrown value is a TypeError as Node does. Fix: detect the cycle/BigInt condition structurally (a dedicated stringify binding that reports the failure kind, or a one-time probe of the engine's actual message like Node's tryStringify) and keep the name check, so user-thrown errors are rethrown and an engine upgrade cannot silently revert the [Circular] fix.
Why this was flagged
Row value { toJSON() { throw new Error("JSON.stringify cannot serialize cyclic structures.") } } with the title %j reaches write_json at src/runtime/test_runner/jest.rs:702. json_stringify_fast returns Err(Thrown), take_exception yields the user's Error, to_error() is Some, and the message comparison at src/runtime/test_runner/jest.rs:718 succeeds, so the title becomes [Circular] and the test is registered; the user's own error is swallowed. Node's tryStringify additionally requires err.name === 'TypeError' and learns the circular message at runtime from the engine, so it rethrows a plain Error and keeps working across engine upgrades. On the base branch %j propagated any stringify error, so the failure was visible. The two strings at src/runtime/test_runner/jest.rs:718 and :722 are copied from the WebKit submodule; the next WebKit bump that rewords them makes %j of a cyclic row fall through to Err(global.throw_value(thrown)) at :726 and the entire file fails at declaration. Nothing in the test suite pins the wording through this path.
Verification: A test.each row value whose toJSON throws an Error whose message is byte-identical to JSC's sentence, under a %j title. src/runtime/test_runner/jest.rs:718 does message.eq_ascii(b"JSON.stringify cannot serialize cyclic structures.") -> writes [Circular]. There is no check of the error's name/type, so the user's own thrown error is swallowed instead of the file failing.
| let mut formatter = crate::test_runner::expect::make_formatter(global); | ||
| formatter.single_line = true; | ||
| formatter.format_value::<false>(value, title) |
There was a problem hiding this comment.
🟡 nit (optional): %s and %o of a nested object print every level (up to the formatter default of 8), so titles for fixture rows become long single lines where Jest/Node print { a: [Object] }. write_inspected at src/runtime/test_runner/jest.rs:668-670 sets only single_line and keeps the default depth for every specifier, while Node's util.format uses depth: 0 for %s and depth: 4 for %o (src/js/internal/util/inspect.js:2636 and :2664). Fix: give the %s and %o paths a per-specifier max depth matching util.format (0 and 4) while leaving %p, %O and $var at their current depth.
Why this was flagged
A row like [[{ a: { b: [1, { c: 2 }] } }]] with the title "%s" goes through write_placeholder at src/runtime/test_runner/jest.rs:772-786 to write_inspected at jest.rs:668-670, which builds the formatter from make_formatter (src/runtime/test_runner/mod.rs:282) and only sets single_line; Formatter::new leaves max_depth: 8 (src/jsc/ConsoleObject.rs:1631). The title is { a: { b: [ 1, { c: 2 } ] } }, and the new test in test/js/bun/test/jest-each.test.ts asserts exactly that. Node's %s branch inspects with depth: 0 (src/js/internal/util/inspect.js:2632-2637) so Jest 30 titles the same test { a: [Object] }; %o uses depth: 4 (inspect.js:2660-2665). The PR text says the expected titles are the real output of Jest and lists the formatter differences it knows of; depth is not among them. On the base branch %s of an object left the literal %s, so users with large fixture objects now get titles that print the whole structure on one line instead of the truncated form Jest gives.
Verification: nit. Triggering condition: any test.each/describe.each row value that is an object nested more than one level (%s) or more than four levels (%o). write_inspected at src/runtime/test_runner/jest.rs:668-670 only sets single_line and leaves max_depth: 8. Node's util.format uses depth: 0 for %s and depth: 4 for %o. The base already printed a literal %s for objects, so there is no regression.
| if let Some(hex) = text.strip_prefix(b"0x").or_else(|| text.strip_prefix(b"0X")) { | ||
| let digits = hex.iter().map_while(|&c| bun_core::fmt::hex_digit_value(c)).map(f64::from); | ||
| return sign * digits.reduce(|value, digit| value * 16.0 + digit).unwrap_or(f64::NAN); |
There was a problem hiding this comment.
🟡 (optional) Users whose %i row value is a hex string above 2^53 get a title whose integer differs from what parseInt returns, so -t filters and snapshot keys built from the JS value miss. parse_int at src/runtime/test_runner/jest.rs:749 folds hex digits left to right as value * 16.0 + digit, rounding after every digit. JSC's parseInt switches to an exact power-of-two accumulation (parseIntOverflow) once the value reaches 2^53, so a double-rounded intermediate changes the result. Fix: parse the hex run with the same overflow-exact routine JSC uses (or accumulate in u128/BigInt and convert once), and do the same for any other radix prefix handled here.
Why this was flagged
A test.each row holding the string "0x400000000000021" and a title using %i reaches write_placeholder at src/runtime/test_runner/jest.rs:784, which calls parse_int on the UTF-8 text. At src/runtime/test_runner/jest.rs:748-749 the digits are folded as f64 value * 16.0 + digit. After the first 14 digits the running value is 2^54+2, which is a tie in f64 and rounds to even, 2^54; the final digit gives 2^58+1, which rounds to 2^58. JSC's parseInt (JSGlobalObjectFunctions.cpp) re-parses any hex run whose value reaches mantissaOverflowLowerBound with parseIntOverflow, which sums digit * 16^k from the least significant digit and yields 2^58+33 rounded once to 2^58+64. The title therefore prints 288230376151711740 where parseInt(value) in the user's own test gives 288230376151711800. On the base branch %i of a string was left literal, so no wrong number was ever printed. The documented contract is %i = parseInt(String(value)).
Verification: parse_int at src/runtime/test_runner/jest.rs:745-752 folds hex digits with digits.reduce(|value, digit| value * 16.0 + digit), an f64 accumulation that rounds after every digit. For "0x400000000000021" (2^58 + 33), %i prints 288230376151711744 (2^58) while parseInt(String(value)) gives 288230376151711808 (2^58 + 64). The PR's own test at test/js/bun/test/jest-each.test.ts:192 only exercises small hex, so this range is untested.
| } | ||
| let mut formatter = crate::test_runner::expect::make_formatter(global); | ||
| formatter.single_line = true; | ||
| formatter.format_value::<false>(value, title) |
There was a problem hiding this comment.
🟡 (optional) Users get multi-line test.each titles for %s, %o, %O, %p and $row values that are an array of 100+ elements or an object holding an Error. write_inspected sets single_line = true at src/runtime/test_runner/jest.rs:669, but the formatter writes an unconditional "\n" before "... N more items" (ConsoleObject.rs:4347) and prints a nested Error through print_error (ConsoleObject.rs:3880) with its full stack and absolute paths. Fix: make write_inspected guarantee one line for every placeholder it serves, by honoring single_line at ConsoleObject.rs:4347 and printing nested errors as [Error: message] as the top-level branch at jest.rs:662 already does. On the base branch %o printed single-line JSON and %s of these values stayed literal, so this is a new source of newline-bearing, machine-specific titles.
Why this was flagged
A test file does test.each([[{ err: new Error("boom") }]])("case %o", () => {}) or test.each([[Array.from({ length: 150 }, (_, i) => i)]])("sum %s", () => {}). During collection ScopeFunctions.rs:202 calls format_label, which reaches write_placeholder's _ => return write_inspected(...) arm (jest.rs:788) for %o and the %s object branch. write_inspected at jest.rs:668-670 sets single_line = true and calls format_value. For the array, ConsoleObject.rs:4345-4347 writes writer.write_all(b"\n") once nonempty_count reaches 100 regardless of single_line. For the nested Error, ConsoleObject.rs:3318 dispatches Tag::Error to print_error, which at ConsoleObject.rs:3880 calls vm.print_errorlike_object and emits the stack trace with absolute file paths, on several lines. The title therefore contains newlines and, for errors, paths that differ per machine, so a snapshot committed on one machine is a missing key elsewhere. On the base branch %o printed {"err":{}} and %s of a non-string stayed %s, both single-line and stable.
Verification: write_inspected (jest.rs:660-670) only wraps a direct Error in [...] and otherwise sets formatter.single_line = true and calls format_value. src/jsc/ConsoleObject.rs:4345-4347 does if nonempty_count >= 100 { writer.print_comma(); writer.write_all(b"\n"); } with no single_line check. A nested Error is dispatched to print_error (ConsoleObject.rs:3318), which calls vm.print_errorlike_object(...) (3880).
What does this PR do?
Fixes #37821. Supersedes #41565 and #38831.
test.each/describe.eachtitles kept a literal%s,%d,%ior%fwhenever the value was not of the "right" type, printed%oas JSON, dropped the character after an unresolved$name, and threw on a few inputs. The title is now formatted in one left-to-right scan, and what a value prints is never scanned again.row %j shows %s[{ a: 1 }, false]row {"a":1} shows %srow {"a":1} shows falsevalue %s[undefined]value %svalue undefined%s is 100%%["x"]x is 100%%x is 100%%o[{ a: 1 }]{"a":1}{ a: 1 }%j[Circular]1 | $missing | 1{ }1 | $missing| 11 | $missing | 1%sisString(value). An object without its owntoStringis printed on one line, decided the way Node'sutil.formatdecides it (hasBuiltInToString).%disNumber(value),%iisparseInt(String(value)),%fisparseFloat(String(value)). BigInt prints10n, Symbol printsNaN.%jprintsundefinedforundefined, functions and symbols.%p,%oand%Oprint on one line.%O,%$,%cand$#are new.%%,%#and%$work whether or not values are left. An unknown%xkeeps its%. A placeholder with no value left stays as written.$a,$a.b.c,$a.0,$a.length:nullandundefinedproperties print, a path throughnullno longer throws, non-ASCII names work.This renames some existing tests.
%ogoes from JSON to{ a: 1 },%pand$variableof an object go from several lines to one, and every title that kept a literal placeholder or lost a character changes. Titles feed snapshot keys,-tand JUnit names. No snapshot in this repository moved.Where Jest and Vitest disagree:
%sof an array[ 1, 2 ]1,2[ 1, 2 ]%sof a DateThu Jan 01 1970 …%outil.inspectwithshowHidden[ 1, 2 ][ 1, 2 ]%s/%oof an ErrorError: boom/[Error: boom]Error: boom/[Error: boom]undefined$missingundefined%#in a titleKnown differences that come from Bun's formatter rather than from this code: strings inside objects print with
"where Jest and Vitest print'; an array of more than 100 elements still gets a line break before... N more items; an Error nested inside an object still prints its stack.How did you verify your code works?
test/js/bun/test/jest-each.test.ts: newdescribe("titles"), 9 tests. All 9 fail on bun 1.4.3-canary and pass with this change (34 of 34 in the file).test/cli/test/bun-test.test.ts: five assertions that encoded the old output were corrected, andtest.todo("check formatting for %p")is now a real test.parseInt/parseFloat: 70 strings (Unicode white space, hex, signs, exponents, 400 digits) compared with Node, 0 mismatches.BUN_JSC_validateExceptionChecks=1: no unchecked exception.bun:testchanges. This branch on its own has not been built locally; everything it calls exists onmain.