Skip to content
Closed

ai slop #29200

Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/bun.js/bindings/JSMockFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
}

JSC_DECLARE_HOST_FUNCTION(jsMockFunctionCall);
JSC_DECLARE_HOST_FUNCTION(jsMockFunctionConstruct);
JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_protoImpl);
JSC_DECLARE_CUSTOM_GETTER(jsMockFunctionGetter_mock);
JSC_DECLARE_HOST_FUNCTION(jsMockFunctionGetter_mockGetLastCall);
Expand Down Expand Up @@ -462,7 +463,7 @@
}

JSMockFunction(JSC::VM& vm, JSC::Structure* structure, CallbackKind wrapKind)
: Base(vm, structure, jsMockFunctionCall, jsMockFunctionCall)
: Base(vm, structure, jsMockFunctionCall, jsMockFunctionConstruct)
{
initMock();
}
Expand Down Expand Up @@ -978,9 +979,21 @@
}

setReturnValue(createMockResult(vm, globalObject, "return"_s, jsUndefined()));
return JSValue::encode(jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe);
RETURN_IF_EXCEPTION(scope, {});
JSValue result = JSValue::decode(encodedResult);
if (!result.isObject())
return JSValue::encode(JSC::constructEmptyObject(lexicalGlobalObject));
return encodedResult;
}

Check warning on line 995 in src/bun.js/bindings/JSMockFunction.cpp

View check run for this annotation

Claude / Claude Code Review

jsMockFunctionConstruct ignores newTarget.prototype

The fallback path in `jsMockFunctionConstruct` calls `constructEmptyObject(lexicalGlobalObject)` when the mock returns a non-object, creating a plain `Object.prototype`-based object while ignoring `callframe->newTarget()`. This means `Reflect.construct(mock, [], Foo)` returns an object that fails `instanceof Foo`, violating the ECMAScript `[[Construct]]` spec requirement that the fallback object be derived from `newTarget.prototype`.
Comment on lines +985 to +998

@coderabbitai coderabbitai Bot Apr 11, 2026

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In ECMAScript, when Reflect.construct(target, args, newTarget) calls a constructor that returns a primitive, should the result be the original receiver created from newTarget.prototype or a fresh empty object? For JavaScriptCore native constructor host functions, is CallFrame::thisValue() that pre-created receiver?

💡 Result:

In ECMAScript, Reflect.construct(target, args, newTarget) invokes the [[Construct]] internal method of target with arguments args and newTarget. For ordinary ECMAScript function objects, [[Construct]] (10.2.2) creates thisArgument via OrdinaryCreateFromConstructor(newTarget, "%Object.prototype%"), which uses newTarget.prototype (or fallback) as [[Prototype]], then calls the function body with this bound to that object. After execution, if the constructor returns a primitive (including undefined), it ignores the return value and returns thisArgument—the pre-created object from newTarget.prototype—for base constructors. For derived constructors, it throws if non-object/non-undefined is returned. Thus, the result is always the original receiver created from newTarget.prototype. For JavaScriptCore native constructor host functions (built-in functions), CallFrame::thisValue holds the pre-created receiver. Per CallFrame.h comment: "Under the constructor implemented in C++, thisValue holds the newTarget instead of the automatically constructed value. The result of this function is only effective under the "construct" context. JSValue newTarget const { return thisValue; }". However, this indicates newTarget is accessible via thisValue in C++ host constructors, but the actual receiver (this binding) is the pre-created object from OrdinaryCreateFromConstructor or equivalent, passed as thisArgument to the host function per spec BuiltinCallOrConstruct (10.3.3). The comment clarifies special handling for C++ implemented constructors where thisValue slot serves dual purpose, but standard behavior provides the pre-created empty object as this.

Citations:


🏁 Script executed:

# Search for tests related to mock functions and Reflect.construct
rg "Reflect\.construct" --type js --type ts -A 3 -B 1

Repository: oven-sh/bun

Length of output: 23495


🏁 Script executed:

# Check for any existing JSC mock constructor implementations or patterns
rg "jsMockFunction" src/bun.js/bindings/ -A 2 -B 1

Repository: oven-sh/bun

Length of output: 16827


🏁 Script executed:

# Look at the surrounding context of the jsMockFunctionConstruct function
sed -n '970,1005p' src/bun.js/bindings/JSMockFunction.cpp

Repository: oven-sh/bun

Length of output: 1546


Return the constructor receiver instead of allocating a fresh object.

