Skip to content

Commit d4bf73b

Browse files
author
zhibisora
committed
Replace native spawn with cross-spawn for improved cross-platform compatibility
1 parent 0fa2397 commit d4bf73b

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"dependencies": {
4949
"content-type": "^1.0.5",
5050
"cors": "^2.8.5",
51+
"cross-spawn": "^7.0.3",
5152
"eventsource": "^3.0.2",
5253
"express": "^5.0.1",
5354
"express-rate-limit": "^7.5.0",

src/client/cross-spawn.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { StdioClientTransport } from "./stdio.js";
2+
import spawn from "cross-spawn";
3+
4+
// mock cross-spawn
5+
jest.mock("cross-spawn");
6+
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
7+
8+
describe("StdioClientTransport using cross-spawn", () => {
9+
beforeEach(() => {
10+
// mock cross-spawn's return value
11+
mockSpawn.mockImplementation(() => {
12+
const mockProcess = {
13+
on: jest.fn((event: string, callback: Function) => {
14+
if (event === "spawn") {
15+
callback();
16+
}
17+
return mockProcess;
18+
}),
19+
stdin: {
20+
on: jest.fn(),
21+
write: jest.fn().mockReturnValue(true)
22+
},
23+
stdout: {
24+
on: jest.fn()
25+
},
26+
stderr: null
27+
};
28+
return mockProcess as any;
29+
});
30+
});
31+
32+
afterEach(() => {
33+
jest.clearAllMocks();
34+
});
35+
36+
test("should call cross-spawn correctly", async () => {
37+
const transport = new StdioClientTransport({
38+
command: "test-command",
39+
args: ["arg1", "arg2"]
40+
});
41+
42+
await transport.start();
43+
44+
// verify spawn is called correctly
45+
expect(mockSpawn).toHaveBeenCalledWith(
46+
"test-command",
47+
["arg1", "arg2"],
48+
expect.objectContaining({
49+
shell: false
50+
})
51+
);
52+
});
53+
54+
test("should pass environment variables correctly", async () => {
55+
const customEnv = { TEST_VAR: "test-value" };
56+
const transport = new StdioClientTransport({
57+
command: "test-command",
58+
env: customEnv
59+
});
60+
61+
await transport.start();
62+
63+
// verify environment variables are passed correctly
64+
expect(mockSpawn).toHaveBeenCalledWith(
65+
"test-command",
66+
[],
67+
expect.objectContaining({
68+
env: customEnv
69+
})
70+
);
71+
});
72+
73+
test("should send messages correctly", async () => {
74+
const transport = new StdioClientTransport({
75+
command: "test-command"
76+
});
77+
78+
// get the mock process object
79+
const mockProcess = {
80+
on: jest.fn((event, callback) => {
81+
if (event === "spawn") {
82+
callback();
83+
}
84+
return mockProcess;
85+
}),
86+
stdin: {
87+
on: jest.fn(),
88+
write: jest.fn().mockReturnValue(true),
89+
once: jest.fn()
90+
},
91+
stdout: {
92+
on: jest.fn()
93+
},
94+
stderr: null
95+
};
96+
97+
mockSpawn.mockReturnValue(mockProcess as any);
98+
99+
await transport.start();
100+
101+
const message = {
102+
jsonrpc: "2.0",
103+
id: "test-id",
104+
method: "test-method"
105+
};
106+
107+
await transport.send(message);
108+
109+
// verify message is sent correctly
110+
expect(mockProcess.stdin.write).toHaveBeenCalled();
111+
});
112+
});

src/client/stdio.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ChildProcess, IOType, spawn } from "node:child_process";
1+
import { ChildProcess, IOType } from "node:child_process";
2+
import spawn from "cross-spawn";
23
import process from "node:process";
34
import { Stream } from "node:stream";
45
import { ReadBuffer, serializeMessage } from "../shared/stdio.js";

0 commit comments

Comments
 (0)