Skip to content

Commit 475961d

Browse files
committed
add sync shell command support, remove non-async for safety.
1 parent 17c938e commit 475961d

File tree

8 files changed

+661
-2
lines changed

8 files changed

+661
-2
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"chalk": "^5",
5353
"dotenv": "^16",
5454
"source-map-support": "^0.5",
55+
"uuid": "^11.0.5",
5556
"yargs": "^17",
5657
"yargs-file-commands": "^0.0.17",
5758
"zod": "^3",
@@ -61,6 +62,7 @@
6162
"@changesets/cli": "^2",
6263
"@eslint/js": "^9",
6364
"@types/node": "^18",
65+
"@types/uuid": "^10.0.0",
6466
"@types/yargs": "^17",
6567
"@typescript-eslint/eslint-plugin": "^8.23.0",
6668
"@typescript-eslint/parser": "^8.23.0",

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/getTools.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import { subAgentTool } from "../tools/interaction/subAgent.js";
2-
import { shellExecuteTool } from "../tools/system/shellExecute.js";
32
import { readFileTool } from "../tools/io/readFile.js";
43
import { userPromptTool } from "../tools/interaction/userPrompt.js";
54
import { sequenceCompleteTool } from "../tools/system/sequenceComplete.js";
65
import { fetchTool } from "../tools/io/fetch.js";
76
import { Tool } from "../core/types.js";
87
import { updateFileTool } from "./io/updateFile.js";
8+
import { shellStartTool } from "./system/shellStart.js";
9+
import { shellMessageTool } from "./system/shellMessage.js";
910

1011
export async function getTools(): Promise<Tool[]> {
1112
return [
1213
subAgentTool,
1314
readFileTool,
1415
updateFileTool,
15-
shellExecuteTool,
16+
//shellExecuteTool, - remove for now.
1617
userPromptTool,
1718
sequenceCompleteTool,
1819
fetchTool,
20+
shellStartTool,
21+
shellMessageTool,
1922
] as Tool[];
2023
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
2+
import { shellStartTool } from "./shellStart.js";
3+
import { MockLogger } from "../../utils/mockLogger.js";
4+
import { shellMessageTool } from "./shellMessage.js";
5+
6+
// eslint-disable-next-line max-lines-per-function
7+
describe("shellMessageTool", () => {
8+
const mockLogger = new MockLogger();
9+
10+
let testInstanceId = "";
11+
12+
beforeEach(() => {
13+
global.startedProcesses.clear();
14+
});
15+
16+
afterEach(() => {
17+
for (const process of global.startedProcesses.values()) {
18+
process.kill();
19+
}
20+
global.startedProcesses.clear();
21+
});
22+
23+
it("should interact with a running process", async () => {
24+
// Start a test process
25+
const startResult = await shellStartTool.execute(
26+
{
27+
command: "cat", // cat will echo back input
28+
description: "Test interactive process",
29+
},
30+
{ logger: mockLogger }
31+
);
32+
33+
testInstanceId = startResult.instanceId;
34+
35+
// Send input and get response
36+
const result = await shellMessageTool.execute(
37+
{
38+
instanceId: testInstanceId,
39+
stdin: "hello world",
40+
description: "Test interaction",
41+
},
42+
{ logger: mockLogger }
43+
);
44+
45+
expect(result.stdout).toBe("hello world");
46+
expect(result.stderr).toBe("");
47+
expect(result.completed).toBe(false);
48+
});
49+
50+
it("should handle nonexistent process", async () => {
51+
const result = await shellMessageTool.execute(
52+
{
53+
instanceId: "nonexistent-id",
54+
description: "Test invalid process",
55+
},
56+
{ logger: mockLogger }
57+
);
58+
59+
expect(result.error).toBeDefined();
60+
expect(result.completed).toBe(false);
61+
});
62+
63+
it("should handle process completion", async () => {
64+
// Start a quick process
65+
const startResult = await shellStartTool.execute(
66+
{
67+
command: 'echo "test" && exit',
68+
description: "Test completion",
69+
},
70+
{ logger: mockLogger }
71+
);
72+
73+
// Wait a moment for process to complete
74+
await new Promise((resolve) => setTimeout(resolve, 100));
75+
76+
const result = await shellMessageTool.execute(
77+
{
78+
instanceId: startResult.instanceId,
79+
description: "Check completion",
80+
},
81+
{ logger: mockLogger }
82+
);
83+
84+
expect(result.completed).toBe(true);
85+
// Process should still be in startedProcesses even after completion
86+
expect(global.startedProcesses.has(startResult.instanceId)).toBe(true);
87+
});
88+
89+
it("should handle SIGTERM signal correctly", async () => {
90+
// Start a long-running process
91+
const startResult = await shellStartTool.execute(
92+
{
93+
command: "sleep 10",
94+
description: "Test SIGTERM handling",
95+
},
96+
{ logger: mockLogger }
97+
);
98+
99+
const result = await shellMessageTool.execute(
100+
{
101+
instanceId: startResult.instanceId,
102+
signal: "SIGTERM",
103+
description: "Send SIGTERM",
104+
},
105+
{ logger: mockLogger }
106+
);
107+
expect(result.signaled).toBe(true);
108+
109+
await new Promise((resolve) => setTimeout(resolve, 100));
110+
111+
const result2 = await shellMessageTool.execute(
112+
{
113+
instanceId: startResult.instanceId,
114+
description: "Check on status",
115+
},
116+
{ logger: mockLogger }
117+
);
118+
119+
expect(result2.completed).toBe(true);
120+
expect(result2.error).toBeUndefined();
121+
});
122+
123+
it("should handle signals on terminated process gracefully", async () => {
124+
// Start a process
125+
const startResult = await shellStartTool.execute(
126+
{
127+
command: "sleep 1",
128+
description: "Test signal handling on terminated process",
129+
},
130+
{ logger: mockLogger }
131+
);
132+
133+
// Wait for process to complete
134+
await new Promise((resolve) => setTimeout(resolve, 1500));
135+
136+
// Try to send signal to completed process
137+
const result = await shellMessageTool.execute(
138+
{
139+
instanceId: startResult.instanceId,
140+
signal: "SIGTERM",
141+
description: "Send signal to terminated process",
142+
},
143+
{ logger: mockLogger }
144+
);
145+
146+
expect(result.error).toBeDefined();
147+
expect(result.signaled).toBe(false);
148+
expect(result.completed).toBe(true);
149+
});
150+
151+
it("should verify signaled flag after process termination", async () => {
152+
// Start a process
153+
const startResult = await shellStartTool.execute(
154+
{
155+
command: "sleep 5",
156+
description: "Test signal flag verification",
157+
},
158+
{ logger: mockLogger }
159+
);
160+
161+
// Send SIGTERM
162+
await shellMessageTool.execute(
163+
{
164+
instanceId: startResult.instanceId,
165+
signal: "SIGTERM",
166+
description: "Send SIGTERM",
167+
},
168+
{ logger: mockLogger }
169+
);
170+
171+
await new Promise((resolve) => setTimeout(resolve, 300));
172+
173+
// Check process state after signal
174+
const checkResult = await shellMessageTool.execute(
175+
{
176+
instanceId: startResult.instanceId,
177+
description: "Check signal state",
178+
},
179+
{ logger: mockLogger }
180+
);
181+
182+
console.log({ checkResult });
183+
184+
expect(checkResult.signaled).toBe(true);
185+
expect(checkResult.completed).toBe(true);
186+
expect(global.startedProcesses.has(startResult.instanceId)).toBe(true);
187+
});
188+
});

0 commit comments

Comments
 (0)