Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
58 changes: 58 additions & 0 deletions src/io/source.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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")) });

Expand Down
13 changes: 3 additions & 10 deletions src/io/source.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
Loading