The current implementation violates ECMAScript [[Construct]] semantics. When the mock implementation returns a primitive, constructEmptyObject() creates a fresh plain object, discarding the receiver that was pre-created from newTarget.prototype. Per the spec, [[Construct]] must return the pre-created receiver. The fix is to use callframe->thisValue() (the JSC equivalent of the spec's pre-created receiver) with a fallback to constructEmptyObject() for defensive safety.

💡 Proposed fix
 JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
 {
     auto& vm = JSC::getVM(lexicalGlobalObject);
     auto scope = DECLARE_THROW_SCOPE(vm);
     JSC::EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe);
     RETURN_IF_EXCEPTION(scope, {});
     JSValue result = JSValue::decode(encodedResult);
-    if (!result.isObject())
-        return JSValue::encode(JSC::constructEmptyObject(lexicalGlobalObject));
+    if (!result.isObject()) {
+        JSValue thisValue = callframe->thisValue();
+        if (thisValue.isObject())
+            return JSValue::encode(thisValue);
+        return JSValue::encode(JSC::constructEmptyObject(lexicalGlobalObject));
+    }
     return encodedResult;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe);
RETURN_IF_EXCEPTION(scope, {});
JSValue result = JSValue::decode(encodedResult);
if (!result.isObject())
return JSValue::encode(JSC::constructEmptyObject(lexicalGlobalObject));
return encodedResult;
}
JSC_DEFINE_HOST_FUNCTION(jsMockFunctionConstruct, (JSGlobalObject * lexicalGlobalObject, CallFrame* callframe))
{
auto& vm = JSC::getVM(lexicalGlobalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::EncodedJSValue encodedResult = jsMockFunctionCall(lexicalGlobalObject, callframe);
RETURN_IF_EXCEPTION(scope, {});
JSValue result = JSValue::decode(encodedResult);
if (!result.isObject()) {
JSValue thisValue = callframe->thisValue();
if (thisValue.isObject())
return JSValue::encode(thisValue);
return JSValue::encode(JSC::constructEmptyObject(lexicalGlobalObject));
}
return encodedResult;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/bindings/JSMockFunction.cpp` around lines 985 - 995, The
constructor implementation jsMockFunctionConstruct must return the pre-created
receiver instead of always allocating a fresh object: if jsMockFunctionCall
returns a non-object (primitive or undefined), read the pre-created receiver via
callframe->thisValue() and return/encode that, and only fall back to
JSC::constructEmptyObject(lexicalGlobalObject) defensively if
callframe->thisValue() is not an object; keep the existing exception handling
and continue returning encoded values from jsMockFunctionCall when it already
returns an object.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

For InternalFunction native constructors in JSC, callframe->thisValue() is newTarget() (see the comment in CallFrame.h you quoted) — there is no pre-created receiver. Returning thisValue() would return the newTarget constructor itself, not an instance.

Updated in 98a869b to use InternalFunction::createSubclassStructure so the fallback object inherits from newTarget.prototype, matching other native constructors in the codebase. Added a test for instanceof.

Comment thread
claude[bot] marked this conversation as resolved.

void JSMockFunctionPrototype::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
Comment on lines 982 to 1001

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.

🟡 When jsMockFunctionConstruct delegates to jsMockFunctionCall, the thisValue captured inside jsMockFunctionCall is callframe->thisValue(), which for JSC InternalFunction native constructors equals newTarget() (the constructor function itself). This causes two observable deviations from Jest semantics: (1) mock().mockReturnThis() used as a constructor via Reflect.construct returns the mock function itself as the result (since result.isObject() is true for functions); (2) mock.mock.contexts records the mock constructor function rather than the constructed instance for every construct invocation. Neither issue is covered by the new mock-construct.test.ts suite.

Extended reasoning...

What the bug is and how it manifests

When jsMockFunctionConstruct delegates to jsMockFunctionCall by passing the same callframe, the line 'JSValue thisValue = callframe->thisValue()' inside jsMockFunctionCall captures newTarget() rather than a pre-created instance. This is an inherent property of JSC::InternalFunction native constructors, confirmed explicitly in the PR timeline by the author: 'For InternalFunction native constructors in JSC, callframe->thisValue() is newTarget() — there is no pre-created receiver.' Two separate misbehaviors follow.

Bug 1 — mockReturnThis() in construct context returns the mock constructor itself

When mockReturnThis() is active, jsMockFunctionCall executes 'case ReturnThis: return JSValue::encode(thisValue)', returning the mock constructor function (which equals newTarget()). Back in jsMockFunctionConstruct, the check 'if (result.isObject()) return encodedResult' passes — because functions are objects — so the constructor returns the mock function itself as the constructed result. Concretely: const m = mock().mockReturnThis(); const r = Reflect.construct(m, []); r === m // true. In Jest, mockReturnThis() in a constructor would return the pre-created this (the new instance), not the constructor function.

Bug 2 — mock.mock.contexts records newTarget instead of the constructed instance

In jsMockFunctionCall, the contexts array is populated with thisValue (which equals the mock constructor function). The actual constructed instance is only synthesized later, inside jsMockFunctionConstruct, after jsMockFunctionCall has already pushed to contexts. So mock.mock.contexts[0] === m for construct calls, instead of the newly allocated instance. Jest's mock.mock.contexts[i] for a constructor call holds the newly created this-object.

Why existing code does not prevent it

Before this PR, Reflect.construct(mock(), []) crashed (assertion in executeConstruct due to the native handler returning jsUndefined()), so neither behavior was observable. The PR correctly introduces jsMockFunctionConstruct to fix the crash, and correctly uses InternalFunction::createSubclassStructure for the primitive-return fallback path. However, the two paths that explicitly return thisValue (the ReturnThis implementation case) and record thisValue (contexts) remain affected, since thisValue equals newTarget — not an instance — in the construct callframe.

Addressing the refutations

The refutations correctly note that thisValue == newTarget is a pre-existing JSC limitation inherent to InternalFunction, and that a full fix would require architectural changes. However, the observable misbehaviors are new: before this PR, any Reflect.construct call on a mock would crash, making these bugs moot. After this PR enables construct, they become observable for the first time. The bugs exist in newly reachable code paths introduced by this PR.

Step-by-step proof for Bug 1

  1. const m = mock().mockReturnThis() sets impl.kind = ReturnThis.
  2. Reflect.construct(m, []) calls jsMockFunctionConstruct.
  3. jsMockFunctionCall is invoked with the same callframe; thisValue = callframe->thisValue() equals newTarget = m (the mock function).
  4. ReturnThis branch returns JSValue::encode(thisValue) — i.e., encodes the mock function m.
  5. Back in jsMockFunctionConstruct: result.isObject() is true (functions are objects), so return encodedResult — returns m as the constructed result.
  6. r === m is true, violating expected constructor semantics. The new tests do not exercise mockReturnThis() in construct context and do not assert on mock.mock.contexts values for construct calls.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Both behaviors are pre-existing on the new operator path (which never crashed) — verified on the unpatched binary:

const m = mock().mockReturnThis();
new m() === m;              // true (pre-existing)
m.mock.contexts[0] === m;   // true (pre-existing)

This PR's scope is fixing the crash when the construct path is entered via Interpreter::executeConstruct (e.g. Reflect.construct). It doesn't change what thisValue is during construction — that's inherent to InternalFunction and, as noted, would require a larger refactor (pre-creating the instance and threading it through jsMockFunctionCall). That's a separate behavioral improvement, not part of this crash fix.

Base::finishCreation(vm);
Expand Down
17 changes: 15 additions & 2 deletions src/bun.js/bindings/webcore/Worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,21 @@ extern "C" void WebWorker__setRef(

void Worker::setKeepAlive(bool keepAlive)
{
WebWorker__setRef(impl_, keepAlive);
Locker locker { m_implLock };
if (impl_)
WebWorker__setRef(impl_, keepAlive);
}

void Worker::clearZigImpl()
{
Locker locker { m_implLock };
impl_ = nullptr;
}
Comment on lines +141 to 145

@coderabbitai coderabbitai Bot Apr 11, 2026

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Worker::updatePtr call sites =="
rg -nP '\bupdatePtr\s*\(' src --glob '*.{cpp,h,zig}'

echo
echo "== All impl_ accesses in Worker.* =="
rg -nP '\bimpl_\b' src/bun.js/bindings/webcore/Worker.cpp src/bun.js/bindings/webcore/Worker.h

echo
echo "== WebWorker__create declarations/implementations =="
rg -nP 'WebWorker__create\s*\(' src --glob '*.{cpp,h,zig}'

echo
echo "== WebWorker__dispatchExit declarations/implementations =="
rg -nP 'WebWorker__dispatchExit\s*\(' src --glob '*.{cpp,h,zig}'

Repository: oven-sh/bun

Length of output: 1545


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Worker.cpp lines 145-155 (updatePtr function context) =="
sed -n '145,155p' src/bun.js/bindings/webcore/Worker.cpp

echo
echo "== Worker.cpp lines 195-235 (WebWorker__create and impl_ assignment) =="
sed -n '195,235p' src/bun.js/bindings/webcore/Worker.cpp

echo
echo "== Worker::create full signature =="
rg -B5 'void\* WebWorker__create' src/bun.js/bindings/webcore/Worker.cpp | head -20

Repository: oven-sh/bun

Length of output: 2193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== JSWorker.cpp lines 340-360 (updatePtr call context) =="
sed -n '340,360p' src/bun.js/bindings/webcore/JSWorker.cpp

echo
echo "== Worker.cpp lines 130-155 (full context of locking changes) =="
sed -n '130,155p' src/bun.js/bindings/webcore/Worker.cpp

echo
echo "== Worker.cpp lines 270-285 (terminate path) =="
sed -n '270,285p' src/bun.js/bindings/webcore/Worker.cpp

Repository: oven-sh/bun

Length of output: 2139


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Worker constructor/create full flow =="
rg -B10 -A15 'static ExceptionOr<RefPtr<Worker>> create' src/bun.js/bindings/webcore/Worker.cpp | head -60

echo
echo "== Check if m_implLock is reentrant =="
rg -B5 -A5 'class Worker' src/bun.js/bindings/webcore/Worker.h | grep -A30 'm_implLock'

echo
echo "== Verify: does WebWorker__updatePtr check for null? =="
rg -A10 'WebWorker__updatePtr' src/bun.js/web_worker.zig | head -30

Repository: oven-sh/bun

Length of output: 96


🏁 Script executed:

#!/bin/bash
set -euo pipeflag

echo "== Worker.h - class definition and members =="
sed -n '50,130p' src/bun.js/bindings/webcore/Worker.h

echo
echo "== Check m_implLock type =="
rg -B2 -A2 'm_implLock' src/bun.js/bindings/webcore/Worker.h

echo
echo "== Worker::create implementation start =="
sed -n '160,210p' src/bun.js/bindings/webcore/Worker.cpp

Repository: oven-sh/bun

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Worker.h - class definition and members =="
sed -n '50,130p' src/bun.js/bindings/webcore/Worker.h

echo
echo "== Check m_implLock type =="
rg -B2 -A2 'm_implLock' src/bun.js/bindings/webcore/Worker.h

echo
echo "== Worker::create implementation start =="
sed -n '160,210p' src/bun.js/bindings/webcore/Worker.cpp

Repository: oven-sh/bun

Length of output: 5782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WebWorker__updatePtr in web_worker.zig =="
rg -B5 -A15 'fn WebWorker__updatePtr' src/bun.js/web_worker.zig

echo
echo "== Verify: is there a null check in WebWorker__updatePtr? =="
rg -A20 'export fn WebWorker__updatePtr' src/bun.js/web_worker.zig

Repository: oven-sh/bun

Length of output: 1186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WebWorker__dispatchExit call from Zig worker thread =="
rg -B10 -A5 'WebWorker__dispatchExit' src/bun.js/web_worker.zig

echo
echo "== Full context: when does clearZigImpl happen? =="
sed -n '475,490p' src/bun.js/bindings/webcore/Worker.cpp

echo
echo "== Check: is Worker::create and updatePtr in same synchronous call? =="
sed -n '348,360p' src/bun.js/bindings/webcore/JSWorker.cpp

Repository: oven-sh/bun

Length of output: 2492


Lock impl_ reads in updatePtr() and writes during initialization.

The new lock protections in setKeepAlive(), clearZigImpl(), and terminate() are sound, but updatePtr() still reads impl_ at line 149 without holding m_implLock, and the initial write at line 228 (Worker::create()) is also unprotected. Although the current usage pattern calls updatePtr() once synchronously during construction (before the worker thread can execute), if this function were called again—or if initialization timing changes—a worker thread running WebWorker__dispatchExit() could race with an unlocked read. The complete fix requires protecting both the read in updatePtr() and the write in Worker::create().

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

In `@src/bun.js/bindings/webcore/Worker.cpp` around lines 141 - 145, updatePtr()
currently reads impl_ without holding m_implLock and Worker::create() assigns
impl_ without locking, which can race with the new protected writers
(setKeepAlive(), clearZigImpl(), terminate()); fix by taking the same lock
(m_implLock) when accessing impl_: add a Locker (or equivalent lock guard)
around the read in updatePtr() and around the initial write to impl_ inside
Worker::create() so all reads/writes of impl_ are consistently synchronized with
the existing setKeepAlive()/clearZigImpl()/terminate() locking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

impl_ is written in Worker::create() and read in updatePtr() strictly before the worker thread is spawned (the thread is created inside WebWorker__updatePtr). No other thread can observe impl_ at those points, so no synchronization is needed there. The lock exists solely to order parent-thread terminate()/setKeepAlive() against the worker-thread clearZigImpl() in WebWorker__dispatchExit.


bool Worker::updatePtr()
{
if (!WebWorker__updatePtr(impl_, this)) {
clearZigImpl();
m_onlineClosingFlags = ClosingFlag;
m_terminationFlags.fetch_or(TerminatedFlag);
return false;
Expand Down Expand Up @@ -263,7 +272,9 @@ void Worker::terminate()
{
// m_contextProxy.terminateWorkerGlobalScope();
m_terminationFlags.fetch_or(TerminateRequestedFlag);
WebWorker__notifyNeedTermination(impl_);
Locker locker { m_implLock };
if (impl_)
WebWorker__notifyNeedTermination(impl_);
}

// const char* Worker::activeDOMObjectName() const
Expand Down Expand Up @@ -468,6 +479,8 @@ void Worker::forEachWorker(const Function<Function<void(ScriptExecutionContext&)
extern "C" void WebWorker__dispatchExit(Zig::GlobalObject* globalObject, Worker* worker, int32_t exitCode)
{
worker->dispatchExit(exitCode);
// The Zig WebWorker is about to be freed; prevent terminate()/ref()/unref() from touching it.
worker->clearZigImpl();
// no longer referenced by Zig
worker->deref();

Expand Down
2 changes: 2 additions & 0 deletions src/bun.js/bindings/webcore/Worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class Worker final : public ThreadSafeRefCounted<Worker>, public EventTargetWith
void dispatchEvent(Event&);
void dispatchCloseEvent(Event&);
void setKeepAlive(bool);
void clearZigImpl();

void postTaskToWorkerGlobalScope(Function<void(ScriptExecutionContext&)>&&);

Expand Down Expand Up @@ -119,6 +120,7 @@ class Worker final : public ThreadSafeRefCounted<Worker>, public EventTargetWith
// Tracks TerminateRequestedFlag and TerminatedFlag
std::atomic<uint8_t> m_terminationFlags { 0 };
const ScriptExecutionContextIdentifier m_clientIdentifier;
Lock m_implLock;
void* impl_ { nullptr };
};

Expand Down
47 changes: 47 additions & 0 deletions test/js/bun/test/mock-construct.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, mock, test } from "bun:test";

describe("mock() used as a constructor", () => {
test("Reflect.construct with no implementation returns an object", () => {
const m = mock();
const r = Reflect.construct(m, []);
expect(typeof r).toBe("object");
expect(r).not.toBeNull();
});

test("Reflect.construct with a different newTarget returns an object", () => {
const m = mock();
const r = Reflect.construct(m, [], function () {});
expect(typeof r).toBe("object");
expect(r).not.toBeNull();
});

test("Reflect.construct with implementation returning a primitive returns an object", () => {
const m = mock(() => 42);
const r = Reflect.construct(m, []);
expect(typeof r).toBe("object");
expect(r).not.toBeNull();
});

test("Reflect.construct with mockReturnValue(primitive) returns an object", () => {
const m = mock().mockReturnValue(7);
const r = Reflect.construct(m, []);
expect(typeof r).toBe("object");
expect(r).not.toBeNull();
});

test("Reflect.construct preserves object return values", () => {
const obj = { hello: "world" };
const m = mock(() => obj);
const r = Reflect.construct(m, []);
expect(r).toBe(obj);
});

test("calling without new still returns the implementation's value", () => {
const m1 = mock();
expect(m1()).toBeUndefined();
const m2 = mock(() => 42);
expect(m2()).toBe(42);
const m3 = mock().mockReturnValue("x");
expect(m3()).toBe("x");
});
});
35 changes: 35 additions & 0 deletions test/js/web/workers/worker-terminate-after-exit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";

describe("Worker", () => {
test("terminate()/ref()/unref() after the worker has exited does not crash", async () => {
const url = URL.createObjectURL(new Blob(["/* exits immediately */"], { type: "application/javascript" }));
try {
const workers: Worker[] = [];
for (let i = 0; i < 4; i++) {
const w = new Worker(url);
workers.push(w);
}

await Promise.all(
workers.map(w => {
const { promise, resolve } = Promise.withResolvers<void>();
w.addEventListener("close", () => resolve(), { once: true });
return promise;
}),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Bun.gc(true);

for (const w of workers) {
w.terminate();
w.ref();
w.unref();
w.terminate();
}
} finally {
URL.revokeObjectURL(url);
}

expect(true).toBe(true);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
});
Loading