Skip to content

Commit c4b01ba

Browse files
authored
chore: keep release notes in PR context
1 parent 74e7e51 commit c4b01ba

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

packages/agent-core/src/harness/env/nodejs.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,27 @@ describe("NodeExecutionEnv exec stream errors", () => {
112112
const result = await resultPromise;
113113
expect(result.ok).toBe(true);
114114
});
115+
116+
it("contains stdout errors during Windows shell discovery", async () => {
117+
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
118+
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
119+
try {
120+
const child = mockSpawnChild();
121+
const resultPromise = new NodeExecutionEnv({ cwd: process.cwd() }).exec("echo hello");
122+
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalled(), { timeout: 2000 });
123+
124+
child.stdout.emit("error", new Error("where stdout failed"));
125+
126+
const result = await resultPromise;
127+
expect(result.ok).toBe(false);
128+
if (!result.ok) {
129+
expect(result.error.code).toBe("shell_unavailable");
130+
}
131+
expect(spawnMock.mock.calls[0]?.[0]).toBe("where");
132+
} finally {
133+
if (platformDescriptor) {
134+
Object.defineProperty(process, "platform", platformDescriptor);
135+
}
136+
}
137+
});
115138
});

packages/agent-core/src/harness/env/nodejs.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,17 @@ function abortResult(
131131
return signal?.aborted ? err(new FileError("aborted", "aborted", path)) : undefined;
132132
}
133133

134+
type ChildOutputStreamName = "stdout" | "stderr";
135+
136+
function listenForChildOutputErrors(
137+
child: ReturnType<typeof spawn>,
138+
onError: (stream: ChildOutputStreamName, error: Error) => void,
139+
): void {
140+
for (const streamName of ["stdout", "stderr"] as const) {
141+
child[streamName]?.on("error", (error: Error) => onError(streamName, error));
142+
}
143+
}
144+
134145
async function pathExists(path: string): Promise<boolean> {
135146
try {
136147
await access(path, constants.F_OK);
@@ -159,13 +170,20 @@ async function runCommand(
159170
}
160171
const timeout = setTimeout(() => {
161172
if (child.pid) {
162-
killProcessTree(child.pid, { force: true });
173+
killProcessTree(child.pid, { force: true, detached: false });
163174
}
164175
}, timeoutMs);
165176
child.stdout?.setEncoding("utf8");
166177
child.stdout?.on("data", (chunk: string) => {
167178
stdout += chunk;
168179
});
180+
listenForChildOutputErrors(child, () => {
181+
if (child.pid) {
182+
killProcessTree(child.pid, { force: true, detached: false });
183+
}
184+
clearTimeout(timeout);
185+
resolveLocal({ stdout: "", status: null });
186+
});
169187
child.on("error", () => {
170188
clearTimeout(timeout);
171189
resolveLocal({ stdout: "", status: null });
@@ -374,7 +392,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
374392
// Guard stdout/stderr against stream errors (e.g. EPIPE when the
375393
// child exits before all pipe data is consumed). Without listeners,
376394
// Node.js throws an uncaught exception that crashes the process.
377-
const onStreamError = (stream: "stdout" | "stderr", error: Error) => {
395+
const onStreamError = (stream: ChildOutputStreamName, error: Error) => {
378396
if (settled) {
379397
return;
380398
}
@@ -383,8 +401,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
383401
err(new ExecutionError("spawn_error", `${stream} read error: ${error.message}`, error)),
384402
);
385403
};
386-
child.stdout?.on("error", (e) => onStreamError("stdout", e));
387-
child.stderr?.on("error", (e) => onStreamError("stderr", e));
404+
listenForChildOutputErrors(child, onStreamError);
388405

389406
child.on("error", (error) => {
390407
settle(err(new ExecutionError("spawn_error", error.message, error)));

0 commit comments

Comments
 (0)