177 lines
5.0 KiB
TypeScript
177 lines
5.0 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { promises as fs } from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { shell } from "../tools/impl/Shell.js";
|
|
|
|
const isWindows = process.platform === "win32";
|
|
|
|
describe("shell codex tool", () => {
|
|
let tempDir: string;
|
|
|
|
async function setupTempDir(): Promise<string> {
|
|
if (!tempDir) {
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "shell-test-"));
|
|
}
|
|
return tempDir;
|
|
}
|
|
|
|
test("executes simple command with execvp-style args", async () => {
|
|
const result = await shell({
|
|
command: ["echo", "hello", "world"],
|
|
});
|
|
|
|
expect(result.output).toBe("hello world");
|
|
expect(result.stdout).toContain("hello world");
|
|
expect(result.stderr.length).toBe(0);
|
|
});
|
|
|
|
test("executes bash -lc style command", async () => {
|
|
const result = await shell({
|
|
command: ["bash", "-lc", "echo 'hello from bash'"],
|
|
});
|
|
|
|
expect(result.output).toContain("hello from bash");
|
|
});
|
|
|
|
test("handles arguments with spaces correctly", async () => {
|
|
// This is the key test for execvp semantics - args with spaces
|
|
// should NOT be split
|
|
const result = await shell({
|
|
command: ["echo", "hello world", "foo bar"],
|
|
});
|
|
|
|
expect(result.output).toBe("hello world foo bar");
|
|
});
|
|
|
|
test.skipIf(isWindows)("respects workdir parameter", async () => {
|
|
const dir = await setupTempDir();
|
|
// Resolve symlinks (macOS /var -> /private/var)
|
|
const resolvedDir = await fs.realpath(dir);
|
|
|
|
const result = await shell({
|
|
command: ["pwd"],
|
|
workdir: dir,
|
|
});
|
|
|
|
expect(result.output).toBe(resolvedDir);
|
|
});
|
|
|
|
test.skipIf(isWindows)("captures stderr output", async () => {
|
|
const result = await shell({
|
|
command: ["bash", "-c", "echo 'error message' >&2"],
|
|
});
|
|
|
|
expect(result.stderr).toContain("error message");
|
|
});
|
|
|
|
test.skipIf(isWindows)("handles non-zero exit codes", async () => {
|
|
const result = await shell({
|
|
command: ["bash", "-c", "exit 1"],
|
|
});
|
|
|
|
// Should still resolve (not reject), but output may indicate failure
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test.skipIf(isWindows)(
|
|
"handles command with output in both stdout and stderr",
|
|
async () => {
|
|
const result = await shell({
|
|
command: ["bash", "-c", "echo 'stdout'; echo 'stderr' >&2"],
|
|
});
|
|
|
|
expect(result.stdout).toContain("stdout");
|
|
expect(result.stderr).toContain("stderr");
|
|
expect(result.output).toContain("stdout");
|
|
expect(result.output).toContain("stderr");
|
|
},
|
|
);
|
|
|
|
test("times out long-running commands", async () => {
|
|
await expect(
|
|
shell({
|
|
command: ["sleep", "10"],
|
|
timeout_ms: 100,
|
|
}),
|
|
).rejects.toThrow("timed out");
|
|
});
|
|
|
|
test("throws error for empty command array", async () => {
|
|
await expect(shell({ command: [] })).rejects.toThrow(
|
|
"command must be a non-empty array",
|
|
);
|
|
});
|
|
|
|
test("throws error for missing command", async () => {
|
|
// @ts-expect-error Testing invalid input
|
|
await expect(shell({})).rejects.toThrow();
|
|
});
|
|
|
|
test.skipIf(isWindows)("handles relative workdir", async () => {
|
|
// Set USER_CWD to a known location
|
|
const originalCwd = process.env.USER_CWD;
|
|
const dir = await setupTempDir();
|
|
process.env.USER_CWD = dir;
|
|
|
|
// Create a subdirectory
|
|
const subdir = path.join(dir, "subdir");
|
|
await fs.mkdir(subdir, { recursive: true });
|
|
// Resolve symlinks (macOS /var -> /private/var)
|
|
const resolvedSubdir = await fs.realpath(subdir);
|
|
|
|
try {
|
|
const result = await shell({
|
|
command: ["pwd"],
|
|
workdir: "subdir",
|
|
});
|
|
|
|
expect(result.output).toBe(resolvedSubdir);
|
|
} finally {
|
|
if (originalCwd !== undefined) {
|
|
process.env.USER_CWD = originalCwd;
|
|
} else {
|
|
delete process.env.USER_CWD;
|
|
}
|
|
}
|
|
});
|
|
|
|
test.skipIf(isWindows)(
|
|
"handles command that produces multi-line output",
|
|
async () => {
|
|
const result = await shell({
|
|
command: ["bash", "-c", "echo 'line1'; echo 'line2'; echo 'line3'"],
|
|
});
|
|
|
|
expect(result.stdout).toContain("line1");
|
|
expect(result.stdout).toContain("line2");
|
|
expect(result.stdout).toContain("line3");
|
|
expect(result.stdout.length).toBe(3);
|
|
},
|
|
);
|
|
|
|
test("handles special characters in arguments", async () => {
|
|
const result = await shell({
|
|
command: ["echo", "$HOME", "$(whoami)", "`date`"],
|
|
});
|
|
|
|
// Since we're using execvp-style (not shell expansion),
|
|
// these should be treated as literal strings
|
|
expect(result.output).toContain("$HOME");
|
|
expect(result.output).toContain("$(whoami)");
|
|
expect(result.output).toContain("`date`");
|
|
});
|
|
|
|
test.skipIf(isWindows)("handles file operations with bash -lc", async () => {
|
|
const dir = await setupTempDir();
|
|
const testFile = path.join(dir, "test-output.txt");
|
|
|
|
await shell({
|
|
command: ["bash", "-lc", `echo 'test content' > "${testFile}"`],
|
|
});
|
|
|
|
const content = await fs.readFile(testFile, "utf8");
|
|
expect(content.trim()).toBe("test content");
|
|
});
|
|
});
|