Files
clawdbot/src/agents/pi-tools.workspace-paths.test.ts
Peter Steinberger c379191f80 chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
2026-01-14 15:02:19 +00:00

210 lines
7.5 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { createClawdbotCodingTools } from "./pi-tools.js";
async function withTempDir<T>(prefix: string, fn: (dir: string) => Promise<T>) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
try {
return await fn(dir);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}
function getTextContent(result?: { content?: Array<{ type: string; text?: string }> }) {
const textBlock = result?.content?.find((block) => block.type === "text");
return textBlock?.text ?? "";
}
describe("workspace path resolution", () => {
it("reads relative paths against workspaceDir even after cwd changes", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
await withTempDir("clawdbot-cwd-", async (otherDir) => {
const prevCwd = process.cwd();
const testFile = "read.txt";
const contents = "workspace read ok";
await fs.writeFile(path.join(workspaceDir, testFile), contents, "utf8");
process.chdir(otherDir);
try {
const tools = createClawdbotCodingTools({ workspaceDir });
const readTool = tools.find((tool) => tool.name === "read");
expect(readTool).toBeDefined();
const result = await readTool?.execute("ws-read", { path: testFile });
expect(getTextContent(result)).toContain(contents);
} finally {
process.chdir(prevCwd);
}
});
});
});
it("writes relative paths against workspaceDir even after cwd changes", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
await withTempDir("clawdbot-cwd-", async (otherDir) => {
const prevCwd = process.cwd();
const testFile = "write.txt";
const contents = "workspace write ok";
process.chdir(otherDir);
try {
const tools = createClawdbotCodingTools({ workspaceDir });
const writeTool = tools.find((tool) => tool.name === "write");
expect(writeTool).toBeDefined();
await writeTool?.execute("ws-write", {
path: testFile,
content: contents,
});
const written = await fs.readFile(path.join(workspaceDir, testFile), "utf8");
expect(written).toBe(contents);
} finally {
process.chdir(prevCwd);
}
});
});
});
it("edits relative paths against workspaceDir even after cwd changes", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
await withTempDir("clawdbot-cwd-", async (otherDir) => {
const prevCwd = process.cwd();
const testFile = "edit.txt";
await fs.writeFile(path.join(workspaceDir, testFile), "hello world", "utf8");
process.chdir(otherDir);
try {
const tools = createClawdbotCodingTools({ workspaceDir });
const editTool = tools.find((tool) => tool.name === "edit");
expect(editTool).toBeDefined();
await editTool?.execute("ws-edit", {
path: testFile,
oldText: "world",
newText: "clawdbot",
});
const updated = await fs.readFile(path.join(workspaceDir, testFile), "utf8");
expect(updated).toBe("hello clawdbot");
} finally {
process.chdir(prevCwd);
}
});
});
});
it("defaults exec cwd to workspaceDir when workdir is omitted", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
const tools = createClawdbotCodingTools({ workspaceDir });
const execTool = tools.find((tool) => tool.name === "exec");
expect(execTool).toBeDefined();
const result = await execTool?.execute("ws-exec", {
command: "echo ok",
});
const cwd =
result?.details && typeof result.details === "object" && "cwd" in result.details
? (result.details as { cwd?: string }).cwd
: undefined;
expect(cwd).toBeTruthy();
const [resolvedOutput, resolvedWorkspace] = await Promise.all([
fs.realpath(String(cwd)),
fs.realpath(workspaceDir),
]);
expect(resolvedOutput).toBe(resolvedWorkspace);
});
});
it("lets exec workdir override the workspace default", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
await withTempDir("clawdbot-override-", async (overrideDir) => {
const tools = createClawdbotCodingTools({ workspaceDir });
const execTool = tools.find((tool) => tool.name === "exec");
expect(execTool).toBeDefined();
const result = await execTool?.execute("ws-exec-override", {
command: "echo ok",
workdir: overrideDir,
});
const cwd =
result?.details && typeof result.details === "object" && "cwd" in result.details
? (result.details as { cwd?: string }).cwd
: undefined;
expect(cwd).toBeTruthy();
const [resolvedOutput, resolvedOverride] = await Promise.all([
fs.realpath(String(cwd)),
fs.realpath(overrideDir),
]);
expect(resolvedOutput).toBe(resolvedOverride);
});
});
});
});
describe("sandboxed workspace paths", () => {
it("uses sandbox workspace for relative read/write/edit", async () => {
await withTempDir("clawdbot-sandbox-", async (sandboxDir) => {
await withTempDir("clawdbot-workspace-", async (workspaceDir) => {
const sandbox = {
enabled: true,
sessionKey: "sandbox:test",
workspaceDir: sandboxDir,
agentWorkspaceDir: workspaceDir,
workspaceAccess: "rw",
containerName: "clawdbot-sbx-test",
containerWorkdir: "/workspace",
docker: {
image: "clawdbot-sandbox:bookworm-slim",
containerPrefix: "clawdbot-sbx-",
workdir: "/workspace",
readOnlyRoot: true,
tmpfs: [],
network: "none",
user: "1000:1000",
capDrop: ["ALL"],
env: { LANG: "C.UTF-8" },
},
tools: { allow: [], deny: [] },
browserAllowHostControl: false,
};
const testFile = "sandbox.txt";
await fs.writeFile(path.join(sandboxDir, testFile), "sandbox read", "utf8");
await fs.writeFile(path.join(workspaceDir, testFile), "workspace read", "utf8");
const tools = createClawdbotCodingTools({ workspaceDir, sandbox });
const readTool = tools.find((tool) => tool.name === "read");
const writeTool = tools.find((tool) => tool.name === "write");
const editTool = tools.find((tool) => tool.name === "edit");
expect(readTool).toBeDefined();
expect(writeTool).toBeDefined();
expect(editTool).toBeDefined();
const result = await readTool?.execute("sbx-read", { path: testFile });
expect(getTextContent(result)).toContain("sandbox read");
await writeTool?.execute("sbx-write", {
path: "new.txt",
content: "sandbox write",
});
const written = await fs.readFile(path.join(sandboxDir, "new.txt"), "utf8");
expect(written).toBe("sandbox write");
await editTool?.execute("sbx-edit", {
path: "new.txt",
oldText: "write",
newText: "edit",
});
const edited = await fs.readFile(path.join(sandboxDir, "new.txt"), "utf8");
expect(edited).toBe("sandbox edit");
});
});
});
});