Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
137
packages/coding-agent/test/tool-result-images.test.ts
Normal file
137
packages/coding-agent/test/tool-result-images.test.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { crc32, deflateSync } from "node:zlib";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ToolResultContent } from "../src/utils/tool-result-images.ts";
|
||||
import { normalizeToolResultImages } from "../src/utils/tool-result-images.ts";
|
||||
|
||||
const TINY_PNG_BASE64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
|
||||
|
||||
function pngChunk(type: string, body: Buffer): Buffer {
|
||||
const header = Buffer.alloc(8);
|
||||
header.writeUInt32BE(body.length, 0);
|
||||
header.write(type, 4, "ascii");
|
||||
const checksum = Buffer.alloc(4);
|
||||
checksum.writeUInt32BE(crc32(Buffer.concat([header.subarray(4), body])), 0);
|
||||
return Buffer.concat([header, body, checksum]);
|
||||
}
|
||||
|
||||
/** Build an 8-bit grayscale PNG of arbitrary dimensions without pulling in an encoder. */
|
||||
function createPng(width: number, height: number): Buffer {
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(width, 0);
|
||||
ihdr.writeUInt32BE(height, 4);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 0; // color type: grayscale
|
||||
const raw = Buffer.alloc((width + 1) * height);
|
||||
for (let row = 0; row < height; row++) {
|
||||
raw.fill(row % 256, row * (width + 1) + 1, (row + 1) * (width + 1));
|
||||
}
|
||||
return Buffer.concat([
|
||||
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
||||
pngChunk("IHDR", ihdr),
|
||||
pngChunk("IDAT", deflateSync(raw)),
|
||||
pngChunk("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
function readPngDimensions(base64Data: string): { width: number; height: number } {
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
||||
}
|
||||
|
||||
function createTinyBmp1x1Red24bpp(): Buffer {
|
||||
const buffer = Buffer.alloc(58);
|
||||
buffer.write("BM", 0, "ascii");
|
||||
buffer.writeUInt32LE(buffer.length, 2);
|
||||
buffer.writeUInt32LE(54, 10);
|
||||
buffer.writeUInt32LE(40, 14);
|
||||
buffer.writeInt32LE(1, 18);
|
||||
buffer.writeInt32LE(1, 22);
|
||||
buffer.writeUInt16LE(1, 26);
|
||||
buffer.writeUInt16LE(24, 28);
|
||||
buffer.writeUInt32LE(0, 30);
|
||||
buffer.writeUInt32LE(4, 34);
|
||||
buffer[56] = 0xff;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function imageBlock(bytes: Buffer, mimeType: string): ToolResultContent {
|
||||
return { type: "image", data: bytes.toString("base64"), mimeType };
|
||||
}
|
||||
|
||||
describe("normalizeToolResultImages", () => {
|
||||
it("returns the original array when there are no image blocks", async () => {
|
||||
const content: ToolResultContent[] = [{ type: "text", text: "no images here" }];
|
||||
|
||||
expect(await normalizeToolResultImages(content)).toBe(content);
|
||||
});
|
||||
|
||||
it("returns the original array when images are already within limits", async () => {
|
||||
const content: ToolResultContent[] = [
|
||||
{ type: "text", text: "screenshot" },
|
||||
{ type: "image", data: TINY_PNG_BASE64, mimeType: "image/png" },
|
||||
];
|
||||
|
||||
expect(await normalizeToolResultImages(content)).toBe(content);
|
||||
});
|
||||
|
||||
it("resizes oversized images and reports the original dimensions", async () => {
|
||||
const content: ToolResultContent[] = [imageBlock(createPng(2400, 4800), "image/png")];
|
||||
|
||||
const normalized = await normalizeToolResultImages(content);
|
||||
|
||||
expect(normalized).not.toBe(content);
|
||||
expect(normalized).toHaveLength(2);
|
||||
const image = normalized[0];
|
||||
expect(image.type).toBe("image");
|
||||
if (image.type !== "image") return;
|
||||
const { width, height } = readPngDimensions(image.data);
|
||||
expect(width).toBeLessThanOrEqual(2000);
|
||||
expect(height).toBeLessThanOrEqual(2000);
|
||||
const note = normalized[1];
|
||||
expect(note.type).toBe("text");
|
||||
if (note.type !== "text") return;
|
||||
expect(note.text).toContain("original 2400x4800");
|
||||
});
|
||||
|
||||
it("leaves oversized images alone when auto-resize is disabled", async () => {
|
||||
const content: ToolResultContent[] = [imageBlock(createPng(2400, 4800), "image/png")];
|
||||
|
||||
const normalized = await normalizeToolResultImages(content, { autoResizeImages: false });
|
||||
|
||||
expect(normalized).toBe(content);
|
||||
});
|
||||
|
||||
it("converts unsupported image formats even when auto-resize is disabled", async () => {
|
||||
const content: ToolResultContent[] = [imageBlock(createTinyBmp1x1Red24bpp(), "image/bmp")];
|
||||
|
||||
const normalized = await normalizeToolResultImages(content, { autoResizeImages: false });
|
||||
|
||||
expect(normalized).not.toBe(content);
|
||||
expect(normalized[0]).toMatchObject({ type: "image", mimeType: "image/png" });
|
||||
expect(normalized[1]).toMatchObject({
|
||||
type: "text",
|
||||
text: "[Image converted from image/bmp to image/png.]",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps undecodable images instead of dropping tool output", async () => {
|
||||
const content: ToolResultContent[] = [{ type: "image", data: "bm90LWFuLWltYWdl", mimeType: "image/png" }];
|
||||
|
||||
expect(await normalizeToolResultImages(content)).toBe(content);
|
||||
});
|
||||
|
||||
it("preserves surrounding text blocks and their order", async () => {
|
||||
const content: ToolResultContent[] = [
|
||||
{ type: "text", text: "before" },
|
||||
imageBlock(createPng(2400, 100), "image/png"),
|
||||
{ type: "text", text: "after" },
|
||||
];
|
||||
|
||||
const normalized = await normalizeToolResultImages(content);
|
||||
|
||||
expect(normalized.map((block) => block.type)).toEqual(["text", "image", "text", "text"]);
|
||||
expect(normalized[0]).toEqual({ type: "text", text: "before" });
|
||||
expect(normalized[3]).toEqual({ type: "text", text: "after" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user