Skip to content

Commit 1d94261

Browse files
committed
Add unit tests for bash and create tools: validate execution, error handling, and output
1 parent b36d76d commit 1d94261

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
import bash from "@/agent/tools/definitions/bash";
3+
import { ToolError } from "@/agent/errors";
4+
import { ChildProcess, spawn } from "child_process";
5+
import { EventEmitter } from "events";
6+
7+
vi.mock("child_process", () => ({
8+
spawn: vi.fn(),
9+
}));
10+
11+
describe("bash tool", () => {
12+
let spawnMock: vi.MockedFunction<typeof spawn>;
13+
let childProcessMock: ChildProcess;
14+
let stdoutMock: EventEmitter;
15+
let stderrMock: EventEmitter;
16+
17+
beforeEach(() => {
18+
childProcessMock = new EventEmitter() as ChildProcess;
19+
stdoutMock = new EventEmitter();
20+
stderrMock = new EventEmitter();
21+
// @ts-expect-error - Mocking properties for testing
22+
childProcessMock.stdout = stdoutMock;
23+
// @ts-expect-error - Mocking properties for testing
24+
childProcessMock.stderr = stderrMock;
25+
26+
spawnMock = vi.mocked(spawn).mockReturnValue(childProcessMock);
27+
});
28+
29+
afterEach(() => {
30+
vi.resetAllMocks();
31+
});
32+
33+
it("should resolve with output on successful execution and use default timeout", async () => {
34+
const args = bash.schema.shape.arguments.parse({ cmd: "ls -l" });
35+
const promise = bash.implementation(args);
36+
37+
stdoutMock.emit("data", "file1.txt");
38+
stdoutMock.emit("data", "file2.txt");
39+
childProcessMock.emit("close", 0);
40+
41+
await expect(promise).resolves.toBe("file1.txtfile2.txt");
42+
expect(spawnMock).toHaveBeenCalledWith("ls -l", {
43+
cwd: process.cwd(),
44+
shell: "/bin/bash",
45+
stdio: ["ignore", "pipe", "pipe"],
46+
timeout: 30000,
47+
});
48+
});
49+
50+
it("should use provided timeout", async () => {
51+
const args = bash.schema.shape.arguments.parse({ cmd: "sleep 5", timeout: 5000 });
52+
const promise = bash.implementation(args);
53+
54+
childProcessMock.emit("close", 0);
55+
56+
await expect(promise).resolves.toBe("");
57+
expect(spawnMock).toHaveBeenCalledWith("sleep 5", {
58+
cwd: process.cwd(),
59+
shell: "/bin/bash",
60+
stdio: ["ignore", "pipe", "pipe"],
61+
timeout: 5000,
62+
});
63+
});
64+
65+
it("should reject with ToolError on non-zero exit code", async () => {
66+
const args = bash.schema.shape.arguments.parse({ cmd: "ls non_existent_dir" });
67+
const promise = bash.implementation(args);
68+
69+
stderrMock.emit("data", "ls: cannot access 'non_existent_dir': No such file or directory");
70+
childProcessMock.emit("close", 2);
71+
72+
await expect(promise).rejects.toThrow(ToolError);
73+
await expect(promise).rejects.toThrow(
74+
"Command exited with code: 2\nOutput:\nls: cannot access 'non_existent_dir': No such file or directory",
75+
);
76+
});
77+
78+
it("should reject with ToolError on command start error", async () => {
79+
const error = new Error("spawn ENOENT");
80+
const args = bash.schema.shape.arguments.parse({ cmd: "invalid_command" });
81+
const promise = bash.implementation(args);
82+
83+
childProcessMock.emit("error", error);
84+
85+
await expect(promise).rejects.toThrow(ToolError);
86+
await expect(promise).rejects.toThrow(`Command failed to start: ${error.message}`);
87+
});
88+
89+
it("should handle stderr output correctly", async () => {
90+
const args = bash.schema.shape.arguments.parse({ cmd: "some_command" });
91+
const promise = bash.implementation(args);
92+
93+
stderrMock.emit("data", "some error");
94+
childProcessMock.emit("close", 1);
95+
96+
await expect(promise).rejects.toThrow("Command exited with code: 1\nOutput:\nsome error");
97+
});
98+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import create from "@/agent/tools/definitions/create";
3+
import { fileTracker, FileExistsError } from "@/agent/file-tracker";
4+
5+
vi.mock("@/agent/file-tracker", () => ({
6+
fileTracker: {
7+
assertCanCreate: vi.fn(),
8+
write: vi.fn(),
9+
},
10+
FileExistsError: class FileExistsError extends Error {
11+
constructor(message: string) {
12+
super(message);
13+
this.name = "FileExistsError";
14+
}
15+
},
16+
}));
17+
18+
describe("create tool", () => {
19+
const mockArgs = {
20+
path: "test.txt",
21+
content: "hello world",
22+
};
23+
24+
beforeEach(() => {
25+
vi.resetAllMocks();
26+
});
27+
28+
it("should return a success message on successful creation", async () => {
29+
vi.mocked(fileTracker.assertCanCreate).mockResolvedValue(undefined);
30+
vi.mocked(fileTracker.write).mockResolvedValue(undefined);
31+
32+
const result = await create.implementation(mockArgs);
33+
34+
expect(result).toBe(`Successfully created file at ${mockArgs.path}`);
35+
expect(fileTracker.assertCanCreate).toHaveBeenCalledWith(mockArgs.path);
36+
expect(fileTracker.write).toHaveBeenCalledWith(mockArgs.path, mockArgs.content);
37+
});
38+
39+
it("should return an error message if the file already exists", async () => {
40+
const error = new FileExistsError(`File already exists at path: ${mockArgs.path}`);
41+
vi.mocked(fileTracker.assertCanCreate).mockRejectedValue(error);
42+
43+
const result = await create.implementation(mockArgs);
44+
45+
expect(result).toBe(`Error: ${error.message}`);
46+
expect(fileTracker.write).not.toHaveBeenCalled();
47+
});
48+
49+
it("should return a generic error message for other errors", async () => {
50+
const error = new Error("Something went wrong");
51+
vi.mocked(fileTracker.assertCanCreate).mockRejectedValue(error);
52+
53+
const result = await create.implementation(mockArgs);
54+
55+
expect(result).toBe(`Error creating file: ${error.message}`);
56+
expect(fileTracker.write).not.toHaveBeenCalled();
57+
});
58+
59+
it("should return a message for unknown errors", async () => {
60+
vi.mocked(fileTracker.assertCanCreate).mockRejectedValue("an unknown error");
61+
62+
const result = await create.implementation(mockArgs);
63+
64+
expect(result).toBe("An unknown error occurred while creating the file.");
65+
expect(fileTracker.write).not.toHaveBeenCalled();
66+
});
67+
});

0 commit comments

Comments
 (0)