Skip to content

test: support auto-mock for jest.mock(module) / jest.requireMock - #29836

Open
robobun wants to merge 36 commits into
mainfrom
farm/80129f56/jest-auto-mock
Open

test: support auto-mock for jest.mock(module) / jest.requireMock#29836
robobun wants to merge 36 commits into
mainfrom
farm/80129f56/jest-auto-mock

Conversation

@robobun

@robobun robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes #29834
Fixes #11018
Refs #16140 (checks off the vi.mock() no-factory form; the rest of the tracker stays open)

Before

jest.mock("./api-client"); // TypeError: mock(module, fn) requires a function

After

jest.mock(specifier) without a factory synthesises an auto-mock: the real
module is loaded synchronously and a mocked copy of its exports is generated,
following jest-mock's _getSlots rules:

  • properties are collected up the prototype chain (stopping before
    Object.prototype/Function.prototype), so subclass statics, inherited
    prototype methods, and exported class instances keep their API
  • functions and classes become jest.fn()-style mocks; new MockedClass()
    returns an instance inheriting the mocked prototype and records it in
    mock.instances
  • accessors are skipped (getters never run as a side effect of mocking),
    except on __esModule interop objects, where the getters are the exports
    (esbuild/tsc-compiled CJS)
  • primitives, arrays, dates, regexps, and other exotic objects are preserved

jest.requireMock(specifier) returns the mocked exports for a module: the
registered mock from a prior jest.mock(specifier) if there is one, or an
on-demand auto-mock cached in a side map that leaves import/require
resolution untouched. (jest only; vitest has no vi.requireMock.)

Consumer behavior, asserted by tests:

  • require(id) and import (named and default) of a mocked local module see
    the mock; CJS modules get a synthesized default mirroring
    require-to-import interop, and primitive modules keep their raw value shape
  • builtins: import "fs" / import "node:fs" and jest.requireMock("fs")
    see the mock; require("fs") bypasses the mock registry and keeps
    returning the real module (pre-existing builtin require behavior, same as
    the factory form)

How did you verify your code works?

  • bun bd test test/js/bun/test/mock/ — 39 pass, 1 todo, 0 fail on the
    debug/ASAN build, including new coverage for: subclass/instance prototype
    chains, esbuild-shaped __esModule getter modules, CJS default-export
    synthesis, primitive module shape across require/import/requireMock,
    new on mocked classes (mock.instances), plugin-provided modules
    (builder.module() entries are no longer removed by the auto-mock stash),
    and a spawned builtin-fs consumer matrix
  • BUN_JSC_validateExceptionChecks=1 run of the mock suite passes (the
    walker now propagates exceptions with RETURN_IF_EXCEPTION instead of
    clearing them)
  • USE_SYSTEM_BUN=1 bun test on the same file: 17 fail (auto-mock absent),
    confirming the tests exercise the new code
  • mock-fn/spy/disposable/jest-extended suites pass unchanged

Deliberate behavior changes beyond the no-factory form

  • Factory mocks shaped { __esModule: true, default, ... }: require() now
    returns the default in both cache states. Previously only a fresh
    require() unwrapped; re-mocking an already-required specifier left the
    whole factory object as module.exports, so the shape depended on load
    order. A factory-form test pins both orders.
  • Bun.plugin.clearAll() now resets mustDoExpensiveRelativeLookup when it
    deletes the virtual module map; leaving it set primed a debug-build assert
    in the module loader on the next ESM import (spawned regression test).
  • The promise-unwrap loop in mock.module's cached-module patch no longer
    spins forever on a still-pending promise (async factory re-mocking an
    already-required module); it now surfaces the pending promise, matching the
    requireMock path (spawned regression test).

Background

  • Bun's module mocks live in a virtualModules map consulted by the module
    loader; jest.mock(id, factory) has stored a JSModuleMock there for a
    while. The auto-mock form requires the real module first, so any prior
    mock entry is stashed and restored around that internal require. That map
    also holds Bun.plugin builder.module() callbacks, which is why the
    stash is now restricted to JSModuleMock entries.
  • Bun's CJS-to-ESM interop treats a mock object shaped
    { __esModule: true, default: X } as "module.exports is X". The primitive
    carrier uses that shape so require() of a mocked primitive module still
    returns the raw value.
  • JSMockFunction previously routed [[Construct]] through its
    [[Call]] handler, so new on any mock returned undefined; it now has
    a proper construct path (instance from new.target.prototype, recorded in
    mock.instances).

[review] gate passed · iteration 45 · 24 files touched

fails on main (without fix)
ASAN without fix: 24 failed, 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/mock-fn.test.js test/js/bun/test/mock/mock-module-non-string.test.ts test/js/bun/test/mock/mock-module.test.ts "test/regression/issue/ENG-24434.test.ts"
bun test v1.4.0 (8326d1bd3)

test/regression/issue/ENG-24434.test.ts:
(pass) jest.mock() with non-string first argument should throw TypeError [20.99ms]
(pass) jest.mock() with object as first argument should throw TypeError [3.85ms]
27 |   // (we must fail cleanly, not crash with a stack-buffer-overflow).
28 |   const jestObj = Bun.jest(import.meta.path).jest;
29 | 
30 |   expect(() => {
31 |     jestObj.mock("some-module-that-does-not-exist-abcdef");
32 |   }).toThrow(/Cannot find package|Module not found|find module/);
          ^
error: expect(received).toThrow(expected)

Expected pattern: /Cannot find package|Module not found|find module/
Received message: "mock(module, fn) requires a function"

      at <anonymous> (/workspace/bun/test/regression/issue/ENG-24434.test.ts:32:6)
