diff --git a/package-lock.json b/package-lock.json index ca040c1d..9619c3ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2833,6 +2833,12 @@ "win32" ] }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, "node_modules/@sinclair/typebox": { "version": "0.34.47", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", @@ -8292,15 +8298,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/wasm-vips": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/wasm-vips/-/wasm-vips-0.0.16.tgz", - "integrity": "sha512-4/bEq8noAFt7DX3VT+Vt5AgNtnnOLwvmrDbduWfiv9AV+VYkbUU4f9Dam9e6khRqPinyClFHCqiwATTTJEiGwA==", - "license": "MIT", - "engines": { - "node": ">=16.4.0" - } - }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -8677,6 +8674,7 @@ "@mariozechner/pi-agent-core": "^0.45.7", "@mariozechner/pi-ai": "^0.45.7", "@mariozechner/pi-tui": "^0.45.7", + "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", @@ -8684,8 +8682,7 @@ "glob": "^11.0.3", "marked": "^15.0.12", "minimatch": "^10.1.1", - "proper-lockfile": "^4.1.2", - "wasm-vips": "^0.0.16" + "proper-lockfile": "^4.1.2" }, "bin": { "pi": "dist/cli.js" diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 2dc4a4d2..e80a0471 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -51,7 +51,7 @@ "marked": "^15.0.12", "minimatch": "^10.1.1", "proper-lockfile": "^4.1.2", - "wasm-vips": "^0.0.16" + "@silvia-odwyer/photon-node": "^0.3.4" }, "devDependencies": { "@types/diff": "^7.0.2", diff --git a/packages/coding-agent/src/utils/image-convert.ts b/packages/coding-agent/src/utils/image-convert.ts index c47a8509..590dcaf5 100644 --- a/packages/coding-agent/src/utils/image-convert.ts +++ b/packages/coding-agent/src/utils/image-convert.ts @@ -1,4 +1,4 @@ -import { getVips } from "./vips.js"; +import photon from "@silvia-odwyer/photon-node"; /** * Convert image to PNG format for terminal display. @@ -13,21 +13,17 @@ export async function convertToPng( return { data: base64Data, mimeType }; } - const vips = await getVips(); - if (!vips) { - // wasm-vips not available - return null; - } - try { - const buffer = Buffer.from(base64Data, "base64"); - const img = vips.Image.newFromBuffer(buffer); - const pngBuffer = img.writeToBuffer(".png"); - img.delete(); - return { - data: Buffer.from(pngBuffer).toString("base64"), - mimeType: "image/png", - }; + const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64"))); + try { + const pngBuffer = image.get_bytes(); + return { + data: Buffer.from(pngBuffer).toString("base64"), + mimeType: "image/png", + }; + } finally { + image.free(); + } } catch { // Conversion failed return null; diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index 968e7d22..7679dcc2 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -1,5 +1,5 @@ import type { ImageContent } from "@mariozechner/pi-ai"; -import { getVips } from "./vips.js"; +import photon from "@silvia-odwyer/photon-node"; export interface ImageResizeOptions { maxWidth?: number; // Default: 2000 @@ -40,8 +40,8 @@ function pickSmaller( * Resize an image to fit within the specified max dimensions and file size. * Returns the original image if it already fits within the limits. * - * Uses wasm-vips for image processing. If wasm-vips is not available (e.g., in some - * environments), returns the original image unchanged. + * Uses Photon (Rust/WASM) for image processing. If Photon is not available, + * returns the original image unchanged. * * Strategy for staying under maxBytes: * 1. First resize to maxWidth/maxHeight @@ -51,126 +51,74 @@ function pickSmaller( */ export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise { const opts = { ...DEFAULT_OPTIONS, ...options }; - const buffer = Buffer.from(img.data, "base64"); + const inputBuffer = Buffer.from(img.data, "base64"); - const vipsOrNull = await getVips(); - if (!vipsOrNull) { - // wasm-vips not available - return original image - // We can't get dimensions without vips, so return 0s - return { - data: img.data, - mimeType: img.mimeType, - originalWidth: 0, - originalHeight: 0, - width: 0, - height: 0, - wasResized: false, - }; - } - // Capture non-null reference for use in nested functions - const vips = vipsOrNull; - - // Load image to get metadata - let sourceImg: InstanceType; + let image: ReturnType | undefined; try { - sourceImg = vips.Image.newFromBuffer(buffer); - } catch { - // Failed to load image - return { - data: img.data, - mimeType: img.mimeType, - originalWidth: 0, - originalHeight: 0, - width: 0, - height: 0, - wasResized: false, - }; - } + image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer)); - const originalWidth = sourceImg.width; - const originalHeight = sourceImg.height; - - // Check if already within all limits (dimensions AND size) - const originalSize = buffer.length; - if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) { - sourceImg.delete(); + const originalWidth = image.get_width(); + const originalHeight = image.get_height(); const format = img.mimeType?.split("/")[1] ?? "png"; - return { - data: img.data, - mimeType: img.mimeType ?? `image/${format}`, - originalWidth, - originalHeight, - width: originalWidth, - height: originalHeight, - wasResized: false, - }; - } - // Calculate initial dimensions respecting max limits - let targetWidth = originalWidth; - let targetHeight = originalHeight; - - if (targetWidth > opts.maxWidth) { - targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth); - targetWidth = opts.maxWidth; - } - if (targetHeight > opts.maxHeight) { - targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight); - targetHeight = opts.maxHeight; - } - - // Helper to resize and encode in both formats, returning the smaller one - function tryBothFormats( - width: number, - height: number, - jpegQuality: number, - ): { buffer: Uint8Array; mimeType: string } { - // Load image fresh and resize using scale factor - // (Using newFromBuffer + resize instead of thumbnailBuffer to avoid lazy re-read issues) - const img = vips.Image.newFromBuffer(buffer); - const scale = Math.min(width / img.width, height / img.height); - const resized = scale < 1 ? img.resize(scale) : img; - - const pngBuffer = resized.writeToBuffer(".png"); - const jpegBuffer = resized.writeToBuffer(".jpg", { Q: jpegQuality }); - - if (resized !== img) { - resized.delete(); + // Check if already within all limits (dimensions AND size) + const originalSize = inputBuffer.length; + if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) { + return { + data: img.data, + mimeType: img.mimeType ?? `image/${format}`, + originalWidth, + originalHeight, + width: originalWidth, + height: originalHeight, + wasResized: false, + }; } - img.delete(); - return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" }); - } + // Calculate initial dimensions respecting max limits + let targetWidth = originalWidth; + let targetHeight = originalHeight; - // Clean up the source image - sourceImg.delete(); + if (targetWidth > opts.maxWidth) { + targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth); + targetWidth = opts.maxWidth; + } + if (targetHeight > opts.maxHeight) { + targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight); + targetHeight = opts.maxHeight; + } - // Try to produce an image under maxBytes - const qualitySteps = [85, 70, 55, 40]; - const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25]; + // Helper to resize and encode in both formats, returning the smaller one + function tryBothFormats( + width: number, + height: number, + jpegQuality: number, + ): { buffer: Uint8Array; mimeType: string } { + const resized = photon.resize(image!, width, height, photon.SamplingFilter.Lanczos3); - let best: { buffer: Uint8Array; mimeType: string }; - let finalWidth = targetWidth; - let finalHeight = targetHeight; + try { + const pngBuffer = resized.get_bytes(); + const jpegBuffer = resized.get_bytes_jpeg(jpegQuality); - // First attempt: resize to target dimensions, try both formats - best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality); + return pickSmaller( + { buffer: pngBuffer, mimeType: "image/png" }, + { buffer: jpegBuffer, mimeType: "image/jpeg" }, + ); + } finally { + resized.free(); + } + } - if (best.buffer.length <= opts.maxBytes) { - return { - data: Buffer.from(best.buffer).toString("base64"), - mimeType: best.mimeType, - originalWidth, - originalHeight, - width: finalWidth, - height: finalHeight, - wasResized: true, - }; - } + // Try to produce an image under maxBytes + const qualitySteps = [85, 70, 55, 40]; + const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25]; - // Still too large - try JPEG with decreasing quality (and compare to PNG each time) - for (const quality of qualitySteps) { - best = tryBothFormats(targetWidth, targetHeight, quality); + let best: { buffer: Uint8Array; mimeType: string }; + let finalWidth = targetWidth; + let finalHeight = targetHeight; + + // First attempt: resize to target dimensions, try both formats + best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality); if (best.buffer.length <= opts.maxBytes) { return { @@ -183,20 +131,10 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption wasResized: true, }; } - } - - // Still too large - reduce dimensions progressively - for (const scale of scaleSteps) { - finalWidth = Math.round(targetWidth * scale); - finalHeight = Math.round(targetHeight * scale); - - // Skip if dimensions are too small - if (finalWidth < 100 || finalHeight < 100) { - break; - } + // Still too large - try JPEG with decreasing quality for (const quality of qualitySteps) { - best = tryBothFormats(finalWidth, finalHeight, quality); + best = tryBothFormats(targetWidth, targetHeight, quality); if (best.buffer.length <= opts.maxBytes) { return { @@ -210,19 +148,59 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption }; } } - } - // Last resort: return smallest version we produced even if over limit - // (the API will reject it, but at least we tried everything) - return { - data: Buffer.from(best.buffer).toString("base64"), - mimeType: best.mimeType, - originalWidth, - originalHeight, - width: finalWidth, - height: finalHeight, - wasResized: true, - }; + // Still too large - reduce dimensions progressively + for (const scale of scaleSteps) { + finalWidth = Math.round(targetWidth * scale); + finalHeight = Math.round(targetHeight * scale); + + if (finalWidth < 100 || finalHeight < 100) { + break; + } + + for (const quality of qualitySteps) { + best = tryBothFormats(finalWidth, finalHeight, quality); + + if (best.buffer.length <= opts.maxBytes) { + return { + data: Buffer.from(best.buffer).toString("base64"), + mimeType: best.mimeType, + originalWidth, + originalHeight, + width: finalWidth, + height: finalHeight, + wasResized: true, + }; + } + } + } + + // Last resort: return smallest version we produced + return { + data: Buffer.from(best.buffer).toString("base64"), + mimeType: best.mimeType, + originalWidth, + originalHeight, + width: finalWidth, + height: finalHeight, + wasResized: true, + }; + } catch { + // Failed to load image + return { + data: img.data, + mimeType: img.mimeType, + originalWidth: 0, + originalHeight: 0, + width: 0, + height: 0, + wasResized: false, + }; + } finally { + if (image) { + image.free(); + } + } } /** diff --git a/packages/coding-agent/src/utils/vips.ts b/packages/coding-agent/src/utils/vips.ts deleted file mode 100644 index 1afabc9c..00000000 --- a/packages/coding-agent/src/utils/vips.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Singleton wrapper for wasm-vips initialization. - * wasm-vips requires async initialization, so we cache the instance. - */ - -import type Vips from "wasm-vips"; - -let vipsInstance: Awaited> | null = null; -let vipsInitPromise: Promise> | null> | null = null; - -/** - * Get the initialized wasm-vips instance. - * Returns null if wasm-vips is not available or fails to initialize. - */ -export async function getVips(): Promise> | null> { - if (vipsInstance) { - return vipsInstance; - } - - if (vipsInitPromise) { - return vipsInitPromise; - } - - vipsInitPromise = (async () => { - try { - const VipsInit = (await import("wasm-vips")).default; - vipsInstance = await VipsInit(); - return vipsInstance; - } catch { - // wasm-vips not available - return null; - } - })(); - - const result = await vipsInitPromise; - if (!result) { - vipsInitPromise = null; // Allow retry on failure - } - return result; -} diff --git a/packages/coding-agent/test/image-processing.test.ts b/packages/coding-agent/test/image-processing.test.ts index c2f4f4e3..18140a5e 100644 --- a/packages/coding-agent/test/image-processing.test.ts +++ b/packages/coding-agent/test/image-processing.test.ts @@ -1,39 +1,26 @@ /** - * Tests for image processing utilities using wasm-vips. + * Tests for image processing utilities using Photon. */ import { describe, expect, it } from "vitest"; import { convertToPng } from "../src/utils/image-convert.js"; import { formatDimensionNote, resizeImage } from "../src/utils/image-resize.js"; -import { getVips } from "../src/utils/vips.js"; -// Small 2x2 red PNG image (base64) -const TINY_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAADklEQVQI12P4z8DAwMAAAA0BA/m5sb9AAAAAAElFTkSuQmCC"; +// Small 2x2 red PNG image (base64) - generated with ImageMagick +const TINY_PNG = + "iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAQMAAABIeJ9nAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURf8AAP///0EdNBEAAAABYktHRAH/Ai3eAAAAB3RJTUUH6gEOADM5Ddoh/wAAAAxJREFUCNdjYGBgAAAABAABJzQnCgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNi0wMS0xNFQwMDo1MTo1NyswMDowMOnKzHgAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjYtMDEtMTRUMDA6NTE6NTcrMDA6MDCYl3TEAAAAKHRFWHRkYXRlOnRpbWVzdGFtcAAyMDI2LTAxLTE0VDAwOjUxOjU3KzAwOjAwz4JVGwAAAABJRU5ErkJggg=="; -// Small 2x2 blue JPEG image (base64) +// Small 2x2 blue JPEG image (base64) - generated with ImageMagick const TINY_JPEG = - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAACAAIDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAn/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwAB//2Q=="; + "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAACAAIDAREAAhEBAxEB/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAVAQEBAAAAAAAAAAAAAAAAAAAGCf/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AD3VTB3/2Q=="; -// 100x100 gray PNG (generated with wasm-vips) +// 100x100 gray PNG const MEDIUM_PNG_100x100 = - "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAtGVYSWZJSSoACAAAAAYAEgEDAAEAAAABAAAAGgEFAAEAAABWAAAAGwEFAAEAAABeAAAAKAEDAAEAAAACAAAAEwIDAAEAAAABAAAAaYcEAAEAAABmAAAAAAAAADhjAADoAwAAOGMAAOgDAAAGAACQBwAEAAAAMDIxMAGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA//8AAAKgBAABAAAAZAAAAAOgBAABAAAAZAAAAAAAAAC1xMTxAAAA4klEQVR4nO3QoQEAAAiAME/3dF+QvmUSs7zNP8WswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKz9zzpHfptnWvrkoQAAAABJRU5ErkJggg=="; + "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAAAAABVicqIAAAAAmJLR0QA/4ePzL8AAAAHdElNRQfqAQ4AMzkN2iH/AAAAP0lEQVRo3u3NQQEAAAQEMASXXYrz2gqst/Lm4ZBIJBKJRCKRSCQSiUQikUgkEolEIpFIJBKJRCKRSCQSiSTsAP1cAUZeKtreAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI2LTAxLTE0VDAwOjUxOjU3KzAwOjAw6crMeAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNi0wMS0xNFQwMDo1MTo1NyswMDowMJiXdMQAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjYtMDEtMTRUMDA6NTE6NTcrMDA6MDDPglUbAAAAAElFTkSuQmCC"; -// 200x200 colored PNG (generated with wasm-vips) +// 200x200 colored PNG const LARGE_PNG_200x200 = - "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAtGVYSWZJSSoACAAAAAYAEgEDAAEAAAABAAAAGgEFAAEAAABWAAAAGwEFAAEAAABeAAAAKAEDAAEAAAACAAAAEwIDAAEAAAABAAAAaYcEAAEAAABmAAAAAAAAADhjAADoAwAAOGMAAOgDAAAGAACQBwAEAAAAMDIxMAGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA//8AAAKgBAABAAAAyAAAAAOgBAABAAAAyAAAAAAAAADqHRv+AAAD8UlEQVR4nO2UAQnAQACEFtZMy/SxVmJDdggmOOUu7hMtwNsZXG3aAnxwLoVVWKewiuD85V97LN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN8BixSW74BFCst3wCKF5TtgkcLyHbBIYfkOWKSwfAcsUli+AxYpLN/BJIXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5YpLB8ByxSWL4DFiks3wGLFJbvgEUKy3fAIoXlO2CRwvIdsEhh+Q5Y5AHNA7iPx5BmcQAAAABJRU5ErkJggg=="; - -describe("wasm-vips initialization", () => { - it("should initialize wasm-vips successfully", async () => { - const vips = await getVips(); - expect(vips).not.toBeNull(); - }); - - it("should return cached instance on subsequent calls", async () => { - const vips1 = await getVips(); - const vips2 = await getVips(); - expect(vips1).toBe(vips2); - }); -}); + "iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAIAAAByquWKAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGYktHRP///////wlY99wAAAAHdElNRQfqAQ4AMzqU03BFAAAEEElEQVR42u3dvWqUURSF4XXMRAyIhSAWQvwnWtjaWusFx9bW1kKD/wELESxEiBjDjJdwysOC57mCXW32+zGcGbtdACpsdi+T8Xz1GABzm91xMl6sHgNgblzcT/Y+rh4DYG6cv0/2H64eA2BOEgI1fHQHaoyzbXIwVo8BMCcJgRqSEKgxft9Lrn5aPQbA3Ph1klw7Wj0GwJwkBGpYWECN8fMiub63egyAORcWUMPCAmqMH3eTG59XjwEwN76/S24+Wj0GwJwkBGpYWECN8e1fcmuzegyAORcWUMPCAmqM09vJ4dfVYwDMjS9vkzuPV48BMCcJgRqeSAZqjA/nyYP91WMAzElCoIYkBGqMk8Pk6HT1GABzm+3L5JIkBApIQqCGj+5AjfHmb/Lk8uoxAOYkIVBDEgI1LCygxmZ7nFyShEABFxZQw8ICaozXf5KnV1aPATDnwgJqWFhADQsLqOFnDUANFxZQw8ICaoxXZ8mzg9VjAMy5sIAaFhZQw3tYQA1/QgHUkIRADUkI1JCEQA1JCNSQhEANFxZQwzcsoIYkBGpIQqCGJARqSEKghiQEalhYQA1/QgHUcGEBNSwsoIYkBGq4sIAaFhZQw8ICaviGBdRwYQE1LCyghiQEariwgBoWFlDDe1hADS+OAjUkIVBDEgI1JCFQQxICNSQhUMOFBdTwDQuoIQmBGpIQqCEJgRqSEKghCYEaFhZQw4ujQA0XFlDDwgJqSEKghgsLqGFhATUsLKCGb1hADRcWUMPCAmpIQqCGCwuoYWEBNbyHBdTw4ihQQxICNSQhUEMSAjUkIVBDEgI1XFhADd+wgBqSEKghCYEakhCoIQmBGpIQqGFhATW8OArUcGEBNSwsoIYkBGq4sIAaFhZQw8ICaviGBdRwYQE1LCyghiQEariwgBoWFlDDe1hADS+OAjUkIVBDEgI1JCFQQxICNSQhUMOFBdTwDQuoIQmBGpIQqCEJgRqSEKghCYEa49lB8ups9RgAc14cBWpIQqCGhQXUkIRADRcWUMPCAmqMp1eS139WjwEw5xsWUEMSAjUsLKCGJARquLCAGhYWUGM8uZy8+bt6DIA5L44CNSQhUMMTyUCNcXSanByuHgNgThICNSQhUGM82E8+nK8eA2DOzxqAGpIQqDHuPE6+vF09BsDcOPyanN5ePQbAnCQEavgdFlBj3Nok3/6tHgNgzoujQA1JCNQYNx8l39+tHgNgbtz4nPy4u3oMgDlJCNSwsIAa4/pe8vNi9RgAc37WANSQhECNce0o+XWyegyAuXH1U/L73uoxAOYkIVDDwgJqjIORnG1XjwEw508ogBqSEKgx9h8m5+9XjwEwN/Y+Jhf3V48BMCcJgRpjPE+2x6vHAJgbSbLbrR4DYO4/GqiSgXN+ksgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjYtMDEtMTRUMDA6NTE6NTcrMDA6MDDpysx4AAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI2LTAxLTE0VDAwOjUxOjU3KzAwOjAwmJd0xAAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNi0wMS0xNFQwMDo1MTo1NyswMDowMM+CVRsAAAAASUVORK5CYII="; describe("convertToPng", () => { it("should return original data for PNG input", async () => { diff --git a/packages/coding-agent/vitest.config.ts b/packages/coding-agent/vitest.config.ts index a6febf2f..eaf37211 100644 --- a/packages/coding-agent/vitest.config.ts +++ b/packages/coding-agent/vitest.config.ts @@ -5,5 +5,10 @@ export default defineConfig({ globals: true, environment: 'node', testTimeout: 30000, // 30 seconds for API calls - } + server: { + deps: { + external: [/@silvia-odwyer\/photon-node/], + }, + }, + }, });