
































@@ -0,0 +1,120 @@
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+import { stageIMessageAttachments } from "./media-staging.js";
6+7+let tempDir: string;
8+9+async function writeTempFile(name: string, contents: Buffer | string): Promise<string> {
10+const filePath = path.join(tempDir, name);
11+await fs.writeFile(filePath, contents);
12+return filePath;
13+}
14+15+describe("stageIMessageAttachments", () => {
16+beforeEach(async () => {
17+tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-imessage-media-"));
18+});
19+20+afterEach(async () => {
21+await fs.rm(tempDir, { recursive: true, force: true });
22+});
23+24+it("copies allowed iMessage attachments into the inbound media store", async () => {
25+const sourcePath = await writeTempFile("photo.png", Buffer.from("png-bytes"));
26+const saveMediaBuffer = vi.fn(async () => ({
27+id: "saved.png",
28+path: "/state/media/inbound/saved.png",
29+size: 9,
30+contentType: "image/png",
31+}));
32+33+await expect(
34+stageIMessageAttachments(
35+[{ original_path: sourcePath, mime_type: "image/png", missing: false }],
36+{ maxBytes: 1024, allowedRoots: [tempDir], deps: { saveMediaBuffer } },
37+),
38+).resolves.toEqual([{ path: "/state/media/inbound/saved.png", contentType: "image/png" }]);
39+40+expect(saveMediaBuffer).toHaveBeenCalledWith(
41+Buffer.from("png-bytes"),
42+"image/png",
43+"inbound",
44+1024,
45+"photo.png",
46+);
47+});
48+49+it("drops attachments whose canonical path escapes the allowed root", async () => {
50+const allowedRoot = path.join(tempDir, "allowed");
51+const outsideRoot = path.join(tempDir, "outside");
52+await fs.mkdir(allowedRoot, { recursive: true });
53+await fs.mkdir(outsideRoot, { recursive: true });
54+const outsidePath = path.join(outsideRoot, "secret.png");
55+await fs.writeFile(outsidePath, Buffer.from("secret-bytes"));
56+await fs.symlink(outsideRoot, path.join(allowedRoot, "link"), "dir");
57+58+const saveMediaBuffer = vi.fn();
59+const logVerbose = vi.fn();
60+61+await expect(
62+stageIMessageAttachments(
63+[
64+{
65+original_path: path.join(allowedRoot, "link", "secret.png"),
66+mime_type: "image/png",
67+missing: false,
68+},
69+],
70+{ maxBytes: 1024, allowedRoots: [allowedRoot], deps: { saveMediaBuffer, logVerbose } },
71+),
72+).resolves.toEqual([]);
73+74+expect(saveMediaBuffer).not.toHaveBeenCalled();
75+expect(logVerbose).toHaveBeenCalledWith(
76+expect.stringContaining("attachment path resolves outside allowed roots"),
77+);
78+});
79+80+it("converts HEIC iMessage attachments to JPEG before staging", async () => {
81+const sourcePath = await writeTempFile("IMG_0001.HEIC", Buffer.from("heic-bytes"));
82+const saveMediaBuffer = vi.fn(async () => ({
83+id: "saved.jpg",
84+path: "/state/media/inbound/saved.jpg",
85+size: 10,
86+contentType: "image/jpeg",
87+}));
88+const convertHeicToJpeg = vi.fn(async () => Buffer.from("jpeg-bytes"));
89+90+await stageIMessageAttachments(
91+[{ original_path: sourcePath, mime_type: "image/heic", missing: false }],
92+{ maxBytes: 1024, deps: { saveMediaBuffer, convertHeicToJpeg } },
93+);
94+95+expect(convertHeicToJpeg).toHaveBeenCalledWith(sourcePath, 1024);
96+expect(saveMediaBuffer).toHaveBeenCalledWith(
97+Buffer.from("jpeg-bytes"),
98+"image/jpeg",
99+"inbound",
100+1024,
101+"IMG_0001.jpg",
102+);
103+});
104+105+it("drops attachments over the inbound media limit", async () => {
106+const sourcePath = await writeTempFile("huge.png", Buffer.from("too large"));
107+const saveMediaBuffer = vi.fn();
108+const logVerbose = vi.fn();
109+110+await expect(
111+stageIMessageAttachments(
112+[{ original_path: sourcePath, mime_type: "image/png", missing: false }],
113+{ maxBytes: 4, deps: { saveMediaBuffer, logVerbose } },
114+),
115+).resolves.toEqual([]);
116+117+expect(saveMediaBuffer).not.toHaveBeenCalled();
118+expect(logVerbose).toHaveBeenCalledWith(expect.stringContaining("failed to stage"));
119+});
120+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。