import fs from "node:fs" ;
import os from "node:os" ;
import path from "node:path" ;
import { afterEach, describe, expect, it, vi } from "vitest" ;
import {
getHomeDir,
resolveQQBotLocalMediaPath,
resolveQQBotPayloadLocalFilePath,
} from "./platform.js" ;
describe("qqbot local media path remapping" , () => {
const createdPaths: string[] = [];
function createOpenClawTestRoot() {
const actualHome = getHomeDir();
const openclawDir = path.join(actualHome, ".openclaw" );
fs.mkdirSync(openclawDir, { recursive: true });
const testRoot = fs.mkdtempSync(path.join(openclawDir, "qqbot-platform-test-" ));
createdPaths.push(testRoot);
return { actualHome, testRootName: path.basename(testRoot) };
}
function createQqbotMediaFile(fileName: string) {
const { actualHome, testRootName } = createOpenClawTestRoot();
const mediaFile = path.join(
actualHome,
".openclaw" ,
"media" ,
"qqbot" ,
"downloads" ,
testRootName,
fileName,
);
fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
fs.writeFileSync(mediaFile, "image" , "utf8" );
createdPaths.push(path.dirname(mediaFile));
return { actualHome, testRootName, mediaFile };
}
afterEach(() => {
vi.restoreAllMocks();
for (const target of createdPaths.splice(0 )) {
fs.rmSync(target, { recursive: true , force: true });
}
});
it("remaps missing workspace media paths to the real media directory" , () => {
const { actualHome, testRootName, mediaFile } = createQqbotMediaFile("example.png" );
const missingWorkspacePath = path.join(
actualHome,
".openclaw" ,
"workspace" ,
"qqbot" ,
"downloads" ,
testRootName,
"example.png" ,
);
expect(resolveQQBotLocalMediaPath(missingWorkspacePath)).toBe(mediaFile);
});
it("leaves existing media paths unchanged" , () => {
const { mediaFile } = createQqbotMediaFile("existing.png" );
expect(resolveQQBotLocalMediaPath(mediaFile)).toBe(mediaFile);
});
it("blocks structured payload files outside QQ Bot storage" , () => {
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-platform-outside-" ));
createdPaths.push(outsideRoot);
const outsideFile = path.join(outsideRoot, "secret.txt" );
fs.writeFileSync(outsideFile, "secret" , "utf8" );
expect(resolveQQBotPayloadLocalFilePath(outsideFile)).toBeNull();
});
it("blocks structured payload paths that escape QQ Bot media via '..'" , () => {
const escapedPath = path.join(
getHomeDir(),
".openclaw" ,
"media" ,
"qqbot" ,
".." ,
".." ,
"qqbot-escape.txt" ,
);
expect(resolveQQBotPayloadLocalFilePath(escapedPath)).toBeNull();
});
it("allows structured payload files inside the QQ Bot media directory" , () => {
const { mediaFile } = createQqbotMediaFile("allowed.png" );
expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(fs.realpathSync(mediaFile));
});
it("allows structured payload files inside sibling OpenClaw media subdirectories" , () => {
// Core helpers such as `saveMediaBuffer(..., "outbound", ...)` place framework
// attachments under sibling directories of `media/qqbot/`. The plugin must
// trust the shared `~/.openclaw/media` root so auto-routed sends can access
// those files without the path-outside-storage guard firing.
const actualHome = getHomeDir();
const outboundDir = path.join(actualHome, ".openclaw" , "media" , "outbound" );
fs.mkdirSync(outboundDir, { recursive: true });
const outboundFile = fs.mkdtempSync(path.join(outboundDir, "qqbot-outbound-" ));
const mediaFile = path.join(outboundFile, "tts.mp3" );
fs.writeFileSync(mediaFile, "audio" , "utf8" );
createdPaths.push(outboundFile);
expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(fs.realpathSync(mediaFile));
});
it("blocks structured payload files inside the QQ Bot data directory" , () => {
const { actualHome, testRootName } = createOpenClawTestRoot();
const dataFile = path.join(
actualHome,
".openclaw" ,
"qqbot" ,
"sessions" ,
testRootName,
"session.json" ,
);
fs.mkdirSync(path.dirname(dataFile), { recursive: true });
fs.writeFileSync(dataFile, "{}" , "utf8" );
createdPaths.push(path.dirname(dataFile));
expect(resolveQQBotPayloadLocalFilePath(dataFile)).toBeNull();
});
it("allows legacy workspace paths when they remap into QQ Bot media storage" , () => {
const { actualHome, testRootName, mediaFile } = createQqbotMediaFile("legacy.png" );
const missingWorkspacePath = path.join(
actualHome,
".openclaw" ,
"workspace" ,
"qqbot" ,
"downloads" ,
testRootName,
"legacy.png" ,
);
expect(resolveQQBotPayloadLocalFilePath(missingWorkspacePath)).toBe(fs.realpathSync(mediaFile));
});
});
Messung V0.5 in Prozent C=95 H=95 G=94
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-05)
¤
*© Formatika GbR, Deutschland