44424fdeb1
- Added session-based shell execution with persistent shell instances and session management via sessionKey parameter. - Added support for shell snapshots to initialize bash sessions with saved state via snapshotPath parameter. - Added sessionEnv option to set environment variables once per session, separate from command-specific env variables. - Added process group management functions (process_group_id and kill_process_group) for better process lifecycle control on Linux, macOS, and Windows. - Changed env option to apply variables for the current command only, with session-level variables now managed via sessionEnv. - Refactored shell execution to use async output queuing with enqueueChunk for proper chunk sequencing and improved timeout handling with process group termination.
98 lines
2.9 KiB
TypeScript
98 lines
2.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { _resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
|
import { executeBash } from "@oh-my-pi/pi-coding-agent/exec/bash-executor";
|
|
|
|
function makeTempDir(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), "omp-bash-exec-"));
|
|
}
|
|
|
|
describe("executeBash", () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(async () => {
|
|
tempDir = makeTempDir();
|
|
_resetSettingsForTest();
|
|
await Settings.init({ inMemory: true, cwd: tempDir });
|
|
});
|
|
|
|
afterEach(() => {
|
|
_resetSettingsForTest();
|
|
if (fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true });
|
|
}
|
|
});
|
|
|
|
it("returns non-zero exit codes without cancellation", async () => {
|
|
const result = await executeBash("exit 7", { cwd: tempDir, timeout: 5000 });
|
|
expect(result.exitCode).toBe(7);
|
|
expect(result.cancelled).toBe(false);
|
|
});
|
|
|
|
it("honors cwd", async () => {
|
|
const result = await executeBash("pwd", { cwd: tempDir, timeout: 5000 });
|
|
expect(result.output.trim()).toBe(tempDir);
|
|
});
|
|
|
|
it("passes env vars", async () => {
|
|
const result = await executeBash("echo $OMP_TEST_ENV", {
|
|
cwd: tempDir,
|
|
timeout: 5000,
|
|
env: { OMP_TEST_ENV: "hello" },
|
|
});
|
|
expect(result.output.trim()).toBe("hello");
|
|
});
|
|
|
|
it("times out commands", async () => {
|
|
if (process.platform === "win32") {
|
|
return;
|
|
}
|
|
const result = await executeBash("sleep 10", { cwd: tempDir, timeout: 50 });
|
|
expect(result.cancelled).toBe(true);
|
|
expect(result.output).toContain("timed out");
|
|
});
|
|
|
|
it("aborts commands", async () => {
|
|
if (process.platform === "win32") {
|
|
return;
|
|
}
|
|
const controller = new AbortController();
|
|
const promise = executeBash("sleep 10", {
|
|
cwd: tempDir,
|
|
timeout: 5000,
|
|
signal: controller.signal,
|
|
});
|
|
await Bun.sleep(50);
|
|
controller.abort();
|
|
const result = await promise;
|
|
expect(result.cancelled).toBe(true);
|
|
expect(result.output).toContain("Command cancelled");
|
|
});
|
|
|
|
it("streams output chunks", async () => {
|
|
const chunks: string[] = [];
|
|
const result = await executeBash("i=1; while [ $i -le 20 ]; do echo line$i; i=$((i+1)); done", {
|
|
cwd: tempDir,
|
|
timeout: 5000,
|
|
onChunk: chunk => chunks.push(chunk),
|
|
});
|
|
const combined = chunks.join("");
|
|
expect(chunks.length).toBeGreaterThan(0);
|
|
expect(combined).toContain("line1");
|
|
expect(combined).toContain("line20");
|
|
expect(result.output).toContain("line1");
|
|
expect(result.output).toContain("line20");
|
|
});
|
|
|
|
it("does not allow exec to replace the host", async () => {
|
|
const result = await executeBash("exec echo hi", { cwd: tempDir, timeout: 5000 });
|
|
expect(result.cancelled).toBe(false);
|
|
expect(result.exitCode).not.toBeUndefined();
|
|
if (!result.output.includes("hi")) {
|
|
expect(result.output.toLowerCase()).toContain("exec");
|
|
}
|
|
});
|
|
});
|