test(qqbot): share symlink race setup

This commit is contained in:
Vincent Koc
2026-04-12 05:35:57 +01:00
parent d62279a9b2
commit 8f0da7ef06

View File

@@ -172,6 +172,31 @@ function writeFileWithParents(filePath: string, content: string = "payload"): nu
return fs.statSync(filePath).size;
}
function installMissingSegmentSymlinkRace(
delayedVoicePath: string,
outsideRootPrefix: string,
): boolean {
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), outsideRootPrefix));
createdRoots.push(outsideRoot);
const symlinkProbe = path.join(path.dirname(path.dirname(delayedVoicePath)), "probe-link");
try {
fs.symlinkSync(outsideRoot, symlinkProbe, "dir");
fs.unlinkSync(symlinkProbe);
} catch {
return false;
}
audioConvertMocks.waitForFile.mockImplementationOnce(async (candidatePath: string) => {
const symlinkParent = path.dirname(candidatePath);
fs.symlinkSync(outsideRoot, symlinkParent, "dir");
const outsideFile = path.join(outsideRoot, path.basename(candidatePath));
return writeFileWithParents(outsideFile);
});
return true;
}
function expectBlocked(result: OutboundResult, expectedError: string): void {
expect(result.channel).toBe("qqbot");
expect(result.error).toBe(expectedError);
@@ -225,24 +250,10 @@ describe("qqbot outbound local media path security", () => {
it("blocks delayed voice paths when a missing segment is replaced by a symlink after precheck", async () => {
const delayedVoicePath = createDelayedMissingMediaPath(".mp3");
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-outbound-race-outside-"));
createdRoots.push(outsideRoot);
const symlinkProbe = path.join(path.dirname(path.dirname(delayedVoicePath)), "probe-link");
try {
fs.symlinkSync(outsideRoot, symlinkProbe, "dir");
fs.unlinkSync(symlinkProbe);
} catch {
if (!installMissingSegmentSymlinkRace(delayedVoicePath, "qqbot-outbound-race-outside-")) {
return;
}
audioConvertMocks.waitForFile.mockImplementationOnce(async (candidatePath: string) => {
const symlinkParent = path.dirname(candidatePath);
fs.symlinkSync(outsideRoot, symlinkParent, "dir");
const outsideFile = path.join(outsideRoot, path.basename(candidatePath));
return writeFileWithParents(outsideFile);
});
const result = await sendVoice(buildTarget(), delayedVoicePath, undefined, true);
expectBlocked(result, "Voice path must be inside QQ Bot media storage");
@@ -356,24 +367,10 @@ describe("qqbot outbound local media path security", () => {
it("blocks sendMedia delayed audio paths when a missing segment is replaced by a symlink", async () => {
const delayedVoicePath = createDelayedMissingMediaPath(".mp3");
const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-outbound-race-sendmedia-"));
createdRoots.push(outsideRoot);
const symlinkProbe = path.join(path.dirname(path.dirname(delayedVoicePath)), "probe-link");
try {
fs.symlinkSync(outsideRoot, symlinkProbe, "dir");
fs.unlinkSync(symlinkProbe);
} catch {
if (!installMissingSegmentSymlinkRace(delayedVoicePath, "qqbot-outbound-race-sendmedia-")) {
return;
}
audioConvertMocks.waitForFile.mockImplementationOnce(async (candidatePath: string) => {
const symlinkParent = path.dirname(candidatePath);
fs.symlinkSync(outsideRoot, symlinkParent, "dir");
const outsideFile = path.join(outsideRoot, path.basename(candidatePath));
return writeFileWithParents(outsideFile);
});
const result = await sendMedia(buildMediaContext(delayedVoicePath));
expectBlocked(