Skip to content

fix(bun:test): return an object when a mock function is constructed with new#31386

Open
robobun wants to merge 10 commits into
mainfrom
farm/4e0236f3/mock-construct-return-object
Open

fix(bun:test): return an object when a mock function is constructed with new#31386
robobun wants to merge 10 commits into
mainfrom
farm/4e0236f3/mock-construct-return-object

Conversation

@robobun

@robobun robobun commented May 25, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Supersedes #30212 (same fix, rebased onto current main with consolidated test coverage).

Fixes a fuzzer-found crash (ASSERTION FAILED: isCell() in JSCJSValue.h, asCell()) triggered by constructing a bun:test mock function.

JSMockFunction registered jsMockFunctionCall as both its call and construct callback. A native construct callback must return an object, but the call path can return non-objects: undefined when the mock has no implementation, or whatever primitive the implementation / mockReturnValue produced. As a result:

  • Reflect.construct(mock(), []) hit asObject()ASSERT(isCell()) in debug builds (and would reinterpret a non-cell as a JSObject* in release builds).
  • new mock() from bytecode evaluated to undefined, violating new semantics.
import { mock } from "bun:test";
const m = mock();
Reflect.construct(m, []); // ASSERTION FAILED: isCell()
new m();                  // undefined

Fix

Register a dedicated jsMockFunctionConstruct callback that behaves like new on an ordinary JS function (matching Jest, whose mock constructor is a plain function):

  • create this from newTarget.prototype (falling back to Object.prototype),
  • run the existing mock call logic with that this (so mock.calls / mock.contexts / mock.results record the construct invocation, and implementations can assign to this),
  • return the implementation's result if it is an object, otherwise the created this.

The existing call behavior is unchanged; jsMockFunctionCall now forwards to a shared implementation that takes the this value explicitly.

How did you verify your code works?

  • The fuzzer repro and minimized repros no longer assert; new mock() / Reflect.construct(mock(), []) now return objects.
  • Added mock() > are constructable with new to test/js/bun/test/mock-fn.test.js covering no-implementation constructs, Reflect.construct (including an explicit newTarget), implementations assigning to this, implementations returning objects, primitive return values, and newTarget.prototype handling, plus spyOn tests for constructing a spied method and a spy on a missing property. It fails on current bun (new fn() is undefined) and passes with this change.
  • Full mock-fn.test.js (73 tests), mock-disposable.test.ts, and the mock/ module-mocking tests pass with the debug build, including with BUN_JSC_validateExceptionChecks=1.

…ith new

Mock functions registered jsMockFunctionCall as both their call and construct
callback, so `new mock()` could return a non-object (undefined when the mock
has no implementation, or a primitive return value). JSC requires native
construct callbacks to return an object; Reflect.construct() on such a mock
hit an isCell() assertion in debug builds and `new mock()` evaluated to
undefined.

Add a dedicated construct callback that mirrors `new` on an ordinary JS
function: create `this` from newTarget.prototype, invoke the mock with it,
and return the mock's result only when it is an object, otherwise the created
`this`.
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Walkthrough

Adds explicit construct support to JSMockFunction: extracts shared call logic into jsMockFunctionCallImpl(thisValue, isConstruct), adds jsMockFunctionConstruct for new invocations, installs a writable .prototype/constructor on mocks, wires the construct callback, and adds tests validating mock/spy construction behavior.

Changes

Mock function constructor support

Layer / File(s) Summary
Construct host declaration and wiring
src/jsc/bindings/JSMockFunction.cpp
Declare jsMockFunctionConstruct, install a writable mock .prototype with a constructor back-reference during JSMockFunction::create, and update JSMockFunction to use the new construct host as the InternalFunction construct callback.
Shared call helper and construct-instance recording
src/jsc/bindings/JSMockFunction.cpp
Introduce jsMockFunctionCallImpl(thisValue, isConstruct) to centralize invocation logic and record call contexts; when isConstruct is true, push the provided thisValue into mock.instances.
Call wrapper and construct host implementation
src/jsc/bindings/JSMockFunction.cpp
Make jsMockFunctionCall delegate to the shared helper with isConstruct=false; implement jsMockFunctionConstruct to build thisObject from newTarget.prototype (fallback to an empty object), invoke the helper with isConstruct=true, and return the helper result if it's an object or the created thisObject otherwise.
Constructor test coverage for mock() and spyOn()
test/js/bun/test/mock-fn.test.js
Add tests that verify jest.fn() mocks and spyOn spied functions are constructable (including via Reflect.construct), record constructor calls/instances/contexts correctly, handle implementations that mutate this, prefer returned objects over created this, ignore primitive returns, respect newTarget/prototype behavior, and ensure mockRestore() cleans up spies.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing mock function construction to return an object when using new, which is the primary objective of this PR.
Description check ✅ Passed The PR description includes both required template sections with comprehensive detail: 'What does this PR do?' explains the problem, the fix, and implementation details; 'How did you verify your code works?' documents testing and verification.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@robobun

