Files
bun.sh/test/regression/issue/26286.test.ts
robobun d3f8bec565 fix(Terminal): callbacks not invoked inside AsyncLocalStorage.run() (#26288)
## Summary
- Fixed `Bun.Terminal` callbacks (data, exit, drain) not being invoked
when the terminal is created inside `AsyncLocalStorage.run()`

## Root Cause
The bug was a redundant `isCallable()` check when storing callbacks in
`initTerminal()`:

1. In `Options.parseFromJS()`, callbacks are validated with
`isCallable()`, then wrapped with `withAsyncContextIfNeeded()`
2. Inside `AsyncLocalStorage.run()`, `withAsyncContextIfNeeded()`
returns an `AsyncContextFrame` object that wraps the callback + async
context
3. An `AsyncContextFrame` is NOT callable - it's a wrapper object. So
the second `isCallable()` check fails
4. Because the check fails, the callback is never stored via
`js.gc.set()`
5. When `onReadChunk()` tries to get the callback, it returns `null` and
the callback is never invoked

## Fix
Removed the redundant `isCallable()` check in `initTerminal()`. The
check was already performed in `parseFromJS()` before wrapping. Other
similar patterns (socket Handlers, Timer) simply store the wrapped
callback without re-checking.

Fixes #26286

## Test plan
- [x] Added regression test in `test/regression/issue/26286.test.ts`
- [x] Verified test fails with system Bun (times out because callback
never invoked)
- [x] Verified test passes with debug build

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:45:22 -08:00

67 lines
1.9 KiB
TypeScript

import { expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows } from "harness";
import { AsyncLocalStorage } from "node:async_hooks";
// https://github.com/oven-sh/bun/issues/26286
// Bun.Terminal callbacks not invoked inside AsyncLocalStorage.run()
// Bun.Terminal uses PTY which is not supported on Windows
test.skipIf(isWindows)("Bun.Terminal data callback works inside AsyncLocalStorage.run()", async () => {
const storage = new AsyncLocalStorage();
async function terminalTest() {
const { promise, resolve } = Promise.withResolvers<Uint8Array>();
await using terminal = new Bun.Terminal({
data(term, data) {
resolve(data);
},
});
const process = Bun.spawn([bunExe(), "-e", "console.log('Hello')"], {
terminal,
env: bunEnv,
});
const data = await promise;
await process.exited;
return { data };
}
// Test inside AsyncLocalStorage.run()
const result = await storage.run({ testContext: true }, terminalTest);
expect(result.data).not.toBeNull();
expect(new TextDecoder().decode(result.data!)).toContain("Hello");
});
test.skipIf(isWindows)("Bun.Terminal preserves async context inside callbacks", async () => {
const storage = new AsyncLocalStorage<{ id: number }>();
async function terminalTest() {
const { promise, resolve } = Promise.withResolvers<{ id: number } | undefined>();
await using terminal = new Bun.Terminal({
data(term, data) {
resolve(storage.getStore());
},
});
const process = Bun.spawn([bunExe(), "-e", "console.log('Hello')"], {
terminal,
env: bunEnv,
});
const contextInCallback = await promise;
await process.exited;
return { contextInCallback };
}
const result = await storage.run({ id: 42 }, terminalTest);
expect(result.contextInCallback).not.toBeUndefined();
expect(result.contextInCallback?.id).toBe(42);
});