





















@@ -1,9 +1,7 @@
1-import fs from "node:fs";
2-import path from "node:path";
3-import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
41import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5263const getMessageContentMock = vi.hoisted(() => vi.fn());
4+const saveMediaBufferMock = vi.hoisted(() => vi.fn());
7586vi.mock("@line/bot-sdk", () => ({
97messagingApi: {
@@ -29,6 +27,10 @@ vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
2927logVerbose: () => {},
3028}));
312930+vi.mock("openclaw/plugin-sdk/media-store", () => ({
31+saveMediaBuffer: saveMediaBufferMock,
32+}));
33+3234let downloadLineMedia: typeof import("./download.js").downloadLineMedia;
33353436async function* chunks(parts: Buffer[]): AsyncGenerator<Buffer> {
@@ -45,68 +47,87 @@ describe("downloadLineMedia", () => {
4547beforeEach(() => {
4648vi.restoreAllMocks();
4749getMessageContentMock.mockReset();
50+saveMediaBufferMock.mockReset();
51+saveMediaBufferMock.mockImplementation(
52+async (_buffer: Buffer, contentType?: string, subdir?: string) => ({
53+path: `/home/user/.openclaw/media/${subdir ?? "unknown"}/saved-media`,
54+ contentType,
55+}),
56+);
4857});
495850-it("does not derive temp file path from external messageId", async () => {
51-const messageId = "a/../../../../etc/passwd";
59+it("persists inbound media with the shared media store", async () => {
5260const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
5361getMessageContentMock.mockResolvedValueOnce(chunks([jpeg]));
546255-const writeSpy = vi.spyOn(fs.promises, "writeFile").mockResolvedValueOnce(undefined);
63+const result = await downloadLineMedia("mid-jpeg", "token");
64+65+expect(saveMediaBufferMock).toHaveBeenCalledTimes(1);
66+const call = saveMediaBufferMock.mock.calls[0];
67+expect((call?.[0] as Buffer).equals(jpeg)).toBe(true);
68+expect(call?.[1]).toBe("image/jpeg");
69+expect(call?.[2]).toBe("inbound");
70+expect(call?.[3]).toBe(10 * 1024 * 1024);
71+expect(result).toEqual({
72+path: "/home/user/.openclaw/media/inbound/saved-media",
73+contentType: "image/jpeg",
74+size: jpeg.length,
75+});
76+});
77+78+it("does not pass the external messageId to saveMediaBuffer", async () => {
79+const messageId = "a/../../../../etc/passwd";
80+const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
81+getMessageContentMock.mockResolvedValueOnce(chunks([jpeg]));
56825783const result = await downloadLineMedia(messageId, "token");
58-const writtenPath = writeSpy.mock.calls[0]?.[0];
59846085expect(result.size).toBe(jpeg.length);
6186expect(result.contentType).toBe("image/jpeg");
62-expect(typeof writtenPath).toBe("string");
63-if (typeof writtenPath !== "string") {
64-throw new Error("expected string temp file path");
87+for (const arg of saveMediaBufferMock.mock.calls[0] ?? []) {
88+if (typeof arg === "string") {
89+expect(arg).not.toContain(messageId);
90+}
6591}
66-expect(result.path).toBe(writtenPath);
67-expect(writtenPath).toContain("line-media-");
68-expect(writtenPath).toMatch(/\.jpg$/);
69-expect(writtenPath).not.toContain(messageId);
70-expect(writtenPath).not.toContain("..");
71-72-const tmpRoot = path.resolve(resolvePreferredOpenClawTmpDir());
73-const rel = path.relative(tmpRoot, path.resolve(writtenPath));
74-expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
7592});
769377-it("rejects oversized media before writing to disk", async () => {
94+it("rejects oversized media before invoking saveMediaBuffer", async () => {
7895getMessageContentMock.mockResolvedValueOnce(chunks([Buffer.alloc(4), Buffer.alloc(4)]));
79-const writeSpy = vi.spyOn(fs.promises, "writeFile").mockResolvedValue(undefined);
80968197await expect(downloadLineMedia("mid", "token", 7)).rejects.toThrow(/Media exceeds/i);
82-expect(writeSpy).not.toHaveBeenCalled();
98+expect(saveMediaBufferMock).not.toHaveBeenCalled();
8399});
8410085101it("classifies M4A ftyp major brand as audio/mp4", async () => {
86102const m4aHeader = Buffer.from([
871030x00, 0x00, 0x00, 0x1c, 0x66, 0x74, 0x79, 0x70, 0x4d, 0x34, 0x41, 0x20,
88104]);
89105getMessageContentMock.mockResolvedValueOnce(chunks([m4aHeader]));
90-const writeSpy = vi.spyOn(fs.promises, "writeFile").mockResolvedValueOnce(undefined);
9110692107const result = await downloadLineMedia("mid-audio", "token");
93-const writtenPath = writeSpy.mock.calls[0]?.[0];
9410895109expect(result.contentType).toBe("audio/mp4");
96-expect(result.path).toMatch(/\.m4a$/);
97-expect(writtenPath).toBe(result.path);
110+expect(saveMediaBufferMock.mock.calls[0]?.[1]).toBe("audio/mp4");
111+expect(saveMediaBufferMock.mock.calls[0]?.[2]).toBe("inbound");
98112});
99113100114it("detects MP4 video from ftyp major brand (isom)", async () => {
101115const mp4 = Buffer.from([
1021160x00, 0x00, 0x00, 0x1c, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d,
103117]);
104118getMessageContentMock.mockResolvedValueOnce(chunks([mp4]));
105-vi.spyOn(fs.promises, "writeFile").mockResolvedValueOnce(undefined);
106119107120const result = await downloadLineMedia("mid-mp4", "token");
108121109122expect(result.contentType).toBe("video/mp4");
110-expect(result.path).toMatch(/\.mp4$/);
123+expect(saveMediaBufferMock.mock.calls[0]?.[1]).toBe("video/mp4");
124+});
125+126+it("propagates media store failures", async () => {
127+const jpeg = Buffer.from([0xff, 0xd8, 0xff, 0x00]);
128+getMessageContentMock.mockResolvedValueOnce(chunks([jpeg]));
129+saveMediaBufferMock.mockRejectedValueOnce(new Error("Media exceeds 0MB limit"));
130+131+await expect(downloadLineMedia("mid-bad", "token")).rejects.toThrow(/Media exceeds/i);
111132});
112133});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。