Files
openclaw/src/agents/tool-images.e2e.test.ts
Sriram Naidu Thota 63fb998074 fix: address code review feedback
- Use stricter regex: /^[A-Za-z0-9+/]*={0,2}$/ ensures = only at end
- Normalize URL-safe base64 to standard (- → +, _ → /)
- Added tests for padding in wrong position and URL-safe normalization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:53:54 +01:00

260 lines
7.2 KiB
TypeScript

import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { sanitizeContentBlocksImages, sanitizeImageBlocks } from "./tool-images.js";
describe("base64 validation", () => {
it("rejects invalid base64 characters and replaces with error text", async () => {
const blocks = [
{
type: "image" as const,
data: "not-valid-base64!!!@#$%",
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
expect(out[0].text).toContain("invalid");
}
});
it("strips data URL prefix and processes valid base64", async () => {
// Create a small valid image
const jpeg = await sharp({
create: {
width: 10,
height: 10,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.jpeg()
.toBuffer();
const base64 = jpeg.toString("base64");
const dataUrl = `data:image/jpeg;base64,${base64}`;
const blocks = [
{
type: "image" as const,
data: dataUrl,
mimeType: "image/jpeg",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("image");
});
it("rejects base64 with invalid padding", async () => {
const blocks = [
{
type: "image" as const,
data: "SGVsbG8===", // too many padding chars
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
}
});
it("rejects base64 with padding in wrong position", async () => {
const blocks = [
{
type: "image" as const,
data: "SGVs=bG8=", // = in middle is invalid
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
}
});
it("normalizes URL-safe base64 to standard base64", async () => {
// Create a small valid image
const jpeg = await sharp({
create: {
width: 10,
height: 10,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.jpeg()
.toBuffer();
// Convert to URL-safe base64 (replace + with -, / with _)
const standardBase64 = jpeg.toString("base64");
const urlSafeBase64 = standardBase64.replace(/\+/g, "-").replace(/\//g, "_");
const blocks = [
{
type: "image" as const,
data: urlSafeBase64,
mimeType: "image/jpeg",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("image");
});
it("rejects base64 with invalid length", async () => {
const blocks = [
{
type: "image" as const,
data: "AAAAA", // length 5 without padding is invalid (remainder 1)
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted image payload");
}
});
it("handles empty base64 data gracefully", async () => {
const blocks = [
{
type: "image" as const,
data: " ",
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
expect(out.length).toBe(1);
expect(out[0].type).toBe("text");
if (out[0].type === "text") {
expect(out[0].text).toContain("omitted empty image payload");
}
});
});
describe("tool image sanitizing", () => {
it("shrinks oversized images to <=5MB", async () => {
const width = 2800;
const height = 2800;
const raw = Buffer.alloc(width * height * 3, 0xff);
const bigPng = await sharp(raw, {
raw: { width, height, channels: 3 },
})
.png({ compressionLevel: 0 })
.toBuffer();
expect(bigPng.byteLength).toBeGreaterThan(5 * 1024 * 1024);
const blocks = [
{
type: "image" as const,
data: bigPng.toString("base64"),
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
const image = out.find((b) => b.type === "image");
if (!image || image.type !== "image") {
throw new Error("expected image block");
}
const size = Buffer.from(image.data, "base64").byteLength;
expect(size).toBeLessThanOrEqual(5 * 1024 * 1024);
expect(image.mimeType).toBe("image/jpeg");
}, 20_000);
it("sanitizes image arrays and reports drops", async () => {
const width = 2600;
const height = 400;
const raw = Buffer.alloc(width * height * 3, 0x7f);
const png = await sharp(raw, {
raw: { width, height, channels: 3 },
})
.png({ compressionLevel: 9 })
.toBuffer();
const images = [
{ type: "image" as const, data: png.toString("base64"), mimeType: "image/png" },
];
const { images: out, dropped } = await sanitizeImageBlocks(images, "test");
expect(dropped).toBe(0);
expect(out.length).toBe(1);
const meta = await sharp(Buffer.from(out[0].data, "base64")).metadata();
expect(meta.width).toBeLessThanOrEqual(2000);
expect(meta.height).toBeLessThanOrEqual(2000);
}, 20_000);
it("shrinks images that exceed max dimension even if size is small", async () => {
const width = 2600;
const height = 400;
const raw = Buffer.alloc(width * height * 3, 0x7f);
const png = await sharp(raw, {
raw: { width, height, channels: 3 },
})
.png({ compressionLevel: 9 })
.toBuffer();
const blocks = [
{
type: "image" as const,
data: png.toString("base64"),
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
const image = out.find((b) => b.type === "image");
if (!image || image.type !== "image") {
throw new Error("expected image block");
}
const meta = await sharp(Buffer.from(image.data, "base64")).metadata();
expect(meta.width).toBeLessThanOrEqual(2000);
expect(meta.height).toBeLessThanOrEqual(2000);
expect(image.mimeType).toBe("image/jpeg");
}, 20_000);
it("corrects mismatched jpeg mimeType", async () => {
const jpeg = await sharp({
create: {
width: 10,
height: 10,
channels: 3,
background: { r: 255, g: 0, b: 0 },
},
})
.jpeg()
.toBuffer();
const blocks = [
{
type: "image" as const,
data: jpeg.toString("base64"),
mimeType: "image/png",
},
];
const out = await sanitizeContentBlocksImages(blocks, "test");
const image = out.find((b) => b.type === "image");
if (!image || image.type !== "image") {
throw new Error("expected image block");
}
expect(image.mimeType).toBe("image/jpeg");
});
});