Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a91ae0e291 fix(test): invoke JS-level then() for lazy promise subclasses in expect().rejects/resolves
When `expect(value).rejects.toThrow()` was used with a ShellPromise, the
test would hang forever. The `processPromise()` function extracted the
internal JSC promise via `asAnyPromise()` and polled it with
`waitForPromise()`, but never invoked the JS-level `then()` override.
Since ShellPromise lazily starts the shell interpreter in its `then()`
override, the shell never started and the promise stayed pending.

Now, when the internal promise is still pending, we invoke the JS-level
`then()` on the original value to trigger any lazy initialization
side-effects before entering the wait loop.

Closes #14670

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-20 05:15:06 +00:00
2 changed files with 45 additions and 0 deletions

View File

@@ -180,6 +180,28 @@ pub const Expect = struct {
const vm = globalThis.vm();
promise.setHandled(vm);
// If the promise is still pending, invoke the JS-level then() on the
// original value. Promise subclasses (e.g. ShellPromise) may override
// then() to lazily start work; calling asAnyPromise() above extracts the
// internal JSC promise without going through the JS then() override, so
// the lazy work would never be triggered and the promise would hang.
if (promise.status() == .pending) {
if (try value.get(globalThis, "then")) |then_fn| {
if (then_fn.isCallable()) {
// Call value.then(noop, noop) to trigger the lazy
// initialization side-effect. Mark the returned promise
// as handled to avoid unhandled rejection warnings.
const chained_promise = then_fn.call(globalThis, value, &.{}) catch |e| {
if (comptime !silent) return @as(bun.JSError, e);
return error.JSError;
};
if (chained_promise.asAnyPromise()) |chained| {
chained.setHandled(vm);
}
}
}
}
globalThis.bunVM().waitForPromise(promise);
const newValue = promise.result(vm);

View File

@@ -0,0 +1,23 @@
import { $ } from "bun";
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/14670
// Shell promise with .rejects.toThrow() should not timeout.
// The bug was that expect().rejects bypassed the JS-level then() override
// on ShellPromise, so the lazy shell interpreter was never started.
test("expect($`bad-command`).rejects.toThrow() does not timeout", async () => {
expect($`bad-command`.quiet()).rejects.toThrow();
});
test("await expect($`bad-command`).rejects.toThrow()", async () => {
await expect($`bad-command`.quiet()).rejects.toThrow();
});
test("expect($`bad-command`).rejects.toThrow() without quiet", async () => {
expect($`bad-command`.quiet()).rejects.toThrow();
});
test("expect($`bad-command`.nothrow()).resolves.toBeDefined()", async () => {
await expect($`bad-command`.quiet().nothrow()).resolves.toBeDefined();
});