Skip to content

Commit b41bd6d

Browse files
committed
fix(agents): harden tool search child streams
1 parent 32aa71c commit b41bd6d

3 files changed

Lines changed: 24 additions & 84 deletions

File tree

scripts/proof/tool-search-code-mode-stream-errors.mts

Lines changed: 0 additions & 67 deletions
This file was deleted.

src/agents/tool-search.stream-errors.test.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { EventEmitter } from "node:events";
44
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
55

66
type MockSpawnChild = EventEmitter & {
7-
stdout?: EventEmitter & { setEncoding?: (enc: string) => void };
87
stderr?: EventEmitter & { setEncoding?: (enc: string) => void };
98
send?: (message: unknown, callback?: (error?: Error | null) => void) => boolean;
109
connected?: boolean;
@@ -13,16 +12,13 @@ type MockSpawnChild = EventEmitter & {
1312

1413
function createMockSpawnChild() {
1514
const child = new EventEmitter() as MockSpawnChild;
16-
const stdout = new EventEmitter() as MockSpawnChild["stdout"];
17-
stdout!.setEncoding = vi.fn();
1815
const stderr = new EventEmitter() as MockSpawnChild["stderr"];
1916
stderr!.setEncoding = vi.fn();
20-
child.stdout = stdout;
2117
child.stderr = stderr;
2218
child.connected = true;
2319
child.kill = vi.fn();
2420
child.send = vi.fn(() => true);
25-
return { child, stdout, stderr };
21+
return { child, stderr };
2622
}
2723

2824
vi.mock("node:child_process", async () => {
@@ -43,42 +39,52 @@ vi.mock("node:child_process", async () => {
4339

4440
const spawnMock = vi.mocked(spawn);
4541

46-
let testing: typeof import("./tool-search.js").testing;
42+
let toolSearch: typeof import("./tool-search.js");
4743

4844
describe("tool-search code-mode stream errors", () => {
4945
beforeAll(async () => {
50-
testing = (await import("./tool-search.js")).testing;
46+
toolSearch = await import("./tool-search.js");
5147
});
5248

5349
afterEach(() => {
54-
testing.setToolSearchCodeModeSupportedForTest(undefined);
55-
testing.setToolSearchMinCodeTimeoutMsForTest(undefined);
50+
toolSearch.testing.setToolSearchCodeModeSupportedForTest(undefined);
51+
toolSearch.testing.setToolSearchMinCodeTimeoutMsForTest(undefined);
5652
});
5753

58-
it("rejects when the code-mode child stderr emits an error", async () => {
59-
testing.setToolSearchCodeModeSupportedForTest(true);
60-
testing.setToolSearchMinCodeTimeoutMsForTest(1000);
54+
it("rejects stderr errors and leaves the unused stdout unpiped", async () => {
55+
toolSearch.testing.setToolSearchCodeModeSupportedForTest(true);
56+
toolSearch.testing.setToolSearchMinCodeTimeoutMsForTest(1000);
6157

58+
let spawnedChild: MockSpawnChild | undefined;
6259
spawnMock.mockImplementationOnce(
6360
(_command: string, _args: readonly string[], _options: SpawnOptions): ChildProcess => {
6461
const { child, stderr } = createMockSpawnChild();
62+
spawnedChild = child;
6563
process.nextTick(() => {
6664
stderr?.emit("error", new Error("stderr read failed"));
6765
});
6866
return child as unknown as ChildProcess;
6967
},
7068
);
7169

72-
const runtime = new testing.ToolSearchRuntime({}, testing.resolveToolSearchConfig({}));
70+
const runtime = new toolSearch.ToolSearchRuntime(
71+
{},
72+
toolSearch.testing.resolveToolSearchConfig({}),
73+
);
7374

7475
await expect(
75-
testing.runCodeModeChild({
76+
toolSearch.testing.runCodeModeChild({
7677
code: "return 1;",
77-
config: testing.resolveToolSearchConfig({}),
78+
config: toolSearch.testing.resolveToolSearchConfig({}),
7879
logs: [],
7980
parentToolCallId: "call-stderr-error",
8081
runtime,
8182
}),
8283
).rejects.toThrow("stderr read failed");
84+
expect(spawnMock).toHaveBeenCalledOnce();
85+
expect(spawnMock.mock.calls[0]?.[2]).toMatchObject({
86+
stdio: ["ignore", "ignore", "pipe", "ipc"],
87+
});
88+
expect(spawnedChild?.kill).toHaveBeenCalledOnce();
8389
});
8490
});

src/agents/tool-search.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,7 +2111,9 @@ function runCodeModeChild(params: {
21112111
const child = spawn(process.execPath, buildCodeModeChildArgs(), {
21122112
cwd: os.tmpdir(),
21132113
env: {},
2114-
stdio: ["ignore", "pipe", "pipe", "ipc"],
2114+
// The worker returns logs/results over IPC and never writes stdout.
2115+
// Ignore it so an unused pipe cannot fill or surface unhandled errors.
2116+
stdio: ["ignore", "ignore", "pipe", "ipc"],
21152117
});
21162118
let stderrTail = "";
21172119
let settled = false;
@@ -2361,6 +2363,5 @@ export const testing = {
23612363
addClientToolsToToolSearchCatalog,
23622364
appendToolSearchCodeStderrTail,
23632365
runCodeModeChild,
2364-
ToolSearchRuntime,
23652366
};
23662367
export { testing as __testing };

0 commit comments

Comments
 (0)