(fail) jest.mock() with missing callback auto-mocks and surfaces resolution errors [6.01
... (truncated)

release without fix: 1 failed, 1 skipped
bun test v1.4.0-canary.1 (8dd0c61d1)

test/regression/issue/ENG-24434.test.ts:
(pass) jest.mock() with non-string first argument should throw TypeError [1.11ms]
(pass) jest.mock() with object as first argument should throw TypeError [0.03ms]
(pass) jest.mock() with missing callback auto-mocks and surfaces resolution errors [1.67ms]

test/js/bun/test/mock-fn.test.js:
(pass) mock() > exists as jest.fn, bunTest.mock, and vi.fn [0.02ms]
(pass) mock() > mock [0.10ms]
(pass) mock() > checks the this value > mock [0.10ms]
(pass) mock() > checks the this value > _protoImpl
(pass) mock() > checks the this value > getMockImplementation [0.02ms]
(pass) mock() > checks the this value > getMockName
(pass) mock() > checks the this value > mockClear
(pass) mock() > checks the this value > mockReset
(pass) mock() > checks the this value > mockRestore [0.01ms]
(pass) mock() > checks the this value > mockImplementation
(pass) mock() > checks the this value > mockImplementationOnce
(pass) mock() > checks the this value > withImplementation
(pass) mock() > checks the this value > mockName
(pass) mock() > checks the this value > mockReturnThis
(pass) mock() > checks the this value > moc
... (truncated)
passes on PR (with fix)
ASAN with fix: 1 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/test/mock-fn.test.js test/js/bun/test/mock/mock-module-non-string.test.ts test/js/bun/test/mock/mock-module.test.ts "test/regression/issue/ENG-24434.test.ts"
bun test v1.4.0 (8326d1bd3)

test/regression/issue/ENG-24434.test.ts:
(pass) jest.mock() with non-string first argument should throw TypeError [30.30ms]
(pass) jest.mock() with object as first argument should throw TypeError [4.51ms]
(pass) jest.mock() with missing callback auto-mocks and surfaces resolution errors [14.07ms]

test/js/bun/test/mock-fn.test.js:
(pass) mock() > exists as jest.fn, bunTest.mock, and vi.fn [1.42ms]
(pass) mock() > mock [6.21ms]
(pass) mock() > checks the this value > mock [6.35ms]
(pass) mock() > checks the this value > _protoImpl [0.71ms]
(pass) mock() > checks the this value > getMockImplementation [1.46ms]
(pass) mock() > checks the this value > getMockName [1.00ms]
(pass) mock() > checks the this value > mockClear [0.62ms]
(pass) mock() > checks the this value > mockReset [0.67ms]
(pass) mock() > checks the this value > mockRestore [0.63
... (truncated)

release with fix: 1 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 622ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/124] gen cpp.rs (cppbind)
[2/124] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 241 extern-C blocks audited
[2/124] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�
... (truncated)
diff hotspot
docs/test/mocks.mdx                                |  49 +-
 packages/bun-types/test.d.ts                       |  23 +
 src/jsc/bindings/BunPlugin.cpp                     | 456 +++++++++++++---
 src/jsc/bindings/BunPlugin.h                       |   4 +-
 src/jsc/bindings/JSMockFunction.cpp                | 366 ++++++++++++-
 src/jsc/bindings/JSMockFunction.h                  |  32 +-
 src/jsc/bindings/ModuleLoader.cpp                  |  23 +-
 src/runtime/test_runner/jest.rs                    |   5 +-
 test/js/bun/test/mock-fn.test.js                   |   9 +
 .../js/bun/test/mock/auto-mock-fixture-accessor.ts |  25 +
 test/js/bun/test/mock/auto-mock-fixture-cjs.cjs    |  18 +
 test/js/bun/test/mock/auto-mock-fixture-double.ts  |   6 +
 .../js/bun/test/mock/auto-mock-fixture-esbuild.cjs |  27 +
 test/js/bun/test/mock/auto-mock-fixture-indexed.ts |  12 +
 test/js/bun/test/mock/auto-mock-fixture-jest.ts    |  11 +
 .../js/bun/test/mock/auto-mock-fixture-ondemand.ts |   7 +
 .../bun/test/mock/auto-mock-fixture-primitive.cjs  |   3 +
 .../bun/test/mock/auto-mock-fixture-requiremock.ts |  11 +
 .../js/bun/test/mock/auto-mock-fixture-subclass.ts |  36 ++
 test/js/bun/test/mock/auto-mock-fixture-vi.ts      |  11 +
 test/js/bun/test/mock/auto-mock-fixture.ts         |  29 +
 .../bun/test/mock/mock-module-non-string.test.ts   |  84 +--
 test/js/bun/test/mock/mock-module.test.ts          | 594 ++++++++++++++++++++-
 test/regression/issue/ENG-24434.test.ts            |  10 +-
 24 files changed, 1685 insertions(+), 166 deletions(-)

gate history · 5 passed · 0 rejected · iteration 45

evidence per changed file
file                                                 reads  edits  tests
docs/test/mocks.mdx                                      2      5     50
packages/bun-types/test.d.ts                             7      5     50
src/jsc/bindings/BunPlugin.cpp                          26     33     50
src/jsc/bindings/BunPlugin.h                             1      3     50
src/jsc/bindings/JSMockFunction.cpp                     15     21     50
src/jsc/bindings/JSMockFunction.h                        2      4     50
src/jsc/bindings/ModuleLoader.cpp                        4      1     50
src/runtime/test_runner/jest.rs                          2      2     50
test/js/bun/test/mock-fn.test.js                         0      0      0
test/js/bun/test/mock/auto-mock-fixture-accessor.ts      0      1     50
test/js/bun/test/mock/auto-mock-fixture-cjs.cjs          0      1     50
test/js/bun/test/mock/auto-mock-fixture-double.ts        0      1     50
test/js/bun/test/mock/auto-mock-fixture-esbuild.cjs      0      1     50
test/js/bun/test/mock/auto-mock-fixture-indexed.ts       0      1     50
test/js/bun/test/mock/auto-mock-fixture-jest.ts          0      2     50
test/js/bun/test/mock/auto-mock-fixture-ondemand.ts      0      1     50
(+ 8 more files)

root cause · written by the author bot

The mock module binding unconditionally validated its second argument as a callable factory, so calling jest.mock(id) with no factory threw a TypeError instead of auto-mocking the module as Jest does. The fix makes the factory optional: when it is omitted, the runtime synchronously requires the real module, walks its exports to generate an auto-mock that replaces callable exports with mock functions while preserving names, prototypes, and non-callable values, and registers the result in the mock cache. It also adds jest.requireMock and vi.requireMock to return the cached auto-mock or …

@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:40 PM PT - Aug 16th, 2026

@robobun, your commit b81ecb8d49585b1bc9bbed7500d7d4ad047e695e passed in Build #99845! 🎉


🧪   To try this PR locally:

bunx bun-pr 29836

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

bun-29836 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Automatic mock/mocking #11018 - Requests automatic mocking (loading real module and replacing functions with stubs without a factory), which is exactly what this PR implements via jest.mock(specifier) without a factory
  2. Implement vi.mock() #16140 - Requests Vitest mock API support; this PR implements vi.mock() without factory and adds vi.requireMock(), directly satisfying items on the checklist

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #11018
Fixes #16140

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

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 factory-less auto-mocking to Bun’s test runner: mock.module(id) / jest.mock(id) now support optional factory, introduces jest.requireMock<T>() and vi.requireMock, implements runtime auto-mock generation and caching, and adds tests/fixtures covering auto-mock behavior, accessor protection, on-demand synthesis, and related error-handling changes.

Changes

Cohort / File(s) Summary
TypeScript definitions
packages/bun-types/test.d.ts
Adds typings for factory-less APIs: mock.module(id), jest.mock(id, factory?), jest.requireMock<T>(id), and re-exports vi.requireMock.
Runtime: mock API & requireMock binding
src/bun.js/bindings/BunPlugin.cpp
Allows auto-mock when factory is omitted/undefined, synchronously requires real module to generate an auto-mock, registers it as already-called (cached), and adds host function JSMock__jsRequireMock to return cached mocks or synthesize+cache auto-mocks.
Runtime: auto-mock generator & function mocks
src/bun.js/bindings/JSMockFunction.cpp, src/bun.js/bindings/JSMockFunction.h
Implements exports→mock transformer: converts callable exports into mocked functions (preserving name/length), mocks prototypes/static props, recursively walks objects (skipping accessors), tracks visited values to preserve identity, limits recursion depth, and exposes createAutoMockFromExports / createAutoMockedFunction.
Zig bindings: expose requireMock
src/bun.js/test/jest.zig
Adds JSMock__jsRequireMock extern and attaches requireMock to both jest and vi globals (adjusts property counts).
Tests — auto-mock behavior and parity
test/js/bun/test/mock/auto-mock.test.ts
New comprehensive tests validating auto-mock replacements (functions, classes, static methods), nested object walking, preserved primitives/arrays, getter-skipping, parity across mock.module/jest.mock/vi.mock, and jest.requireMock/vi.requireMock caching/on-demand synthesis semantics.
Tests — argument validation & error expectations
test/js/bun/test/mock/mock-module-non-string.test.ts, test/regression/issue/ENG-24434.test.ts
Adjusts tests to assert non-callable second arg throws the expected TypeError; auto-mock mode now surfaces module-resolution errors for missing modules rather than TypeError.
Fixtures — auto-mock inputs (multiple variants)
test/js/bun/test/mock/auto-mock-fixture.ts, test/js/bun/test/mock/auto-mock-fixture-accessor.ts, test/js/bun/test/mock/auto-mock-fixture-ondemand.ts, test/js/bun/test/mock/auto-mock-fixture-*.ts
Adds fixture modules exporting functions, classes, primitives, arrays, nested objects, and accessor-backed properties to exercise auto-mock creation, accessor protection, on-demand synthesis, and isolated requireMock scenarios.
Test helpers / requireMock fixtures
test/js/bun/test/mock/auto-mock-fixture-jest.ts, ...-requiremock.ts, ...-vi.ts, ...-virequiremock.ts
Adds isolated fixtures for Jest/Vi requireMock parity tests to ensure independent mock registration and fresh mock state.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully implements the auto-mock feature required by #29834 and #11018: jest.mock() without factory synthesizes auto-mocks from real modules, jest.requireMock/vi.requireMock return cached mocks, and primitives/built-ins are preserved.
Out of Scope Changes check ✅ Passed All changes directly support auto-mocking: typings, core bindings, mock function creation, on-demand caching, test fixtures, and regression tests—no unrelated scope creep detected.
Title check ✅ Passed The title clearly and concisely describes the main changes: Jest auto-mocking and jest.requireMock support.
Description check ✅ Passed The description includes the required sections and provides detailed behavior, verification results, test coverage, and intentional behavior changes.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bun-types/test.d.ts`:
- Around line 114-118: Change the return type of requireMock<T = unknown>(id:
string) and vi.requireMock to wrap T with the project's mock helper type (e.g.,
Mocked<T> or the existing MockedFunction/MockedObject helpers defined in this
file) so consumers can access .mock and mockReturnValue with correct types;
locate the requireMock declarations and replace the bare T return type with the
appropriate Mocked<T> wrapper used by the other test helpers in this file to
enable type-safe mock methods and properties.

In `@src/bun.js/bindings/BunPlugin.cpp`:
- Around line 843-856: Cached JSModuleMock results are returned directly from
the JSModuleMock path (via moduleMock->executeOnce) which bypasses the promise
normalization applied earlier for factory mocks; change the branch that handles
JSModuleMock (the code using virtualModules.get(specifier), JSModuleMock, and
moduleMock->executeOnce) to take the executeOnce() result and pass it through
the same fulfilled/rejected JSPromise normalization used elsewhere (the
normalization block applied to factory/mock returns) — preserving
RETURN_IF_EXCEPTION checks — and only then encode/return the normalized JSValue
instead of returning executeOnce() directly.

In `@src/bun.js/bindings/JSMockFunction.cpp`:
- Around line 1740-1755: The loop currently calls
object->get(lexicalGlobalObject, name) which invokes property getters; instead
fetch the property descriptor/slot (e.g., use the object's
getOwnPropertySlot/getOwnProperty or equivalent descriptor API) to detect
accessor properties and avoid calling getters: if the slot/descriptor indicates
an accessor (has getter/setter) then either copy/preserve the accessor
descriptor to mockFn (putDirect with the descriptor) or skip synthesizing a
value for that property; only call autoMockValue and object->get when the
descriptor indicates a data property. Apply the same change to the other
occurrence around the auto-mock code (the region referenced at 1790-1805) and
keep the existing exception clearing (scope.tryClearException) behavior for
safety.
- Around line 1731-1738: The loop that skips vm.propertyNames->prototype causes
mocked class constructors to lose instance methods; update the handling in the
for (auto& name : names) loop so that when name == vm.propertyNames->prototype
you do not continue, but instead call autoMockValue() on the original prototype
value and assign that mocked prototype to the mock constructor (i.e., replace
the unconditional skip with logic to retrieve the original Class.prototype, run
autoMockValue(originalProto), and set the returned value as the mock's prototype
so instances inherit mocked methods from the mock constructor).

In `@test/js/bun/test/mock/auto-mock.test.ts`:
- Around line 50-72: Add an assertion that the vi alias of requireMock returns
the same auto-mocked module as jest.requireMock: call
vi.requireMock("./auto-mock-fixture") in the tests alongside jest.requireMock
and assert that the returned object has plainFunction.mock and MyClass.mock
defined and behaves like the jest handle (including the mockReturnValue behavior
test); update the second and third tests to include a vi.requireMock(...) call
and corresponding expect checks to cover the alias wiring for vi.requireMock and
ensure it mirrors jest.requireMock.
- Around line 56-72: The second test currently reuses the same module specifier
"./auto-mock-fixture" that was auto-mocked earlier, so make the test independent
by restoring mocks after each test or by using a unique fixture; add an
afterEach hook that calls mock.restore() (or call mock.restore() at the end of
the first test) so beginModuleMockScope()/endModuleMockScope() state is cleared,
or change the second test to requireMock a different module specifier to ensure
it exercises the fresh auto-mock path.
🪄 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: 62aec59a-0086-4928-bb7f-f5764e9e92e8

📥 Commits

Reviewing files that changed from the base of the PR and between 4d615e8 and 812c5b4.

📒 Files selected for processing (8)
  • packages/bun-types/test.d.ts
  • src/bun.js/bindings/BunPlugin.cpp
  • src/bun.js/bindings/JSMockFunction.cpp
  • src/bun.js/bindings/JSMockFunction.h
  • src/bun.js/test/jest.zig
  • test/js/bun/test/mock/auto-mock-fixture.ts
  • test/js/bun/test/mock/auto-mock.test.ts
  • test/js/bun/test/mock/mock-module-non-string.test.ts

Comment thread packages/bun-types/test.d.ts
Comment thread src/bun.js/bindings/BunPlugin.cpp Outdated
Comment thread src/jsc/bindings/JSMockFunction.cpp Outdated
Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated
Comment thread test/js/bun/test/mock/auto-mock.test.ts Outdated
Comment thread test/js/bun/test/mock/auto-mock.test.ts Outdated

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/bindings/JSMockFunction.cpp`:
- Around line 1725-1726: The auto-mock property walkers currently instantiate
JSC::PropertyNameArrayBuilder with PropertyNameMode::Strings (see
JSC::PropertyNameArrayBuilder and the call to
object->methodTable()->getOwnPropertyNames), which excludes symbol-keyed
properties and drops well-known symbols like
Symbol.iterator/asyncIterator/dispose; update the PropertyNameMode argument from
PropertyNameMode::Strings to PropertyNameMode::StringsAndSymbols in each walker
(the occurrences around the JSC::PropertyNameArrayBuilder construction and
subsequent object->methodTable()->getOwnPropertyNames call) so symbol-keyed
exports and prototype members are included in the mocked module shape.

In `@test/js/bun/test/mock/auto-mock.test.ts`:
- Around line 115-126: The test must assert the real module's accessor
side-effect counter and that the accessor property was not copied onto the mock:
use jest.requireActual("./auto-mock-fixture-accessor") to get the real module
and assert its getter counter (e.g., real.getterHits or real.getterHits.mock as
appropriate) has not increased after mocking, and assert on the mocked object
that the accessor-containing object/property was skipped (e.g.,
expect(mocked.obj).toBeUndefined() or
expect(mocked).not.toHaveProperty("obj.sneaky")), while keeping the existing
checks for mocked.getterHits.mock and mocked.plain.mock.
🪄 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: 5073af0d-ad8a-4b53-bf5a-e9e9b8a38941

📥 Commits

Reviewing files that changed from the base of the PR and between 812c5b4 and c822efa.

📒 Files selected for processing (5)
  • src/bun.js/bindings/BunPlugin.cpp
  • src/bun.js/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock/auto-mock-fixture-accessor.ts
  • test/js/bun/test/mock/auto-mock-fixture-ondemand.ts
  • test/js/bun/test/mock/auto-mock.test.ts

Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated
Comment thread test/js/bun/test/mock/auto-mock.test.ts Outdated

@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

♻️ Duplicate comments (1)
test/js/bun/test/mock/auto-mock.test.ts (1)

52-86: ⚠️ Potential issue | 🟡 Minor

Make these alias/parity checks independent of earlier mock registrations.

These cases still reuse ./auto-mock-fixture, and the mock scope lives for the whole file. If jest.mock(), vi.mock(), or the later mock.module() call regresses into a no-op, the tests can still pass by reading the mock installed by the first test. Add afterEach(() => mock.restore()) or give each case its own specifier/fixture so it proves its own registration path. Based on learnings: beginModuleMockScope() / endModuleMockScope() run once per test file, and mock.restore() is what clears file-local mock entries.

Also applies to: 101-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/bun/test/mock/auto-mock.test.ts` around lines 52 - 86, The tests
reuse the same fixture "./auto-mock-fixture" so earlier mock registrations can
leak into later specs; make each test independent by either restoring file-local
mocks after each test or using unique module specifiers per test: add an
afterEach hook that calls mock.restore() (to clear file-local mock entries
created by jest.mock/vi.mock/mock.module) or change the specifiers used in the
tests that call jest.mock, vi.mock, jest.requireMock and vi.requireMock so each
case registers a fresh module (e.g., "./auto-mock-fixture-1",
"./auto-mock-fixture-2") to ensure each assertion verifies its own registration
path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/bindings/JSMockFunction.cpp`:
- Around line 1774-1819: Seed the visited map with the prototype mapping
immediately after creating mockProto so recursive autoMockValue calls won't
create a second mock for the same prototype: after constructing mockProto (the
result of JSC::constructEmptyObject) insert a visited[originalProtoObj] ->
mockProto entry (use the same map API used elsewhere for visited) before
iterating protoNames and before any calls to autoMockValue; then continue
filling mockProto and finally call mockFn->putDirect(vm,
vm.propertyNames->prototype, mockProto, 0) as existing code does.

---

Duplicate comments:
In `@test/js/bun/test/mock/auto-mock.test.ts`:
- Around line 52-86: The tests reuse the same fixture "./auto-mock-fixture" so
earlier mock registrations can leak into later specs; make each test independent
by either restoring file-local mocks after each test or using unique module
specifiers per test: add an afterEach hook that calls mock.restore() (to clear
file-local mock entries created by jest.mock/vi.mock/mock.module) or change the
specifiers used in the tests that call jest.mock, vi.mock, jest.requireMock and
vi.requireMock so each case registers a fresh module (e.g.,
"./auto-mock-fixture-1", "./auto-mock-fixture-2") to ensure each assertion
verifies its own registration path.
🪄 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: 480e4ed6-a203-42b3-92ca-097a446f8f4b

📥 Commits

Reviewing files that changed from the base of the PR and between c822efa and 3e1ff96.

📒 Files selected for processing (3)
  • src/bun.js/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock/auto-mock.test.ts
  • test/regression/issue/ENG-24434.test.ts

Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated
Comment thread src/bun.js/bindings/BunPlugin.cpp Outdated
Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/bindings/JSMockFunction.cpp`:
- Line 1785: The mock creation uses mockFn->putDirect(vm, name, mockedProp, 0)
which writes every copied member as an enumerable, writable data property and
loses original attributes; change the call to reuse the source property
slot/descriptor attributes when setting the property (read the source's property
slot/descriptor and pass the appropriate flags/attributes instead of literal 0),
so properties like non-enumerable class methods, prototype, ReadOnly/DontDelete
constants retain their original attributes; update all similar sites calling
mockFn->putDirect (including the other putDirect call sites that synthesize
mocks) to fetch and apply the source descriptor/slot flags when writing
mockedProp.
- Around line 1916-1921: Add a regression test and fixture that exports a
primitive (e.g., export default 42) and assert that both the jest.mock() path
(via require("./module")) and jest.requireMock("module") return the primitive 42
(not an object { default: 42 }); locate the auto-mock creation code in
createAutoMockFromExports / JSMockFunction.cpp where a JSObject* wrapper is
created and ensure tests fail if that wrapper leaks into CommonJS, then adjust
implementation so CommonJS require paths unwrap such primitive-wrappers (or
avoid wrapping primitives for auto-mocks) and re-run tests to confirm round-trip
safety.
🪄 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: fc846d13-6395-4640-895d-608aae9dc0ac

📥 Commits

Reviewing files that changed from the base of the PR and between 3e1ff96 and 10b3f1b.

📒 Files selected for processing (6)
  • src/bun.js/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock/auto-mock-fixture-jest.ts
  • test/js/bun/test/mock/auto-mock-fixture-requiremock.ts
  • test/js/bun/test/mock/auto-mock-fixture-vi.ts
  • test/js/bun/test/mock/auto-mock-fixture-virequiremock.ts
  • test/js/bun/test/mock/auto-mock.test.ts

Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated
Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated

@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: 4

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

1785-1785: ⚠️ Potential issue | 🟠 Major

Preserve the source property descriptors when copying members.

These putDirect(..., 0) calls re-emit every copied member as a plain enumerable/writable data property. That changes the mocked surface for non-enumerable class methods, prototype, and any read-only constants copied through the walker. Please carry over the source slot/descriptor attributes instead of hard-coding 0.

Also applies to: 1826-1832, 1893-1893

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/bindings/JSMockFunction.cpp` at line 1785, The code is overwriting
source property descriptors by always calling mockFn->putDirect(vm, name,
mockedProp, 0); instead of preserving attributes; fix by reading the original
property's slot/descriptor attributes from the source (the property's
attributes/flags or PropertyDescriptor retrieved for the source object/member)
and pass those attribute flags into putDirect rather than 0, so non-enumerable,
non-writable, accessor, and prototype flags are preserved; apply the same change
at the other occurrences where putDirect(..., 0) is used for copying members.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/bindings/BunPlugin.cpp`:
- Around line 597-637: The auto-mock path calls the bound require (via
Bun::JSCommonJSModule::createBoundRequireFunction / JSC::profiledCall) which can
resolve an existing entry in onLoadPlugins.virtualModules (populated by
addModuleMock / builder.module), causing the mock to be built from a prior
virtual module instead of the real module; change the auto-mock bootstrap to
bypass virtual-module entries when resolving the real module—either by invoking
a require/resolver variant that ignores onLoadPlugins.virtualModules or by
temporarily removing/isolating the specifier entry from
onLoadPlugins.virtualModules around the profiledCall, then call
Bun::createAutoMockFromExports with the true realExports as before and restore
the virtual-module state. Ensure this change is applied to both places where the
factory-less auto-mock path constructs realExports (the shown block and the
similar block at the other location).

In `@src/bun.js/bindings/JSMockFunction.cpp`:
- Around line 1863-1864: The current plain-object own-property collection uses
PropertyNameMode::StringsAndSymbols with DontEnumPropertiesMode::Exclude which
drops non-enumerable own properties; change the collection to use the same
own-property walk as the function/prototype paths (i.e., include DontEnum
properties and private symbols as those paths do) by adjusting the
PropertyName/PropertyMode flags passed to
JSC::PropertyNameArrayBuilder/getOwnPropertyNames and iterate the original
object's own properties rather than only enumerable ones; when creating
properties on the mock, copy the full property attributes
(writable/configurable/enumerable, getters/setters) from the original (use the
original property descriptor and define the property with the same attributes)
so non-enumerable/hidden methods and constants are preserved on the mocked
object.

In `@test/js/bun/test/mock/auto-mock.test.ts`:
- Around line 1-6: The new test cases in auto-mock.test.ts should be merged into
the existing mock-module test file instead of being a new file: open the
existing mock tests for module mocking and move the tests and any helper imports
from auto-mock.test.ts into that file (preserve the "Auto-mock" describe/it
blocks or adapt them to the existing describe hierarchy), then delete
auto-mock.test.ts; ensure you retain test names and assertions (e.g., any
"jest.mock/vi.mock" and "requireMock" cases) and update imports/exports so the
moved cases compile and run in the original mock test suite.

---

Duplicate comments:
In `@src/bun.js/bindings/JSMockFunction.cpp`:
- Line 1785: The code is overwriting source property descriptors by always
calling mockFn->putDirect(vm, name, mockedProp, 0); instead of preserving
attributes; fix by reading the original property's slot/descriptor attributes
from the source (the property's attributes/flags or PropertyDescriptor retrieved
for the source object/member) and pass those attribute flags into putDirect
rather than 0, so non-enumerable, non-writable, accessor, and prototype flags
are preserved; apply the same change at the other occurrences where
putDirect(..., 0) is used for copying members.
🪄 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: 50e21da0-15d4-495d-b4a6-5fbf7f5af715

📥 Commits

Reviewing files that changed from the base of the PR and between 10b3f1b and 1417f57.

📒 Files selected for processing (3)
  • src/bun.js/bindings/BunPlugin.cpp
  • src/bun.js/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock/auto-mock.test.ts

Comment thread src/jsc/bindings/BunPlugin.cpp
Comment thread src/jsc/bindings/BunPlugin.cpp
Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated
Comment thread test/js/bun/test/mock/auto-mock.test.ts Outdated
Comment thread src/jsc/bindings/BunPlugin.cpp
@robobun

robobun commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

CI (build #48564) finished with 3 Windows-only failures, all unrelated to this PR:

Neither test file contains jest.mock / mock.module / requireMock / JSMockFunction / BunPlugin — this PR does not exercise those code paths. 69/73 steps passed; linux-x64-asan-test-bun passed with all mock tests green.

Can a maintainer retry the failing Windows jobs? Also happy to push a no-op to rebuild.

Comment thread src/bun.js/bindings/BunPlugin.cpp Outdated
Comment thread src/jsc/bindings/BunPlugin.cpp
Comment thread src/bun.js/bindings/BunPlugin.cpp Outdated
Comment thread src/jsc/bindings/JSMockFunction.h
Comment thread src/bun.js/bindings/BunPlugin.cpp
Comment thread src/bun.js/bindings/JSMockFunction.h Outdated
Comment thread src/bun.js/bindings/JSMockFunction.cpp Outdated
Comment thread test/js/bun/test/mock/auto-mock-fixture-jest.ts Outdated
@robobun

robobun commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI (build #49041) failed with 6 errors — the same Windows / Linux-ASAN flakes that appeared on every build of this PR across multiple commits, none of which touch any of the code this PR adds:

  • test/cli/install/bun-create.test.ts — code 1 on 🪟 2019 x64
  • test/js/bun/http/serve-stream-reject-flush-leak.test.tsinternal assertion failure: allocators do not match on Windows (all 3 platforms)
  • test/js/bun/websocket/websocket-server.test.ts — code 1 on Windows
  • test/js/web/fetch/fetch-http2-client.test.ts — timeout on 🐧 13 x64-asan
  • test/js/bun/test/parallel/test-integration-rspack.ts — segfault on 🪟 11 aarch64 (documented NAPI-finalizer class, test/no-validate-exceptions.txt)
  • test/cli/install/bun-install-registry.test.ts — code 1 (auto-marked flaky)

Confirmed unrelated: grep -E "jest\.mock|mock\.module|requireMock|JSMockFunction|BunPlugin|auto-mock" returns zero matches across all 6 files. Also pushed a tightening to the stash-and-restore exception handling (a6a2261) — independent from the CI issue but correct either way.

Happy to push a no-op to retry.

@robobun

robobun commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author

Build #49053 hit the same 3 pre-existing flakes (down from 6 on the previous retry):

  • test/js/bun/http/serve-stream-reject-flush-leak.test.ts — the "allocators do not match" class that commit 33e48bd (ci: fix server.allocator vtable mismatch + 3 broken-on-release tests #29926, "fix server.allocator vtable mismatch + 3 broken-on-release tests") was specifically trying to resolve.
  • test/cli/install/migration/complex-workspace.test.ts — listed in test/no-validate-*.
  • test/js/bun/websocket/websocket-server.test.ts — listed in test/no-validate-* + recent skip/UAF fixes (29885, 29856).

Zero mock/plugin-related content in any of them.

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

Everything I flagged earlier has been addressed and nothing new turned up on this pass — but given the scope (~300 lines of new JSC binding code across BunPlugin.cpp/JSMockFunction.cpp, new GC-rooted cache, exception-safe stash/restore around module loading), this warrants a human pass before landing.

Extended reasoning...

Overview

This PR implements Jest's factory-less jest.mock(id) auto-mock and adds jest.requireMock() / vi.requireMock(). It touches:

  • src/bun.js/bindings/BunPlugin.cpp (~200 new lines): an auto-mock branch in JSMock__jsModuleMock that synchronously requires the real module, stashes/restores prior virtualModules and requireMap entries around the require, and a new JSMock__jsRequireMock host function with its own side-cache lookup.
  • src/bun.js/bindings/JSMockFunction.cpp (~300 new lines): a recursive autoMockValue walker over module exports (functions → jest.fn(), prototypes mocked, objects recursed, accessors skipped, integer keys routed via putDirectIndex, cycle detection via a visited map, depth cap).
  • src/bun.js/bindings/JSMockFunction.h: new requireMockCache Strong<JSMap> field on JSMockModule and two new function declarations.
  • src/bun.js/test/jest.zig: wires requireMock onto jest and vi.
  • packages/bun-types/test.d.ts: typings.
  • 8 new fixture files + ~215 new test lines in mock-module.test.ts, plus updates to two existing tests whose expectations changed.

Security risks

None identified. This is test-runner infrastructure (only reachable via bun:test), operates on user-supplied module specifiers that already flow through the existing mock.module() resolver, and adds no new privilege boundaries.

Level of scrutiny

High. This is non-trivial new C++ in JSC bindings that interacts with module loading, the CJS require cache, the virtual-module map, JSC exception-scope discipline, and GC roots. The PR went through ~10 review rounds (CodeRabbit + my own inline comments) fixing real issues: accessor invocation, prototype mocking, symbol-keyed properties, attribute preservation, on-demand cache idempotency, cache isolation from global resolution, exception-safe stash/restore, integer-key putDirect asserts, and requireMockCache lifecycle. All of those are now addressed and resolved, and the bug hunter found nothing on the latest commit (a6a2261). But the volume and subtlety of issues found during review is itself a signal that the surface area is large enough to merit human sign-off on the final shape — particularly the restoreStash exception-juggling lambda in BunPlugin.cpp and the requireMockCache clearing semantics in JSMock__resetSpies.

Other factors

  • Test coverage is thorough: every review-driven fix has a matching regression test in mock-module.test.ts.
  • CI failures on the latest builds are documented pre-existing flakes (websocket-server, serve-stream-reject-flush-leak, rspack NAPI) unrelated to mock code.
  • Two existing tests (mock-module-non-string.test.ts, ENG-24434.test.ts) had their expectations changed because mock.module(id) without a callback no longer throws a TypeError — that's an intentional behaviour change, not a regression, but worth a human confirming the removed "does not run the resolver when callback is missing" subprocess test is acceptable to drop.

Comment thread src/bun.js/bindings/BunPlugin.cpp Outdated
Comment thread src/bun.js/bindings/BunPlugin.cpp Outdated
Comment thread packages/bun-types/test.d.ts Outdated
@robobun

robobun commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI (build #49132) failed with 1 error — test/bake/dev/ssg-pages-router.test.ts on 🐧 13 x64-asan — a bake dev-server test timing out waiting for the HMR websocket "/[Bun] (Live|Hot-module)-reloading socket connected, waiting for changes/" line. This is the same flake class that drove the deflake commits for bake/dev/css.test.ts (#28300) and bake/dev-and-prod.test.ts (see PRs #29534/#29880). Zero mock/plugin content in the file.

Comment thread src/jsc/bindings/BunPlugin.cpp

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

No bugs found in the current revision, but this is a substantial new feature (~600 lines of new JSC bindings for the auto-mock walker + requireMock host function, new GC-rooted cache, exception-safe stash/restore around module re-evaluation) that also changes the existing behavior of mock.module(id) with no factory — worth a human pass before merge.

Extended reasoning...

Overview

This PR implements Jest-style auto-mocking: jest.mock(id) / mock.module(id) without a factory now synchronously loads the real module and synthesises a mock (functions → jest.fn(), classes → mock constructors with mocked prototypes, plain objects recursed, primitives/arrays/builtins preserved), and adds jest.requireMock / vi.requireMock. The implementation spans ~330 new lines in BunPlugin.cpp (auto-mock branch in JSMock__jsModuleMock with stash/restore of virtualModules + requireMap entries around an internal require(), plus a new JSMock__jsRequireMock host function with a side-cache), ~300 new lines in JSMockFunction.cpp (the recursive autoMockValue walker with cycle detection, accessor skipping, indexed-key handling, attribute preservation), header/Zig wiring, type declarations, and ~215 lines of new tests across 8 fixtures.

Security risks

None identified. This is test-runner-only surface (jest/vi/mock globals), takes string specifiers that flow through the existing module resolver, and doesn't touch auth/crypto/permissions/network. The recursive walker is depth-limited (16) and cycle-safe.

Level of scrutiny

High. This is non-trivial new C++ in the JSC bindings layer with several subtle correctness concerns that surfaced and were fixed across ~15 review iterations: exception-safety of the stash/restore block (4 separate commits to close gaps), putDirect vs putDirectIndex for integer keys (would have tripped a debug assert), requireMockCache lifecycle (would have leaked across test files), accessor-getter invocation, prototype seeding order, promise unwrapping, and the virtualModules vs side-cache distinction for Jest-compat. It also changes existing behavior: mock.module(id) with no second arg previously threw TypeError and now auto-mocks — two existing tests (mock-module-non-string.test.ts, ENG-24434.test.ts) were rewritten to accommodate this.

Other factors

All CodeRabbit and claude[bot] inline comments are resolved. CI has been retried multiple times for unrelated Windows/ASAN flakes; the mock-specific tests pass on all lanes. Test coverage for the new feature is thorough (parity across mock.module/jest.mock/vi.mock, requireMock caching/on-demand, accessor protection, indexed keys, error-path stash restoration, restoreAllMocks cache clearing). Given the volume of new bindings code, the behavioral change to an existing API, and the number of correctness issues found and fixed during review, a human maintainer should sign off on the final shape.

@Jarred-Sumner
Jarred-Sumner force-pushed the farm/80129f56/jest-auto-mock branch from 8cd5999 to bbec484 Compare May 4, 2026 10:29
@robobun
robobun force-pushed the farm/80129f56/jest-auto-mock branch from bbec484 to 9e579fd Compare May 14, 2026 09:24
Comment thread src/jsc/bindings/JSMockFunction.cpp Outdated
Comment thread src/runtime/test_runner/jest.zig Outdated
robobun and others added 17 commits August 17, 2026 02:30
The stash/restore locals (stashedVirtualEntry, restoreStash lambda) are
block-scoped to the `if (isAutoMock)` branch. They go out of scope at
the block's closing brace — but the shared post-block code has several
more RETURN_IF_EXCEPTION guards (the ESM-namespace-patching block in
particular) before `addModuleMock()` finally re-installs the new mock.
If any of those guards fired the prior factory-mock / builder.module()
entry would stay permanently dropped.

Re-insert stashedVirtualEntry into virtualModules just before the block
closes. `addModuleMock()` unconditionally `set()`s the new mock on
the success path, so this re-insert is a no-op there. Any post-block
early-return now leaves the previous entry intact.
The Rust rewrite in 23427db ported src/bun.js/test/jest.zig's
createMockObjects to src/runtime/test_runner/jest.rs but didn't carry
over the requireMock bindings from this PR. Result: jest.requireMock
and vi.requireMock were undefined after the rebase.

Add JSMock__jsRequireMock as a require_mock_fn under both jest.* and
vi.*, bump the createEmptyObject capacities from 9→10 and 6→7 to
match, and declare the extern.
The class-prototype walker skipped `constructor` when copying members
but never wrote the reciprocal back-reference, so
`MockedClass.prototype.constructor` resolved to `Object` (inherited
from `Object.prototype`) instead of `MockedClass`. Jest's auto-mock
explicitly sets this pointer in `_generateMock`, and the ES2015 class
invariant `Class.prototype.constructor === Class` is how consumer code
branches on instance identity.

Add the one-line `mockProto->putDirect(vm, propertyNames->constructor,
mockFn, DontEnum)` after the loop. Regression asserted in the existing
"mock.module without a factory auto-mocks exported functions" test.
Both restoreStash() and the post-block re-seat write to
onLoadPlugins.virtualModules->set() guarded only by the
stashedVirtualEntry Strong<> — which stays truthy regardless of what
happened to the map pointer while boundRequire() ran user JS. If a
module under boundRequire() calls Bun.plugin.clearAll() (which does
`delete virtualModules; virtualModules = nullptr`), both set() calls
would deref null.

Re-check hasVirtualModules() at both sites. If the map has been
destroyed we don't try to resurrect the stashed entry — it's already
gone from the user's perspective.
The specifier-resolution logic (file: URL handling, Bun__resolveSyncWithSource
with the relative-URL fallback, mustDoExpensiveRelativeLookup flag) was
duplicated between JSMock__jsModuleMock's resolveSpecifier lambda and the
inline block in JSMock__jsRequireMock. The two must produce identical
resolved keys — jest.requireMock(id) has to find the same virtualModules
entry jest.mock(id) installed — so the copy was a drift hazard.

Extract a static resolveModuleMockSpecifier(...) that mutates specifier /
specifierString in place and returns false if it threw. Both host functions
now call it, so there's a single source of truth.
…k path

resolveModuleMockSpecifier set onLoadPlugins.mustDoExpensiveRelativeLookup
unconditionally. That flag's invariant is that it's only true while
virtualModules is non-null — the module loader asserts
!mustDoExpensiveRelativeLookup whenever virtualModules == nullptr
(ZigGlobalObject.cpp:3399). jest.mock upholds it (it always reaches
addModuleMock, which allocates the map), but jest.requireMock caches in a
separate side-map and never allocates virtualModules. So
jest.requireMock("file:./x") in a file that never called jest.mock left
the flag set with a null map, tripping the assert on the next ESM import.

Gate the flag behind a setExpensiveLookupFlag param: true for jest.mock,
false for jest.requireMock. Also drops the helper's dead bool return (both
callers use RETURN_IF_EXCEPTION) in favor of void.

Regression test spawns a fresh process (virtualModules starts null),
jest.requireMock("file:./real"), then import()s a real module — fails with
the exact assert before this fix, passes after.
The latest WebKit drop no longer transitively provides JSMap's full
definition where Strong<JSMap>::set is instantiated, so the
requireMockCache field failed to compile with 'incomplete type
JSC::JSMap used in type trait expression'. Include it directly.
… unmocked modules

Two findings on the jest.mock auto-mock branch:

1. resolveModuleMockSpecifier sets mustDoExpensiveRelativeLookup for ./
   and file: specifiers assuming addModuleMock() (which allocates
   virtualModules) will follow. When the internal require() throws — a
   typo'd relative path — none of the failure paths reached it, leaving
   the flag set with virtualModules == nullptr and tripping
   ASSERT(!mustDoExpensiveRelativeLookup) in moduleLoaderResolve on the
   next ESM import. restoreStash() now re-establishes the invariant:
   clear the flag whenever virtualModules doesn't exist. This is the
   jest.mock sibling of the requireMock variant fixed earlier.

2. The requireMap removal before the internal require() fired for any
   previously-require()'d module, forcing a re-evaluation of the source
   (double-running top-level side effects) even when the module was
   never mocked. Only a prior mock (stashedVirtualEntry non-empty) can
   have patched the cached .exports, so gate the removal on it; an
   unmocked cached entry is reused as-is.

Both paths get spawned regression tests that reproduce the exact
failure on the unfixed build (the ZigGlobalObject assert, and
__sideEffectRuns == 2 respectively).
…ock shapes

- Replace the three own-property loops with one chain-walking collector
  matching jest-mock's _getSlots: own string names up the prototype chain
  (stopping before Object.prototype/Function.prototype), so subclass
  statics, inherited prototype methods and exported instances keep their
  API. Accessors are read when the owner has __esModule (esbuild/tsc CJS
  output), mock slots are written as plain properties, and exceptions
  propagate instead of being cleared.
- Give JSMockFunction a [[Construct]] path: instances inherit
  new.target.prototype and are recorded in mock.instances, so
  'new MockedClass()' works.
- Synthesize a 'default' export when the real module was not an ES
  namespace, and give primitive carriers the { default, __esModule }
  interop shape so require() keeps returning the raw value.
- Only stash JSModuleMock entries when auto-mocking; builder.module()
  plugin entries stay in place so the internal require() can find them.
- Move mustDoExpensiveRelativeLookup into addModuleMock so the flag can
  only be set once virtualModules exists.
- Drop the requireMock cache clear from resetSpies (Jest's
  restoreAllMocks does not touch the module registry) and remove
  vi.requireMock (vitest has no such method).
- Update docs, types, and tests; add fixtures for subclass/instance,
  CJS, esbuild-shaped, primitive and plugin-provided modules, plus a
  builtin fs consumer-behavior test.
…ock prototype, reify lazy prototypes

- Auto-mocks of object exports already match the real require() shape, so
  a new JSModuleMock.suppressESModuleInterop flag keeps the loader, the
  cached-module patch and requireMock from unwrapping { __esModule,
  default } out of them; the primitive carrier and factory mocks keep the
  interop. require() of an auto-mocked esbuild/tsc module returns the
  full mocked exports object again.
- The property walk stops at JSMockFunctionPrototype so re-mocking an
  already-mocked value (repeat jest.mock() of an imported ESM module)
  doesn't copy mockReturnValue/mockClear/... onto the new mock as stubs.
- Read the source function's prototype through a PropertySlot instead of
  getDirect so a plain 'export function Foo() {}' with a lazily reified
  prototype still mocks with one.
- Tests: esbuild fixture now covers jest.mock + require + requireMock,
  repeat-mock over a statically imported fixture, LegacyCtor instanceof,
  factory { __esModule, default } unwrap in both cache states; stale
  flag-mechanism comments in the spawned tests trimmed to the invariant.
clearAll() deletes virtualModules, so leaving the flag set primed the
module loader's flag-without-map assert on the next ESM import. Covered
by a spawned test (jest.mock file: specifier, clearAll, then import).
The promise-unwrap loop in mock.module's cached-module patch exited the
switch but not the loop on Status::Pending, so an async factory re-mocking
an already-required module hung the process. Surface the pending promise
instead, matching the requireMock path. Spawned regression test.
@robobun
robobun force-pushed the farm/80129f56/jest-auto-mock branch from e323b0d to 6670956 Compare August 17, 2026 02:33
Comment thread src/jsc/bindings/BunPlugin.cpp Outdated
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto today's main (6670956); the clearAll flag reset folded into main's #32261 during the rebase, with the spawned regression test kept. Two additions since your last look, both noted in the new "Deliberate behavior changes" section of the body:

  • the pending-promise spin in mock.module's cached-module patch is fixed (surfaces the pending promise like the requireMock path, spawned regression test)
  • the factory { __esModule, default } unwrap consistency and the clearAll reset are called out explicitly

Full mock suite is green on a debug build of the rebased head.

Comment thread src/jsc/bindings/ModuleLoader.cpp Outdated
Comment thread test/js/bun/test/mock/mock-module.test.ts
Comment thread test/js/bun/test/mock/mock-module.test.ts
Comment thread src/jsc/bindings/BunPlugin.cpp Outdated
Extract unwrapSynchronouslySettledPromise for the two mock unwrap sites,
rename the unused interop out-param on the ESM fetch path, run the six
independent spawned tests with test.concurrent, and give the async
re-mock fixture a real assertion.
Comment thread src/jsc/bindings/BunPlugin.cpp
Comment thread src/jsc/bindings/ModuleLoader.cpp
Comment thread src/jsc/bindings/JSMockFunction.cpp
Comment thread src/jsc/bindings/JSMockFunction.cpp
Comment thread src/jsc/bindings/BunPlugin.cpp
Comment thread src/jsc/bindings/JSMockFunction.cpp
JSMockFunction extends InternalFunction, which has no default prototype
property, so 'new (jest.fn())() instanceof fn' threw a TypeError. Install
the ordinary-function pair (prototype with a constructor back-reference);
the auto-mock class path overwrites it with the mocked source prototype.
Also mark a rejected promise handled before rethrowing its reason in
unwrapSynchronouslySettledPromise, matching the other rejected-to-throw
sites, so the error doesn't surface again as an unhandledRejection.
Comment thread src/jsc/bindings/JSMockFunction.cpp
Comment thread src/jsc/bindings/JSMockFunction.cpp
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.

Automatic mock/mocking Support jest.mock(module) in Bun test runner

2 participants