



























@@ -0,0 +1,125 @@
1+import { spawn } from "node:child_process";
2+import fs from "node:fs/promises";
3+import os from "node:os";
4+import path from "node:path";
5+import { describe, expect, it } from "vitest";
6+import type {
7+SandboxBackendHandle,
8+SandboxBackendCommandParams,
9+SandboxBackendCommandResult,
10+} from "./backend-handle.types.js";
11+12+async function runLocalShellCommand(
13+params: SandboxBackendCommandParams,
14+): Promise<SandboxBackendCommandResult> {
15+return await new Promise<SandboxBackendCommandResult>((resolve, reject) => {
16+const child = spawn(
17+"sh",
18+["-c", params.script, "openclaw-sandbox-fs", ...(params.args ?? [])],
19+{
20+stdio: ["pipe", "pipe", "pipe"],
21+},
22+);
23+24+const stdoutChunks: Buffer[] = [];
25+const stderrChunks: Buffer[] = [];
26+let aborted = false;
27+28+const onAbort = () => {
29+if (aborted) {
30+return;
31+}
32+aborted = true;
33+child.kill("SIGTERM");
34+};
35+params.signal?.addEventListener("abort", onAbort);
36+37+child.stdout?.on("data", (chunk) => {
38+stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
39+});
40+child.stderr?.on("data", (chunk) => {
41+stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
42+});
43+44+child.on("error", reject);
45+child.on("close", (code) => {
46+params.signal?.removeEventListener("abort", onAbort);
47+if (aborted || params.signal?.aborted) {
48+const error = new Error("Aborted");
49+error.name = "AbortError";
50+reject(error);
51+return;
52+}
53+54+const result = {
55+stdout: Buffer.concat(stdoutChunks),
56+stderr: Buffer.concat(stderrChunks),
57+code: code ?? 0,
58+};
59+if (result.code !== 0 && !params.allowFailure) {
60+reject(new Error(result.stderr.toString("utf8").trim() || `shell exited ${result.code}`));
61+return;
62+}
63+resolve(result);
64+});
65+66+if (child.stdin) {
67+child.stdin.end(params.stdin);
68+}
69+});
70+}
71+72+describe("sandbox fs bridge local backend e2e", () => {
73+it.runIf(process.platform !== "win32")(
74+"writes through backend shell commands using the pinned mutation helper",
75+async () => {
76+const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-fsbridge-e2e-"));
77+const workspacePath = path.join(stateDir, "workspace");
78+await fs.mkdir(workspacePath, { recursive: true });
79+const workspaceDir = await fs.realpath(workspacePath);
80+const scripts: string[] = [];
81+const backend: SandboxBackendHandle = {
82+id: "local-test",
83+runtimeId: "local-backend-fsbridge",
84+runtimeLabel: "local-backend-fsbridge",
85+workdir: workspaceDir,
86+buildExecSpec: async ({ command, env }) => ({
87+argv: ["sh", "-c", command],
88+ env,
89+stdinMode: "pipe-closed",
90+}),
91+runShellCommand: async (params) => {
92+scripts.push(params.script);
93+return await runLocalShellCommand(params);
94+},
95+};
96+97+try {
98+const [{ createSandboxFsBridge }, { createSandboxTestContext }] = await Promise.all([
99+import("./fs-bridge.js"),
100+import("./test-fixtures.js"),
101+]);
102+103+const sandbox = createSandboxTestContext({
104+overrides: {
105+ workspaceDir,
106+agentWorkspaceDir: workspaceDir,
107+containerName: "local-backend-fsbridge",
108+containerWorkdir: workspaceDir,
109+ backend,
110+},
111+});
112+113+const bridge = createSandboxFsBridge({ sandbox });
114+await bridge.writeFile({ filePath: "nested/hello.txt", data: "from-backend" });
115+116+await expect(
117+fs.readFile(path.join(workspaceDir, "nested", "hello.txt"), "utf8"),
118+).resolves.toBe("from-backend");
119+expect(scripts.some((script) => script.includes("operation = sys.argv[1]"))).toBe(true);
120+} finally {
121+await fs.rm(stateDir, { recursive: true, force: true });
122+}
123+},
124+);
125+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。