import fs from "node:fs/promises" ;
import path from "node:path" ;
import { describe, expect, it } from "vitest" ;
import {
createSandbox,
createSandboxFsBridge,
createSeededSandboxFsBridge,
getScriptsFromCalls,
installFsBridgeTestHarness,
mockedExecDockerRaw,
mockedOpenBoundaryFile,
withTempDir,
} from "./fs-bridge.test-helpers.js" ;
describe("sandbox fs bridge shell compatibility" , () => {
installFsBridgeTestHarness();
it("uses POSIX-safe shell prologue in all bridge commands" , async () => {
await withTempDir("openclaw-fs-bridge-shell-" , async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace" );
await fs.mkdir(workspaceDir, { recursive: true });
await fs.writeFile(path.join(workspaceDir, "a.txt" ), "hello" );
await fs.writeFile(path.join(workspaceDir, "b.txt" ), "bye" );
const bridge = createSandboxFsBridge({
sandbox: createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
}),
});
await bridge.readFile({ filePath: "a.txt" });
await bridge.writeFile({ filePath: "b.txt" , data: "hello" });
await bridge.mkdirp({ filePath: "nested" });
await bridge.remove({ filePath: "b.txt" });
await bridge.rename({ from: "a.txt" , to: "c.txt" });
await bridge.stat({ filePath: "c.txt" });
expect(mockedExecDockerRaw).toHaveBeenCalled();
const scripts = getScriptsFromCalls();
const executables = mockedExecDockerRaw.mock.calls.map(([args]) => args[3 ] ?? "" );
expect(executables.every((shell) => shell === "sh" )).toBe(true );
expect(scripts.every((script) => /set -eu[;\n]/.test(script))).toBe(true );
expect(scripts.some((script) => script.includes("pipefail" ))).toBe(false );
});
});
it("path canonicalization recheck script is valid POSIX sh" , async () => {
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
await bridge.writeFile({ filePath: "b.txt" , data: "hello" });
const scripts = getScriptsFromCalls();
const canonicalScript = scripts.find((script) => script.includes("allow_final" ));
expect(canonicalScript).toBeDefined();
expect(canonicalScript).not.toMatch(/\bdo;/);
expect(canonicalScript).toMatch(/\bdo\n\s*parent=/);
});
it("reads inbound media-style filenames with triple-dash ids" , async () => {
await withTempDir("openclaw-fs-bridge-read-" , async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace" );
const inboundPath = "media/inbound/file_1095---f00a04a2-99a0-4d98-99b0-dfe61c5a4198.ogg" ;
await fs.mkdir(path.join(workspaceDir, "media" , "inbound" ), { recursive: true });
await fs.writeFile(path.join(workspaceDir, inboundPath), "voice" );
const bridge = createSandboxFsBridge({
sandbox: createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
}),
});
await expect(bridge.readFile({ filePath: inboundPath })).resolves.toEqual(
Buffer.from("voice" ),
);
expect(mockedExecDockerRaw).not.toHaveBeenCalled();
});
});
it("resolves dash-leading basenames into absolute container paths" , async () => {
await withTempDir("openclaw-fs-bridge-read-" , async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace" );
await fs.mkdir(workspaceDir, { recursive: true });
await fs.writeFile(path.join(workspaceDir, "--leading.txt" ), "dash" );
const bridge = createSandboxFsBridge({
sandbox: createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
}),
});
await expect(bridge.readFile({ filePath: "--leading.txt" })).resolves.toEqual(
Buffer.from("dash" ),
);
expect(mockedExecDockerRaw).not.toHaveBeenCalled();
});
});
it("resolves bind-mounted absolute container paths for reads" , async () => {
await withTempDir("openclaw-fs-bridge-bind-read-" , async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace" );
const bindRoot = path.join(stateDir, "workspace-two" );
await fs.mkdir(workspaceDir, { recursive: true });
await fs.mkdir(bindRoot, { recursive: true });
await fs.writeFile(path.join(bindRoot, "README.md" ), "bind-read" );
const sandbox = createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
docker: {
...createSandbox().docker,
binds: [`${bindRoot}:/workspace-two:ro`],
},
});
const bridge = createSandboxFsBridge({ sandbox });
await expect(bridge.readFile({ filePath: "/workspace-two/README.md" })).resolves.toEqual(
Buffer.from("bind-read" ),
);
expect(mockedExecDockerRaw).not.toHaveBeenCalled();
});
});
it("writes via temp file + atomic rename (never direct truncation)" , async () => {
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
await bridge.writeFile({ filePath: "b.txt" , data: "hello" });
const scripts = getScriptsFromCalls();
expect(scripts.some((script) => script.includes("python3 - \" $@\" <<'PY'" ))).toBe(false );
expect(
scripts.some((script) => script.includes('exec "$python_cmd" -c "$python_script" "$@"' )),
).toBe(true );
expect(scripts.some((script) => script.includes('cat >"$1"' ))).toBe(false );
expect(scripts.some((script) => script.includes('cat >"$tmp"' ))).toBe(false );
expect(scripts.some((script) => script.includes("os.replace(" ))).toBe(true );
});
it("routes mkdirp, remove, and rename through the pinned mutation helper" , async () => {
await withTempDir("openclaw-fs-bridge-shell-write-" , async (stateDir) => {
const { bridge } = await createSeededSandboxFsBridge(stateDir, {
rootFileName: "a.txt" ,
});
await bridge.mkdirp({ filePath: "nested" });
await bridge.remove({ filePath: "nested/file.txt" });
await bridge.rename({ from: "a.txt" , to: "nested/b.txt" });
const scripts = getScriptsFromCalls();
expect(scripts.filter((script) => script.includes("operation = sys.argv[1]" )).length).toBe(3 );
expect(scripts.some((script) => script.includes('mkdir -p -- "$2"' ))).toBe(false );
expect(scripts.some((script) => script.includes('rm -f -- "$2"' ))).toBe(false );
expect(scripts.some((script) => script.includes('mv -- "$3" "$2/$4"' ))).toBe(false );
});
});
it("re-validates target before the pinned write helper runs" , async () => {
mockedOpenBoundaryFile
.mockImplementationOnce(async () => ({ ok: false , reason: "path" }))
.mockImplementationOnce(async () => ({
ok: false ,
reason: "validation" ,
error: new Error("Hardlinked path is not allowed" ),
}));
const bridge = createSandboxFsBridge({ sandbox: createSandbox() });
await expect(bridge.writeFile({ filePath: "b.txt" , data: "hello" })).rejects.toThrow(
/hardlinked path/i,
);
const scripts = getScriptsFromCalls();
expect(scripts.some((script) => script.includes("os.replace(" ))).toBe(false );
});
});
Messung V0.5 in Prozent C=96 H=98 G=96
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland