From 1e7602c08f8bcfbf8b20e8f90f56d0e6a7f38246 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Tue, 3 Nov 2026 20:22:36 +0000 Subject: [PATCH] fix(io): remove obsolete stdin fd workaround --- src/io/source.test.ts | 58 +++++++++++++++++++++++++++++++++++++++++++ src/io/source.ts | 13 +++------- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/io/source.test.ts b/src/io/source.test.ts index 8c99026bed..c29bfa6625 100644 --- a/src/io/source.test.ts +++ b/src/io/source.test.ts @@ -1,4 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -57,6 +59,62 @@ describe("SourceResolver bytes", () => { expect(await resolver.resolveBytes("payload", "-")).toEqual(bytes); }); + test.each(["bun", "node"])( + "waits for delayed process stdin under %s after importing Ink", + async (runtime) => { + const build = await Bun.build({ + entrypoints: [join(import.meta.dir, "source.ts")], + target: "node", + format: "cjs", + }); + expect(build.success).toBe(true); + const directory = await mkdtemp(join(tmpdir(), `agentcore-source-${runtime}-`)); + tempDirectories.push(directory); + const bundle = join(directory, "source.cjs"); + await writeFile(bundle, await build.outputs[0]!.text()); + + const child = spawn( + runtime, + [ + "-e", + ` + (async () => { + await import("ink"); + const { SourceResolver } = require(process.argv[1]); + const resolution = new SourceResolver({ stdin: process.stdin }) + .resolveText("bearer-token", "-"); + process.stderr.write("READY\\n"); + const value = await resolution; + process.stdout.write(value); + })().catch(error => { + console.error(error); + process.exitCode = 1; + }); + `, + bundle, + ], + { stdio: ["pipe", "pipe", "pipe"] }, + ); + let output = ""; + let errors = ""; + child.stdout.on("data", (chunk) => { + output += chunk; + }); + child.stderr.on("data", (chunk) => { + errors += chunk; + }); + + await once(child.stderr, "data"); + await new Promise((resolve) => setTimeout(resolve, 100)); + child.stdin.end("late value"); + + const [code] = await once(child, "close"); + expect(code).toBe(0); + expect(errors).toBe("READY\n"); + expect(output).toBe("late value"); + }, + ); + test("rejects a second stdin consumer and identifies both options", async () => { const resolver = new SourceResolver({ stdin: stdin(new TextEncoder().encode("value")) }); diff --git a/src/io/source.ts b/src/io/source.ts index 5454ca8c5d..f21c1fa8ea 100644 --- a/src/io/source.ts +++ b/src/io/source.ts @@ -1,4 +1,3 @@ -import { createReadStream } from "node:fs"; import { readFile } from "node:fs/promises"; import { addAbortSignal } from "node:stream"; import { buffer } from "node:stream/consumers"; @@ -83,15 +82,9 @@ export class SourceResolver { } this.stdinClaimedBy = name; - // Importing Ink under Bun drains the process.stdin stream object before a - // headless handler can claim it, while file descriptor 0 still retains the - // bytes. Use a fresh non-closing stream for the real process input; injected - // AppIO streams continue through the ordinary testable path. - const source = - this.config.stdin === process.stdin - ? createReadStream("", { fd: 0, autoClose: false }) - : this.config.stdin; - const stdin = this.config.signal ? addAbortSignal(this.config.signal, source) : source; + const stdin = this.config.signal + ? addAbortSignal(this.config.signal, this.config.stdin) + : this.config.stdin; try { return await buffer(stdin); } catch (error) {