Files
openclaw/src/agents/bash-tools.test.ts

221 lines
7.0 KiB
TypeScript
Raw Normal View History

2025-12-25 00:25:11 +00:00
import { beforeEach, describe, expect, it } from "vitest";
2025-12-25 20:20:38 +00:00
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
2025-12-25 17:58:19 +00:00
import {
bashTool,
createBashTool,
createProcessTool,
processTool,
} from "./bash-tools.js";
2025-12-25 00:25:11 +00:00
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
2025-12-25 17:58:19 +00:00
async function waitForCompletion(sessionId: string) {
let status = "running";
const deadline = Date.now() + 2000;
while (Date.now() < deadline && status === "running") {
const poll = await processTool.execute("call-wait", {
action: "poll",
sessionId,
});
status = (poll.details as { status: string }).status;
if (status === "running") {
await sleep(20);
}
}
return status;
}
2025-12-25 00:25:11 +00:00
beforeEach(() => {
resetProcessRegistryForTests();
});
describe("bash tool backgrounding", () => {
it("backgrounds after yield and can be polled", async () => {
const result = await bashTool.execute("call1", {
command: "node -e \"setTimeout(() => { console.log('done') }, 50)\"",
yieldMs: 10,
});
expect(result.details.status).toBe("running");
const sessionId = (result.details as { sessionId: string }).sessionId;
let status = "running";
let output = "";
const deadline = Date.now() + 2000;
while (Date.now() < deadline && status === "running") {
const poll = await processTool.execute("call2", {
action: "poll",
sessionId,
});
status = (poll.details as { status: string }).status;
const textBlock = poll.content.find((c) => c.type === "text");
output = textBlock?.text ?? "";
if (status === "running") {
await sleep(20);
}
}
expect(status).toBe("completed");
expect(output).toContain("done");
});
it("supports explicit background", async () => {
const result = await bashTool.execute("call1", {
command: "node -e \"setTimeout(() => { console.log('later') }, 50)\"",
background: true,
});
expect(result.details.status).toBe("running");
const sessionId = (result.details as { sessionId: string }).sessionId;
const list = await processTool.execute("call2", { action: "list" });
2025-12-25 03:29:36 +01:00
const sessions = (
list.details as { sessions: Array<{ sessionId: string }> }
).sessions;
2025-12-25 00:25:11 +00:00
expect(sessions.some((s) => s.sessionId === sessionId)).toBe(true);
});
2025-12-25 17:58:19 +00:00
it("derives a session name from the command", async () => {
const result = await bashTool.execute("call1", {
command: "echo hello",
background: true,
});
const sessionId = (result.details as { sessionId: string }).sessionId;
await sleep(25);
const list = await processTool.execute("call2", { action: "list" });
const sessions = (
list.details as { sessions: Array<{ sessionId: string; name?: string }> }
).sessions;
const entry = sessions.find((s) => s.sessionId === sessionId);
expect(entry?.name).toBe("echo hello");
});
it("uses default timeout when timeout is omitted", async () => {
const customBash = createBashTool({ timeoutSec: 1, backgroundMs: 10 });
const customProcess = createProcessTool();
const result = await customBash.execute("call1", {
2025-12-25 20:20:38 +00:00
command: 'node -e "setInterval(() => {}, 1000)"',
2025-12-25 17:58:19 +00:00
background: true,
});
const sessionId = (result.details as { sessionId: string }).sessionId;
let status = "running";
const deadline = Date.now() + 5000;
while (Date.now() < deadline && status === "running") {
const poll = await customProcess.execute("call2", {
action: "poll",
sessionId,
});
status = (poll.details as { status: string }).status;
if (status === "running") {
await sleep(50);
}
}
expect(status).toBe("failed");
});
2026-01-04 05:15:42 +00:00
it("rejects elevated requests when not allowed", async () => {
const customBash = createBashTool({
elevated: { enabled: true, allowed: false, defaultLevel: "off" },
});
await expect(
customBash.execute("call1", {
command: "echo hi",
elevated: true,
}),
).rejects.toThrow("elevated is not available right now.");
});
it("does not default to elevated when not allowed", async () => {
const customBash = createBashTool({
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
backgroundMs: 1000,
timeoutSec: 5,
});
const result = await customBash.execute("call1", {
command: "echo hi",
});
const text = result.content.find((c) => c.type === "text")?.text ?? "";
expect(text).toContain("hi");
});
2025-12-25 17:58:19 +00:00
it("logs line-based slices and defaults to last lines", async () => {
const result = await bashTool.execute("call1", {
command:
"node -e \"console.log('one'); console.log('two'); console.log('three');\"",
background: true,
});
const sessionId = (result.details as { sessionId: string }).sessionId;
const status = await waitForCompletion(sessionId);
const log = await processTool.execute("call3", {
action: "log",
sessionId,
limit: 2,
});
const textBlock = log.content.find((c) => c.type === "text");
expect(textBlock?.text).toBe("two\nthree");
expect((log.details as { totalLines?: number }).totalLines).toBe(3);
expect(status).toBe("completed");
});
it("supports line offsets for log slices", async () => {
const result = await bashTool.execute("call1", {
command:
"node -e \"console.log('alpha'); console.log('beta'); console.log('gamma');\"",
background: true,
});
const sessionId = (result.details as { sessionId: string }).sessionId;
await waitForCompletion(sessionId);
const log = await processTool.execute("call2", {
action: "log",
sessionId,
offset: 1,
limit: 1,
});
const textBlock = log.content.find((c) => c.type === "text");
expect(textBlock?.text).toBe("beta");
});
2026-01-07 23:35:04 +01:00
it("scopes process sessions by scopeKey", async () => {
const bashA = createBashTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
const processA = createProcessTool({ scopeKey: "agent:alpha" });
const bashB = createBashTool({ backgroundMs: 10, scopeKey: "agent:beta" });
const processB = createProcessTool({ scopeKey: "agent:beta" });
const resultA = await bashA.execute("call1", {
command: 'node -e "setTimeout(() => {}, 50)"',
background: true,
});
const resultB = await bashB.execute("call2", {
command: 'node -e "setTimeout(() => {}, 50)"',
background: true,
});
const sessionA = (resultA.details as { sessionId: string }).sessionId;
const sessionB = (resultB.details as { sessionId: string }).sessionId;
const listA = await processA.execute("call3", { action: "list" });
const sessionsA = (
listA.details as { sessions: Array<{ sessionId: string }> }
).sessions;
expect(sessionsA.some((s) => s.sessionId === sessionA)).toBe(true);
expect(sessionsA.some((s) => s.sessionId === sessionB)).toBe(false);
const pollB = await processB.execute("call4", {
action: "poll",
sessionId: sessionA,
});
expect(pollB.details.status).toBe("failed");
});
2025-12-25 00:25:11 +00:00
});