mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
167 lines
5.3 KiB
TypeScript
167 lines
5.3 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { beforeEach, expect, vi } from "vitest";
|
|
|
|
vi.mock("./docker.js", () => ({
|
|
execDockerRaw: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../infra/boundary-file-read.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../../infra/boundary-file-read.js")>();
|
|
return {
|
|
...actual,
|
|
openBoundaryFile: vi.fn(actual.openBoundaryFile),
|
|
};
|
|
});
|
|
|
|
import { openBoundaryFile } from "../../infra/boundary-file-read.js";
|
|
import { execDockerRaw } from "./docker.js";
|
|
import * as fsBridgeModule from "./fs-bridge.js";
|
|
import { createSandboxTestContext } from "./test-fixtures.js";
|
|
import type { SandboxContext } from "./types.js";
|
|
|
|
export const createSandboxFsBridge = fsBridgeModule.createSandboxFsBridge;
|
|
|
|
export const mockedExecDockerRaw = vi.mocked(execDockerRaw);
|
|
export const mockedOpenBoundaryFile = vi.mocked(openBoundaryFile);
|
|
const DOCKER_SCRIPT_INDEX = 5;
|
|
const DOCKER_FIRST_SCRIPT_ARG_INDEX = 7;
|
|
|
|
export function getDockerScript(args: string[]): string {
|
|
return String(args[DOCKER_SCRIPT_INDEX] ?? "");
|
|
}
|
|
|
|
export function getDockerArg(args: string[], position: number): string {
|
|
return String(args[DOCKER_FIRST_SCRIPT_ARG_INDEX + position - 1] ?? "");
|
|
}
|
|
|
|
export function getDockerPathArg(args: string[]): string {
|
|
return getDockerArg(args, 1);
|
|
}
|
|
|
|
export function getScriptsFromCalls(): string[] {
|
|
return mockedExecDockerRaw.mock.calls.map(([args]) => getDockerScript(args));
|
|
}
|
|
|
|
export function findCallByScriptFragment(fragment: string) {
|
|
return mockedExecDockerRaw.mock.calls.find(([args]) => getDockerScript(args).includes(fragment));
|
|
}
|
|
|
|
export function findCallsByScriptFragment(fragment: string) {
|
|
return mockedExecDockerRaw.mock.calls.filter(([args]) =>
|
|
getDockerScript(args).includes(fragment),
|
|
);
|
|
}
|
|
|
|
export function findCallByDockerArg(position: number, value: string) {
|
|
return mockedExecDockerRaw.mock.calls.find(([args]) => getDockerArg(args, position) === value);
|
|
}
|
|
|
|
export function dockerExecResult(stdout: string) {
|
|
return {
|
|
stdout: Buffer.from(stdout),
|
|
stderr: Buffer.alloc(0),
|
|
code: 0,
|
|
};
|
|
}
|
|
|
|
export function createSandbox(overrides?: Partial<SandboxContext>): SandboxContext {
|
|
return createSandboxTestContext({
|
|
overrides: {
|
|
containerName: "moltbot-sbx-test",
|
|
...overrides,
|
|
},
|
|
dockerOverrides: {
|
|
image: "moltbot-sandbox:bookworm-slim",
|
|
containerPrefix: "moltbot-sbx-",
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function withTempDir<T>(
|
|
prefix: string,
|
|
run: (stateDir: string) => Promise<T>,
|
|
): Promise<T> {
|
|
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
try {
|
|
return await run(stateDir);
|
|
} finally {
|
|
await fs.rm(stateDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
export function installDockerReadMock(params?: { canonicalPath?: string }) {
|
|
const canonicalPath = params?.canonicalPath;
|
|
mockedExecDockerRaw.mockImplementation(async (args) => {
|
|
const script = getDockerScript(args);
|
|
if (script.includes('readlink -f -- "$cursor"')) {
|
|
return dockerExecResult(`${canonicalPath ?? getDockerArg(args, 1)}\n`);
|
|
}
|
|
if (script.includes('stat -c "%F|%s|%Y"')) {
|
|
return dockerExecResult("regular file|1|2");
|
|
}
|
|
if (script.includes('cat -- "$1"')) {
|
|
return dockerExecResult("content");
|
|
}
|
|
if (script.includes("mktemp")) {
|
|
return dockerExecResult("/workspace/.openclaw-write-b.txt.ABC123\n");
|
|
}
|
|
return dockerExecResult("");
|
|
});
|
|
}
|
|
|
|
export async function createHostEscapeFixture(stateDir: string) {
|
|
const workspaceDir = path.join(stateDir, "workspace");
|
|
const outsideDir = path.join(stateDir, "outside");
|
|
const outsideFile = path.join(outsideDir, "secret.txt");
|
|
await fs.mkdir(workspaceDir, { recursive: true });
|
|
await fs.mkdir(outsideDir, { recursive: true });
|
|
await fs.writeFile(outsideFile, "classified");
|
|
return { workspaceDir, outsideFile };
|
|
}
|
|
|
|
export async function expectMkdirpAllowsExistingDirectory(params?: {
|
|
forceBoundaryIoFallback?: boolean;
|
|
}) {
|
|
await withTempDir("openclaw-fs-bridge-mkdirp-", async (stateDir) => {
|
|
const workspaceDir = path.join(stateDir, "workspace");
|
|
const nestedDir = path.join(workspaceDir, "memory", "kemik");
|
|
await fs.mkdir(nestedDir, { recursive: true });
|
|
|
|
if (params?.forceBoundaryIoFallback) {
|
|
mockedOpenBoundaryFile.mockImplementationOnce(async () => ({
|
|
ok: false,
|
|
reason: "io",
|
|
error: Object.assign(new Error("EISDIR"), { code: "EISDIR" }),
|
|
}));
|
|
}
|
|
|
|
const bridge = createSandboxFsBridge({
|
|
sandbox: createSandbox({
|
|
workspaceDir,
|
|
agentWorkspaceDir: workspaceDir,
|
|
}),
|
|
});
|
|
|
|
await expect(bridge.mkdirp({ filePath: "memory/kemik" })).resolves.toBeUndefined();
|
|
|
|
const mkdirCall = findCallByDockerArg(1, "mkdirp");
|
|
expect(mkdirCall).toBeDefined();
|
|
const mkdirRoot = mkdirCall ? getDockerArg(mkdirCall[0], 2) : "";
|
|
const mkdirParent = mkdirCall ? getDockerArg(mkdirCall[0], 3) : "";
|
|
const mkdirBase = mkdirCall ? getDockerArg(mkdirCall[0], 4) : "";
|
|
expect(mkdirRoot).toBe("/workspace");
|
|
expect(mkdirParent).toBe("memory");
|
|
expect(mkdirBase).toBe("kemik");
|
|
});
|
|
}
|
|
|
|
export function installFsBridgeTestHarness() {
|
|
beforeEach(() => {
|
|
mockedExecDockerRaw.mockClear();
|
|
mockedOpenBoundaryFile.mockClear();
|
|
installDockerReadMock();
|
|
});
|
|
}
|