Files
oh-my-pi/packages/coding-agent/test/bash-executor.test.ts
T
can1357 44424fdeb1 feat(shell): added session-based shell execution with persistent instances and process group management
- 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.
2026-02-01 15:01:43 +01:00

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");
}
});
});