fix(coding-agent): enforce safe auto-resized image limits closes #2055

This commit is contained in:
Mario Zechner
2026-03-22 21:48:34 +01:00
parent f1fe49a641
commit a78882d8b8
6 changed files with 161 additions and 139 deletions

View File

@@ -52,12 +52,13 @@ describe("resizeImage", () => {
{ maxWidth: 100, maxHeight: 100, maxBytes: 1024 * 1024 },
);
expect(result.wasResized).toBe(false);
expect(result.data).toBe(TINY_PNG);
expect(result.originalWidth).toBe(2);
expect(result.originalHeight).toBe(2);
expect(result.width).toBe(2);
expect(result.height).toBe(2);
expect(result).not.toBeNull();
expect(result!.wasResized).toBe(false);
expect(result!.data).toBe(TINY_PNG);
expect(result!.originalWidth).toBe(2);
expect(result!.originalHeight).toBe(2);
expect(result!.width).toBe(2);
expect(result!.height).toBe(2);
});
it("should resize image exceeding dimension limits", async () => {
@@ -66,26 +67,38 @@ describe("resizeImage", () => {
{ maxWidth: 50, maxHeight: 50, maxBytes: 1024 * 1024 },
);
expect(result.wasResized).toBe(true);
expect(result.originalWidth).toBe(100);
expect(result.originalHeight).toBe(100);
expect(result.width).toBeLessThanOrEqual(50);
expect(result.height).toBeLessThanOrEqual(50);
expect(result).not.toBeNull();
expect(result!.wasResized).toBe(true);
expect(result!.originalWidth).toBe(100);
expect(result!.originalHeight).toBe(100);
expect(result!.width).toBeLessThanOrEqual(50);
expect(result!.height).toBeLessThanOrEqual(50);
});
it("should resize image exceeding byte limit", async () => {
const originalBuffer = Buffer.from(LARGE_PNG_200x200, "base64");
const originalSize = originalBuffer.length;
// Set maxBytes to less than the original image size
// Set maxBytes to less than the original encoded image size
const result = await resizeImage(
{ type: "image", data: LARGE_PNG_200x200, mimeType: "image/png" },
{ maxWidth: 2000, maxHeight: 2000, maxBytes: Math.floor(originalSize / 2) },
{ maxWidth: 2000, maxHeight: 2000, maxBytes: Math.floor(LARGE_PNG_200x200.length * 0.9) },
);
// Should have tried to reduce size
const resultBuffer = Buffer.from(result.data, "base64");
expect(result).not.toBeNull();
const resultBuffer = Buffer.from(result!.data, "base64");
expect(resultBuffer.length).toBeLessThan(originalSize);
expect(result!.data.length).toBeLessThan(LARGE_PNG_200x200.length);
});
it("should return null when image cannot be resized below maxBytes", async () => {
const result = await resizeImage(
{ type: "image", data: LARGE_PNG_200x200, mimeType: "image/png" },
{ maxWidth: 2000, maxHeight: 2000, maxBytes: 1 },
);
expect(result).toBeNull();
});
it("should handle JPEG input", async () => {
@@ -94,9 +107,10 @@ describe("resizeImage", () => {
{ maxWidth: 100, maxHeight: 100, maxBytes: 1024 * 1024 },
);
expect(result.wasResized).toBe(false);
expect(result.originalWidth).toBe(2);
expect(result.originalHeight).toBe(2);
expect(result).not.toBeNull();
expect(result!.wasResized).toBe(false);
expect(result!.originalWidth).toBe(2);
expect(result!.originalHeight).toBe(2);
});
});

View File

@@ -0,0 +1,53 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../src/utils/image-resize.js", () => ({
resizeImage: vi.fn(),
formatDimensionNote: vi.fn(() => undefined),
}));
import { processFileArguments } from "../src/cli/file-processor.js";
import { createReadTool } from "../src/core/tools/read.js";
import { resizeImage } from "../src/utils/image-resize.js";
const TINY_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
describe("image resize callers", () => {
let testDir: string;
beforeEach(() => {
testDir = join(tmpdir(), `image-resize-callers-${Date.now()}`);
mkdirSync(testDir, { recursive: true });
vi.mocked(resizeImage).mockReset();
vi.mocked(resizeImage).mockResolvedValue(null);
});
afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});
it("read tool returns text-only output when auto-resize cannot produce a safe image", async () => {
const imagePath = join(testDir, "test.png");
writeFileSync(imagePath, Buffer.from(TINY_PNG_BASE64, "base64"));
const tool = createReadTool(testDir);
const result = await tool.execute("test-read-image", { path: imagePath });
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe("text");
expect((result.content[0] as { type: "text"; text: string }).text).toContain("Image omitted");
});
it("file processor omits image attachments when auto-resize cannot produce a safe image", async () => {
const imagePath = join(testDir, "test.png");
writeFileSync(imagePath, Buffer.from(TINY_PNG_BASE64, "base64"));
const result = await processFileArguments([imagePath]);
expect(result.images).toHaveLength(0);
expect(result.text).toContain("Image omitted");
});
});