robobun commented May 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:25 PM PT - May 31st, 2026

@autofix-ci[bot], your commit 93736e3928db09fb32495f67d4e85f748bfb89a2 passed in Build #59468! 🎉


🧪   To try this PR locally:

bunx bun-pr 31386

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

bun-31386 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix crash constructing jest.fn() via Reflect.construct #30212 - Fixes the same isCell() assertion crash when constructing jest.fn() mocks via new/Reflect.construct, using a nearly identical approach (dedicated jsMockFunctionConstruct callback in JSMockFunction.cpp)

🤖 Generated with Claude Code

Comment thread src/jsc/bindings/JSMockFunction.cpp
@robobun

robobun commented May 25, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for dfc04ef (build #57920): all 72 platform build/test jobs are green on this commit — the only red status is the Buildkite roll-up, caused by a flaky first attempt of windows-2019-x64-test-bun that passed on automatic retry (the previous build's only failure was an expired darwin-14-aarch64 job, also unrelated). No test failures are related to this change; test/js/bun/test/mock-fn.test.js passed on every platform. One retrigger was already pushed, so I'm not pushing another — a Buildkite-side rebuild can green the roll-up if desired.

The fix itself has been verified against eight Fuzzilli reports of the JSCJSValue.h(1043) isCell() assertion (constructing bun:test mocks/spies via new/Reflect.construct), all of which reproduce on main and run clean with this branch.

Jarred-Sumner pushed a commit that referenced this pull request May 25, 2026
…tors (#31393)

### What does this PR do?

Fixes a fuzzer-found crash (`ASSERTION FAILED: isCell()` in
`JSCJSValue.h`, `asCell()`) triggered by constructing
`process.mainModule._compile`:

```js
Reflect.construct(process.mainModule._compile, []); // ASSERTION FAILED: isCell()
```

`Module.runMain`, `Module._resolveFilename`, `module._compile` (in
`src/jsc/modules/NodeModuleModule.cpp`) and `performance.now` (in
`src/jsc/bindings/webcore/JSPerformance.cpp`) were created with their
call function also registered as the **native construct callback**. JSC
requires a native construct callback to return an object, but these
functions return `undefined`, a string, or a number — so `new fn()` /
`Reflect.construct(fn, …)` hit `asObject()` → `ASSERT(isCell())` in
debug builds and produce an invalid `JSObject*` in release builds.

### Fix

Create these functions without a native constructor (the
`JSFunction::create` default, `callHostFunctionAsConstructor`), so
constructing them throws a `TypeError` like other native helper
functions — matching `performance.now` behavior in browsers/Node, while
calling them is unchanged.

Related: #31386 fixes the same assertion fingerprint for `bun:test` mock
functions; this PR covers the remaining non-mock instances the fuzzer
reached.

### How did you verify your code works?

- The minimized repros (`Reflect.construct(module._compile, [])`, `new
process.mainModule._compile()`,
`Reflect.construct(Module._resolveFilename, ["fs"])`, `new
Module.runMain()`, `Reflect.construct(performance.now, [])`) assert on
current main (debug build) and now throw `TypeError` with this change.
- Added regression tests:
- `node-module-module > native module functions are not constructors`
(`test/js/node/module/node-module-module.test.js`)
- `performance.now is not a constructor`
(`test/js/web/timers/performance.test.js`)

Both fail on an unfixed build and pass with this change; the full files
(29 and 7 tests) pass with the debug build, including the existing tests
that call `module._compile` / `Module._resolveFilename`.
Populate fn->instances alongside fn->contexts in the shared call helper so
`new`/Reflect.construct invocations appear in mock.instances, mirroring
jest (which records each call's `this`). Verified identical output against
jest for plain construct, object-returning impl, this-mutating impl, and
regular non-construct calls.

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

Thanks for addressing the mock.instances gap in 3b29284 — that resolves my earlier comment. I'm still deferring to a human since this is a native [[Construct]] callback in JSC bindings with GC/exception-scope interactions, plus a user-visible behavior change (mock.instances is now populated on every call where it was previously always []).

Extended reasoning...

Overview

This PR fixes a fuzzer-found assertion crash (isCell() in asObject()) when constructing a bun:test mock via new / Reflect.construct. It splits jsMockFunctionCall into a shared jsMockFunctionCallImpl(globalObject, callframe, thisValue) helper, registers a new jsMockFunctionConstruct native callback that synthesizes this from newTarget.prototype and applies ordinary [[Construct]] return-value semantics, and (in the latest commit, addressing my prior review) now pushes thisValue onto fn->instances on every invocation to match Jest. Test coverage in mock-fn.test.js exercises no-impl construct, Reflect.construct (with and without explicit newTarget), implementations writing to this, object vs primitive return values, prototype handling, and spyOn constructs.

Security risks

None identified. This is test-runner mock infrastructure; no auth/crypto/permissions surfaces are touched. The previous behavior was strictly worse (debug assert / release UB reinterpreting a non-cell as JSObject*).

Level of scrutiny

Medium-high. While scoped to bun:test mocks, the implementation lives in hand-written JSC bindings: it uses asObject(callframe->newTarget()), nested DECLARE_THROW_SCOPEs across the construct wrapper and the impl, ObjectInitializationScope + tryCreateUninitializedRestricted, and WriteBarrier updates. These patterns are copied verbatim from the adjacent calls/contexts handling and look correct to me, but JSC exception-check and GC discipline are subtle enough that a maintainer familiar with this file should sign off.

Other factors

  • My earlier inline comment about mock.instances never being written has been addressed by commit 3b292844, which records thisValue into instances on every call (matching Jest's actual behavior, not just the construct path). The new tests assert fn.mock.instances[i] accordingly.
  • This is a user-visible behavior change beyond the crash fix: mock.instances was previously always [] and is now populated on every call. Existing mockReset/mockClear tests still pass (they only assert emptiness post-clear or reference inequality), but it's worth a maintainer confirming this Jest-parity change is desired in the same PR.
  • CI was fully green on dfc04efe across all 72 platforms; 3b292844 (the instances follow-up) is a mechanical copy of the contexts block and is currently building.
  • No CODEOWNERS entry covers this path.

@robobun

robobun commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for 3b29284 (build #59409): the only red job is windows-2019-x64-test-bun, and its status history on this commit shows several success reports immediately followed by a single failure — a pass→fail oscillation, i.e. Windows-side flake, not a regression. The same job passed cleanly (11m31s) on the prior commit dfc04ef, and the only delta here is the mechanical mock.instances follow-up (a verbatim copy of the existing contexts write path), which has no Windows-specific surface. Every other platform is green or still running.

I already used my one retrigger earlier in this PR's history, so I'm not pushing another ci: retrigger — a maintainer can re-run the windows-2019-x64-test-bun job from the Buildkite UI to green the roll-up. The fix itself remains verified against the JSCJSValue.h(1043) isCell() fuzzer reports; both reviewing bots returned no actionable findings.

@robobun

robobun commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

CI update for 3b29284 (build #59409): the earlier windows-2019-x64-test-bun red cleared (it was flake — successes then a lone failure). The remaining red job is package-binary-size, which fails deterministically (exit 1, automatic retries exhausted).

This check fails when a stripped release binary grows >0.5 MB vs the latest main build. This PR's only code delta is the mock.instances follow-up — a few lines in JSMockFunction.cpp that populate one array alongside the existing contexts write — which cannot plausibly add 0.5 MB to any stripped binary. The overwhelmingly likely cause is baseline drift: the branch's fork point is behind current main, so unrelated size changes that landed on main since account for the delta. The check is advisory by design (escape hatch [skip size check] in the commit message, and soft_fail when skip-size-check is set).

Resolution is a maintainer decision — rebase onto current main to refresh the baseline, or add [skip size check] — not something fixable in the mock-construct change, and a retrigger won't help since the failure is deterministic, not flake. I've already used my one retrigger on this PR. The fix itself remains verified against the JSCJSValue.h(1043) isCell() fuzzer reports, with no actionable findings from either review bot.

@robobun

robobun commented May 31, 2026

Copy link
Copy Markdown
Collaborator Author

Merged latest main into this branch. The only red check on the prior run was package-binary-size, which compares each stripped release binary against the latest main build — the branch was ~74 commits behind, so the delta reflected that gap rather than this change (the diff here is one new native construct callback in JSMockFunction.cpp, which cannot add 0.5 MB). Refreshing against main makes the comparison like-for-like. Every test and build job was already green.

Re-verified locally after the merge: the fuzzer repro (new/Reflect.construct on a bun:test mock or spy) no longer trips ASSERTION FAILED: isCell() and returns an object; full mock-fn.test.js passes (75/75).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

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

Inline comments:
In `@src/jsc/bindings/JSMockFunction.cpp`:
- Around line 882-894: The helper jsMockFunctionCallImpl currently always
records thisValue into fn->instances, causing regular calls to populate
mock.instances; change the logic so fn->instances is only mutated when invoked
as a constructor. Concretely, add or use an "isConstruct" boolean parameter (or
detect construct-path) in jsMockFunctionCallImpl and move the instances
push/creation code so it runs only when isConstruct is true; leave
jsMockFunctionCall (regular call) calling the helper with isConstruct=false and
jsMockFunctionConstruct calling it with isConstruct=true so that fn->instances
(mock.instances) only contains values produced by the construct path. Ensure you
still use the existing initialization
(JSArray::tryCreateUninitializedRestricted, initializeIndex, fn->instances.set)
when creating the array in the construct case and preserve RETURN_IF_EXCEPTION
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d75d282-b995-4d4a-aa7d-2a3906b38057

📥 Commits

Reviewing files that changed from the base of the PR and between dfc04ef and 47f6d47.

📒 Files selected for processing (1)
  • src/jsc/bindings/JSMockFunction.cpp

Comment thread src/jsc/bindings/JSMockFunction.cpp
mock.instances contains only instances created with new, matching jest;
regular calls record this in mock.contexts instead. Thread an isConstruct
flag through the shared call helper so the instances array is populated
only on the construct path.
Comment thread src/jsc/bindings/JSMockFunction.cpp
Comment thread test/js/bun/test/mock-fn.test.js Outdated
robobun added 2 commits May 31, 2026 18:50
InternalFunction doesn't auto-create a .prototype like JSFunction, so
jest.fn().prototype was undefined: new fn() instances inherited from
Object.prototype and (new fn()) instanceof fn threw a TypeError. Install a
fresh writable prototype with a constructor back-reference at creation, so
mocks behave like ordinary JS functions under new (matches jest).
Both jest-mock and @vitest/spy push `this` onto mock.instances and
mock.contexts unconditionally on every call (no new.target check), so the
two arrays stay in lock-step. An earlier commit gated the instances push on
construct based on jest's docs, but the runtime implementation records every
call; verified against jest 29.7.0 and @vitest/spy. Revert the gating so
mock.instances is drop-in compatible, and update the test to assert the
real every-call behavior.

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

♻️ Duplicate comments (1)
src/jsc/bindings/JSMockFunction.cpp (1)

837-900: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep mock.instances constructor-only.

jsMockFunctionCallImpl() now pushes thisValue into fn->instances for every invocation, so plain calls and .call()/.apply() mutate mock.instances. Jest and Vitest both document mock.instances as the objects created via new; per-call this belongs in mock.contexts instead. This also means the new assertion in test/js/bun/test/mock-fn.test.js Lines 177-189 is codifying the wrong contract. (jestjs.io)

Suggested fix
-static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue)
+static JSC::EncodedJSValue jsMockFunctionCallImpl(JSGlobalObject* lexicalGlobalObject, CallFrame* callframe, JSValue thisValue, bool isConstruct)
 {
   ...
-    JSC::JSArray* instances = fn->instances.get();
-    if (instances) {
-        instances->push(globalObject, thisValue);
-        RETURN_IF_EXCEPTION(scope, {});
-    } else {
-        JSC::ObjectInitializationScope object(vm);
-        instances = JSC::JSArray::tryCreateUninitializedRestricted(
-            object,
-            globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
-            1);
-        instances->initializeIndex(object, 0, thisValue);
-        fn->instances.set(vm, fn, instances);
+    if (isConstruct) {
+        JSC::JSArray* instances = fn->instances.get();
+        if (instances) {
+            instances->push(globalObject, thisValue);
+            RETURN_IF_EXCEPTION(scope, {});
+        } else {
+            JSC::ObjectInitializationScope object(vm);
+            instances = JSC::JSArray::tryCreateUninitializedRestricted(
+                object,
+                globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+                1);
+            instances->initializeIndex(object, 0, thisValue);
+            fn->instances.set(vm, fn, instances);
+        }
     }
   ...
 }
 
 JSC_DEFINE_HOST_FUNCTION(jsMockFunctionCall, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
 {
-    return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue());
+    return jsMockFunctionCallImpl(lexicalGlobalObject, callframe, callframe->thisValue(), false);
 }
 
 JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
 {
   ...
-    JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject));
+    JSValue returnValue = JSValue::decode(jsMockFunctionCallImpl(lexicalGlobalObject, callframe, thisObject, true));
   ...
 }

Also applies to: 1005-1026

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

In `@src/jsc/bindings/JSMockFunction.cpp` around lines 837 - 900, The code in
jsMockFunctionCallImpl is incorrectly pushing per-call thisValue into
fn->instances; change the logic so fn->instances is only mutated when the call
is a constructor invocation (use the CallFrame constructor check, e.g.
callframe->isConstructCall() or equivalent) and for normal calls continue to
push thisValue into fn->contexts only; update the identical instances-handling
block later in the file (the section around the repeated code at lines
~1005-1026) the same way so instances remains constructor-only and contexts
records per-call this values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/jsc/bindings/JSMockFunction.cpp`:
- Around line 837-900: The code in jsMockFunctionCallImpl is incorrectly pushing
per-call thisValue into fn->instances; change the logic so fn->instances is only
mutated when the call is a constructor invocation (use the CallFrame constructor
check, e.g. callframe->isConstructCall() or equivalent) and for normal calls
continue to push thisValue into fn->contexts only; update the identical
instances-handling block later in the file (the section around the repeated code
at lines ~1005-1026) the same way so instances remains constructor-only and
contexts records per-call this values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b0ef2b14-f721-406b-b3a9-857f0e4f9f62

📥 Commits

Reviewing files that changed from the base of the PR and between b32623a and 6e0babe.

📒 Files selected for processing (2)
  • src/jsc/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock-fn.test.js

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

All three of my earlier inline comments have been addressed (mock.instances population, default .prototype install, and Jest-runtime-vs-docs semantics) and the bug hunter found nothing on the final revision — looks correct to me, but ~80 lines of new JSC construct/prototype binding code is worth a quick human pass.

Extended reasoning...

Overview

This PR fixes a fuzzer-found ASSERTION FAILED: isCell() crash when a bun:test mock function is constructed via new / Reflect.construct. It touches two files: src/jsc/bindings/JSMockFunction.cpp (adds a dedicated jsMockFunctionConstruct host callback, refactors the call path into a shared jsMockFunctionCallImpl(thisValue), installs a writable .prototype with a constructor back-ref on every mock, and populates mock.instances on each call to match jest-mock's actual runtime behavior) and test/js/bun/test/mock-fn.test.js (comprehensive coverage of new, Reflect.construct, newTarget.prototype, instanceof, primitive/object return values, mock.instances lock-step with mock.contexts, and spyOn construct cases).

Security risks

None identified. The surface is the bun:test mock-function object, which is test-only infrastructure. The new construct path reads newTarget.prototype (user-controlled but standard [[Construct]] semantics), allocates a plain object, and always returns an object — closing the prior path where a non-cell was reinterpreted as JSObject* in release builds. Exception checks (RETURN_IF_EXCEPTION) are placed after the get() and after the inner call, and the PR description reports BUN_JSC_validateExceptionChecks=1 passes.

Level of scrutiny

Medium. The change is well-scoped and follows existing patterns very closely (the instances push is a verbatim copy of the adjacent contexts push; the .prototype install mirrors how other InternalFunction subclasses in Bun do it). However, it is ~80 lines of new native JSC binding code involving a new construct host callback, throw-scope nesting, and per-mock object allocation in create(). JSC binding correctness (GC, exception propagation) is subtle enough that I'd prefer a human confirm rather than auto-approve.

Other factors

  • I previously left three inline comments on earlier revisions (populate mock.instances; install a default .prototype so instanceof works; match jest-mock's implementation of mock.instances rather than its docs). All three were addressed in commits 8149cc3b32623a6e0babe, and the conversation threads are resolved.
  • The PR went through a semantic flip-flop on mock.instances (construct-only vs. every-call) driven by a CodeRabbit comment citing Jest docs; the final state matches Jest/Vitest's actual runtime behavior and the test is written to pass under all three runners, consistent with the file's header contract.
  • CodeRabbit's latest pass returned no actionable findings; the multi-agent bug hunter found nothing on the final revision.
  • CI: test/build jobs are green across platforms; the only red is an unrelated -no-pie linker warning in build-rust jobs.
  • No CODEOWNERS cover the touched paths.

robobun and others added 2 commits May 31, 2026 19:48
A bound mock (mock.bind(...)) forwards [[Construct]] to the mock's construct
callback; fuzzing rediscovered the isCell() assertion through this path on
unfixed builds. Assert the bound construct returns an object and runs the
mock's this-mutating implementation.
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