


























@@ -4,6 +4,8 @@ import path from "node:path";
44import { afterEach, describe, expect, it, vi } from "vitest";
55import {
66getHomeDir,
7+getQQBotDataPath,
8+getQQBotMediaPath,
79resolveQQBotLocalMediaPath,
810resolveQQBotPayloadLocalFilePath,
911} from "./platform.js";
@@ -146,3 +148,147 @@ describe("qqbot local media path remapping", () => {
146148expect(resolveQQBotPayloadLocalFilePath(missingWorkspacePath)).toBe(fs.realpathSync(mediaFile));
147149});
148150});
151+152+// Regression coverage for https://github.com/openclaw/openclaw/issues/83562 —
153+// when HOME and OPENCLAW_HOME diverge (Docker, multi-user hosts), QQ Bot media
154+// paths must be anchored on OPENCLAW_HOME so files written under
155+// `$OPENCLAW_HOME/.openclaw/media/qqbot/` are accepted by the outbound
156+// allowlist.
157+//
158+// Tests intentionally do NOT mock `os.homedir()` — the helper reads it via
159+// `import * as os from "node:os"` which `vi.spyOn` cannot reliably intercept
160+// across the ESM/CJS interop boundary. Instead each test treats the real OS
161+// home as the baseline and only varies `process.env.OPENCLAW_HOME`.
162+describe("qqbot media path resolution honors OPENCLAW_HOME (#83562)", () => {
163+const tempPaths: string[] = [];
164+const realOsHome = getHomeDir();
165+166+afterEach(() => {
167+vi.unstubAllEnvs();
168+vi.restoreAllMocks();
169+for (const target of tempPaths.splice(0)) {
170+fs.rmSync(target, { recursive: true, force: true });
171+}
172+});
173+174+function makeFakeOpenclawHome(): string {
175+const dir = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-oc-home-"));
176+tempPaths.push(dir);
177+return dir;
178+}
179+180+it("accepts files under $OPENCLAW_HOME/.openclaw/media/qqbot when OPENCLAW_HOME differs from HOME", () => {
181+const fakeOpenclawHome = makeFakeOpenclawHome();
182+// Sanity: the fake OPENCLAW_HOME must not be a subpath of the real OS home,
183+// otherwise the test would pass for the wrong reason on hosts where
184+// `os.tmpdir()` happens to live under `$HOME`.
185+expect(fakeOpenclawHome.startsWith(realOsHome)).toBe(false);
186+vi.stubEnv("OPENCLAW_HOME", fakeOpenclawHome);
187+188+const mediaFile = path.join(fakeOpenclawHome, ".openclaw", "media", "qqbot", "repro.png");
189+fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
190+fs.writeFileSync(mediaFile, "image", "utf8");
191+192+expect(getQQBotMediaPath()).toBe(path.join(fakeOpenclawHome, ".openclaw", "media", "qqbot"));
193+expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(fs.realpathSync(mediaFile));
194+});
195+196+it("expands tilde-prefixed OPENCLAW_HOME against the OS home", () => {
197+// Use a unique subdirectory name so we can clean it up safely without
198+// touching anything that exists under the real home.
199+const sub = `qqbot-tilde-${process.pid}-${Date.now()}`;
200+const expectedHome = path.join(realOsHome, sub);
201+tempPaths.push(expectedHome);
202+vi.stubEnv("OPENCLAW_HOME", `~/${sub}`);
203+204+expect(getQQBotMediaPath()).toBe(path.join(expectedHome, ".openclaw", "media", "qqbot"));
205+206+const mediaFile = path.join(expectedHome, ".openclaw", "media", "qqbot", "tilde.png");
207+fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
208+fs.writeFileSync(mediaFile, "image", "utf8");
209+210+expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(fs.realpathSync(mediaFile));
211+});
212+213+it("falls back to OS home when OPENCLAW_HOME is unset (no regression)", () => {
214+vi.stubEnv("OPENCLAW_HOME", "");
215+216+expect(getQQBotMediaPath()).toBe(path.join(realOsHome, ".openclaw", "media", "qqbot"));
217+});
218+219+it("treats sentinel strings 'undefined' and 'null' as unset", () => {
220+for (const sentinel of ["undefined", "null"]) {
221+vi.stubEnv("OPENCLAW_HOME", sentinel);
222+expect(getQQBotMediaPath()).toBe(path.join(realOsHome, ".openclaw", "media", "qqbot"));
223+}
224+});
225+226+it("keeps persisted QQ Bot data anchored on the OS home (compatibility)", () => {
227+const fakeOpenclawHome = makeFakeOpenclawHome();
228+vi.stubEnv("OPENCLAW_HOME", fakeOpenclawHome);
229+230+// Persisted state (sessions, known users, refs) must NOT migrate when an
231+// operator adds OPENCLAW_HOME — otherwise existing deployments would lose
232+// their session state. Only the media root follows OPENCLAW_HOME.
233+expect(getQQBotDataPath()).toBe(path.join(realOsHome, ".openclaw", "qqbot"));
234+});
235+236+it("rejects files that live under HOME tree when OPENCLAW_HOME is the active root", () => {
237+const fakeOpenclawHome = makeFakeOpenclawHome();
238+vi.stubEnv("OPENCLAW_HOME", fakeOpenclawHome);
239+240+// File under the HOME-side mirror — exactly the path that *worked* on
241+// current main and *broke* the OPENCLAW_HOME setup. After the fix the
242+// active media root is OPENCLAW_HOME, so a file under HOME is no longer
243+// implicitly allowed unless it remaps via the existing workspace fallback.
244+// Use a unique subdirectory so we never collide with real user media.
245+const stale = `qqbot-stale-${process.pid}-${Date.now()}.png`;
246+const homeOnlyFile = path.join(realOsHome, ".openclaw", "media", "qqbot", stale);
247+tempPaths.push(homeOnlyFile);
248+fs.mkdirSync(path.dirname(homeOnlyFile), { recursive: true });
249+fs.writeFileSync(homeOnlyFile, "image", "utf8");
250+251+expect(resolveQQBotPayloadLocalFilePath(homeOnlyFile)).toBeNull();
252+});
253+254+it("remaps workspace paths under either HOME or OPENCLAW_HOME to the OPENCLAW_HOME media root", () => {
255+const fakeOpenclawHome = makeFakeOpenclawHome();
256+vi.stubEnv("OPENCLAW_HOME", fakeOpenclawHome);
257+258+const baseName = `remap-${process.pid}-${Date.now()}`;
259+260+// Real file lives under the OPENCLAW_HOME media tree.
261+const mediaFile = path.join(
262+fakeOpenclawHome,
263+".openclaw",
264+"media",
265+"qqbot",
266+"downloads",
267+baseName,
268+"remap.png",
269+);
270+fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
271+fs.writeFileSync(mediaFile, "image", "utf8");
272+273+// Agent that only knows the HOME-relative workspace path should still
274+// resolve to the real file thanks to the dual-tree workspace fallback.
275+const homeWorkspaceDir = path.join(realOsHome, ".openclaw", "workspace", "qqbot");
276+const homeWorkspacePath = path.join(homeWorkspaceDir, "downloads", baseName, "remap.png");
277+// Track for cleanup; we only created the unique baseName subdir indirectly
278+// through resolveQQBotLocalMediaPath, which does NOT actually create the
279+// HOME-side path, so nothing to clean up there beyond the OPENCLAW_HOME tree.
280+expect(resolveQQBotLocalMediaPath(homeWorkspacePath)).toBe(mediaFile);
281+282+// Same path but under OPENCLAW_HOME should also remap.
283+const openclawWorkspacePath = path.join(
284+fakeOpenclawHome,
285+".openclaw",
286+"workspace",
287+"qqbot",
288+"downloads",
289+baseName,
290+"remap.png",
291+);
292+expect(resolveQQBotLocalMediaPath(openclawWorkspacePath)).toBe(mediaFile);
293+});
294+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。