mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-28 01:21:36 +00:00
* fix(media): resolve relative MEDIA: paths against agent workspace dir * fix(agents): remove stale ollama compat import * fix(media): preserve workspace dir in outbound access
175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
|
import { createJpegBufferWithDimensions, createPngBufferWithDimensions } from "./test-helpers.js";
|
|
|
|
let loadWebMedia: typeof import("./web-media.js").loadWebMedia;
|
|
|
|
const TINY_PNG_BASE64 =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
|
|
|
let fixtureRoot = "";
|
|
let oversizedJpegFile = "";
|
|
let tinyPngFile = "";
|
|
|
|
beforeAll(async () => {
|
|
({ loadWebMedia } = await import("./web-media.js"));
|
|
fixtureRoot = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "web-media-core-"));
|
|
tinyPngFile = path.join(fixtureRoot, "tiny.png");
|
|
oversizedJpegFile = path.join(fixtureRoot, "oversized.jpg");
|
|
await fs.writeFile(tinyPngFile, Buffer.from(TINY_PNG_BASE64, "base64"));
|
|
await fs.writeFile(
|
|
oversizedJpegFile,
|
|
createJpegBufferWithDimensions({ width: 6_000, height: 5_000 }),
|
|
);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (fixtureRoot) {
|
|
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("loadWebMedia", () => {
|
|
function createLocalWebMediaOptions() {
|
|
return {
|
|
maxBytes: 1024 * 1024,
|
|
localRoots: [fixtureRoot],
|
|
};
|
|
}
|
|
|
|
async function expectRejectedWebMedia(
|
|
url: string,
|
|
expectedError: Record<string, unknown> | RegExp,
|
|
setup?: () => { restore?: () => void; mockRestore?: () => void } | undefined,
|
|
) {
|
|
const restoreHandle = setup?.();
|
|
try {
|
|
if (expectedError instanceof RegExp) {
|
|
await expect(loadWebMedia(url, createLocalWebMediaOptions())).rejects.toThrow(
|
|
expectedError,
|
|
);
|
|
return;
|
|
}
|
|
await expect(loadWebMedia(url, createLocalWebMediaOptions())).rejects.toMatchObject(
|
|
expectedError,
|
|
);
|
|
} finally {
|
|
restoreHandle?.mockRestore?.();
|
|
restoreHandle?.restore?.();
|
|
}
|
|
}
|
|
|
|
async function expectRejectedWebMediaWithoutFilesystemAccess(params: {
|
|
url: string;
|
|
expectedError: Record<string, unknown> | RegExp;
|
|
setup?: () => { restore?: () => void; mockRestore?: () => void } | undefined;
|
|
}) {
|
|
const realpathSpy = vi.spyOn(fs, "realpath");
|
|
try {
|
|
await expectRejectedWebMedia(params.url, params.expectedError, params.setup);
|
|
expect(realpathSpy).not.toHaveBeenCalled();
|
|
} finally {
|
|
realpathSpy.mockRestore();
|
|
}
|
|
}
|
|
|
|
async function expectLoadedWebMediaCase(url: string) {
|
|
const result = await loadWebMedia(url, createLocalWebMediaOptions());
|
|
expect(result.kind).toBe("image");
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
}
|
|
|
|
it.each([
|
|
{
|
|
name: "allows localhost file URLs for local files",
|
|
createUrl: () => {
|
|
const fileUrl = pathToFileURL(tinyPngFile);
|
|
fileUrl.hostname = "localhost";
|
|
return fileUrl.href;
|
|
},
|
|
},
|
|
] as const)("$name", async ({ createUrl }) => {
|
|
await expectLoadedWebMediaCase(createUrl());
|
|
});
|
|
|
|
it("rejects oversized pixel-count images before decode/resize backends run", async () => {
|
|
const oversizedPngFile = path.join(fixtureRoot, "oversized.png");
|
|
await fs.writeFile(
|
|
oversizedPngFile,
|
|
createPngBufferWithDimensions({ width: 8_000, height: 4_000 }),
|
|
);
|
|
|
|
await expect(loadWebMedia(oversizedPngFile, createLocalWebMediaOptions())).rejects.toThrow(
|
|
/pixel input limit/i,
|
|
);
|
|
});
|
|
|
|
it("preserves pixel-limit errors for oversized JPEG optimization", async () => {
|
|
await expect(loadWebMedia(oversizedJpegFile, createLocalWebMediaOptions())).rejects.toThrow(
|
|
/pixel input limit/i,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "rejects remote-host file URLs before filesystem checks",
|
|
url: "file://attacker/share/evil.png",
|
|
expectedError: { code: "invalid-file-url" },
|
|
},
|
|
{
|
|
name: "rejects remote-host file URLs with the explicit error message before filesystem checks",
|
|
url: "file://attacker/share/evil.png",
|
|
expectedError: /remote hosts are not allowed/i,
|
|
},
|
|
{
|
|
name: "rejects Windows network paths before filesystem checks",
|
|
url: "\\\\attacker\\share\\evil.png",
|
|
expectedError: { code: "network-path-not-allowed" },
|
|
setup: () => vi.spyOn(process, "platform", "get").mockReturnValue("win32"),
|
|
},
|
|
] as const)("$name", async (testCase) => {
|
|
await expectRejectedWebMediaWithoutFilesystemAccess(testCase);
|
|
});
|
|
|
|
describe("workspaceDir relative path resolution", () => {
|
|
it("resolves a bare filename against workspaceDir", async () => {
|
|
const result = await loadWebMedia("tiny.png", {
|
|
...createLocalWebMediaOptions(),
|
|
workspaceDir: fixtureRoot,
|
|
});
|
|
expect(result.kind).toBe("image");
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("resolves a dot-relative path against workspaceDir", async () => {
|
|
const result = await loadWebMedia("./tiny.png", {
|
|
...createLocalWebMediaOptions(),
|
|
workspaceDir: fixtureRoot,
|
|
});
|
|
expect(result.kind).toBe("image");
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("resolves a MEDIA:-prefixed relative path against workspaceDir", async () => {
|
|
const result = await loadWebMedia("MEDIA:tiny.png", {
|
|
...createLocalWebMediaOptions(),
|
|
workspaceDir: fixtureRoot,
|
|
});
|
|
expect(result.kind).toBe("image");
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("leaves absolute paths unchanged when workspaceDir is set", async () => {
|
|
const result = await loadWebMedia(tinyPngFile, {
|
|
...createLocalWebMediaOptions(),
|
|
workspaceDir: "/some/other/dir",
|
|
});
|
|
expect(result.kind).toBe("image");
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|