
















1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5+6+type CapturedEditOperations = {
7+readFile: (absolutePath: string) => Promise<Buffer>;
8+writeFile: (absolutePath: string, content: string) => Promise<void>;
9+access: (absolutePath: string) => Promise<void>;
10+};
11+12+type CapturedWriteOperations = {
13+mkdir: (dir: string) => Promise<void>;
14+writeFile: (absolutePath: string, content: string) => Promise<void>;
15+};
16+17+const mocks = vi.hoisted(() => ({
18+editOps: undefined as CapturedEditOperations | undefined,
19+writeOps: undefined as CapturedWriteOperations | undefined,
20+}));
21+22+vi.mock("@mariozechner/pi-coding-agent", async () => {
23+const actual = await vi.importActual<typeof import("@mariozechner/pi-coding-agent")>(
24+"@mariozechner/pi-coding-agent",
25+);
26+return {
27+ ...actual,
28+createEditTool: (_cwd: string, options?: { operations?: CapturedEditOperations }) => {
29+mocks.editOps = options?.operations;
30+return {
31+name: "edit",
32+description: "test edit tool",
33+parameters: { type: "object", properties: {} },
34+execute: async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
35+};
36+},
37+createWriteTool: (_cwd: string, options?: { operations?: CapturedWriteOperations }) => {
38+mocks.writeOps = options?.operations;
39+return {
40+name: "write",
41+description: "test write tool",
42+parameters: { type: "object", properties: {} },
43+execute: async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
44+};
45+},
46+};
47+});
48+49+const { createHostWorkspaceEditTool, createHostWorkspaceWriteTool } =
50+await import("./pi-tools.read.js");
51+52+const osHome = () => process.env.HOME ?? os.homedir();
53+const toTildePath = (absolutePath: string) => absolutePath.replace(osHome(), "~");
54+55+describe("host tool tilde expansion (non-workspace mode)", () => {
56+const tempDirs: string[] = [];
57+58+const createTempDir = async (prefix: string, parent = osHome()) => {
59+const dir = await fs.mkdtemp(path.join(parent, prefix));
60+tempDirs.push(dir);
61+return dir;
62+};
63+64+beforeEach(() => {
65+mocks.editOps = undefined;
66+mocks.writeOps = undefined;
67+});
68+69+afterEach(async () => {
70+vi.unstubAllEnvs();
71+mocks.editOps = undefined;
72+mocks.writeOps = undefined;
73+while (tempDirs.length > 0) {
74+await fs.rm(tempDirs.pop()!, { recursive: true, force: true });
75+}
76+});
77+78+it("edit readFile expands ~ to the OS home directory", async () => {
79+const dir = await createTempDir("openclaw-tilde-test-edit-");
80+const testFile = path.join(dir, "test.txt");
81+await fs.writeFile(testFile, "hello", "utf8");
82+83+createHostWorkspaceEditTool(dir, { workspaceOnly: false });
84+const content = await mocks.editOps!.readFile(toTildePath(testFile));
85+86+expect(content.toString("utf8")).toBe("hello");
87+});
88+89+it("edit access expands ~ to the OS home directory", async () => {
90+const dir = await createTempDir("openclaw-tilde-test-edit-");
91+const testFile = path.join(dir, "test.txt");
92+await fs.writeFile(testFile, "hello", "utf8");
93+94+createHostWorkspaceEditTool(dir, { workspaceOnly: false });
95+96+await expect(mocks.editOps!.access(toTildePath(testFile))).resolves.toBeUndefined();
97+});
98+99+it("write writeFile expands ~ to the OS home directory", async () => {
100+const dir = await createTempDir("openclaw-tilde-test-write-");
101+const testFile = path.join(dir, "tilde-write-test.txt");
102+103+createHostWorkspaceWriteTool(dir, { workspaceOnly: false });
104+await mocks.writeOps!.writeFile(toTildePath(testFile), "written via tilde");
105+106+expect(await fs.readFile(testFile, "utf8")).toBe("written via tilde");
107+});
108+109+it("write mkdir expands ~ to the OS home directory", async () => {
110+const dir = await createTempDir("openclaw-tilde-test-mkdir-");
111+const newDir = path.join(dir, "subdir");
112+113+createHostWorkspaceWriteTool(dir, { workspaceOnly: false });
114+await mocks.writeOps!.mkdir(toTildePath(newDir));
115+116+expect((await fs.stat(newDir)).isDirectory()).toBe(true);
117+});
118+119+it("ignores OPENCLAW_HOME for write operations", async () => {
120+const openclawHome = await createTempDir("openclaw-home-override-", os.tmpdir());
121+const dir = await createTempDir("openclaw-tilde-test-write-");
122+const testFile = path.join(dir, "os-home-write.txt");
123+vi.stubEnv("OPENCLAW_HOME", openclawHome);
124+125+createHostWorkspaceWriteTool(openclawHome, { workspaceOnly: false });
126+await mocks.writeOps!.writeFile(toTildePath(testFile), "written via os home");
127+128+expect(await fs.readFile(testFile, "utf8")).toBe("written via os home");
129+await expect(fs.access(path.join(openclawHome, path.basename(testFile)))).rejects.toBeDefined();
130+});
131+132+it("ignores OPENCLAW_HOME for mkdir operations", async () => {
133+const openclawHome = await createTempDir("openclaw-home-override-", os.tmpdir());
134+const dir = await createTempDir("openclaw-tilde-test-mkdir-");
135+const newDir = path.join(dir, "os-home-subdir");
136+vi.stubEnv("OPENCLAW_HOME", openclawHome);
137+138+createHostWorkspaceWriteTool(openclawHome, { workspaceOnly: false });
139+await mocks.writeOps!.mkdir(toTildePath(newDir));
140+141+expect((await fs.stat(newDir)).isDirectory()).toBe(true);
142+await expect(fs.access(path.join(openclawHome, path.basename(newDir)))).rejects.toBeDefined();
143+});
144+145+it("ignores OPENCLAW_HOME for readFile operations", async () => {
146+const openclawHome = await createTempDir("openclaw-home-override-", os.tmpdir());
147+const dir = await createTempDir("openclaw-tilde-test-edit-");
148+const testFile = path.join(dir, "os-home-read.txt");
149+await fs.writeFile(testFile, "OS home content", "utf8");
150+vi.stubEnv("OPENCLAW_HOME", openclawHome);
151+152+createHostWorkspaceEditTool(openclawHome, { workspaceOnly: false });
153+const content = await mocks.editOps!.readFile(toTildePath(testFile));
154+155+expect(content.toString("utf8")).toBe("OS home content");
156+await expect(fs.access(path.join(openclawHome, path.basename(testFile)))).rejects.toBeDefined();
157+});
158+159+it("ignores OPENCLAW_HOME for access operations", async () => {
160+const openclawHome = await createTempDir("openclaw-home-override-", os.tmpdir());
161+const dir = await createTempDir("openclaw-tilde-test-edit-");
162+const testFile = path.join(dir, "os-home-access.txt");
163+await fs.writeFile(testFile, "exists", "utf8");
164+vi.stubEnv("OPENCLAW_HOME", openclawHome);
165+166+createHostWorkspaceEditTool(openclawHome, { workspaceOnly: false });
167+168+await expect(mocks.editOps!.access(toTildePath(testFile))).resolves.toBeUndefined();
169+await expect(fs.access(path.join(openclawHome, path.basename(testFile)))).rejects.toBeDefined();
170+});
171+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。