import { describe, it, expect, vi, beforeEach } from "vitest"; import { getMimeTypeForImage, readImageAsBase64, convertImagesToContentBlocks, formatImagePathsForPrompt, } from "@/lib/image-handler.js"; import { pngBase64Fixture } from "../../fixtures/images.js"; import * as fs from "fs/promises"; vi.mock("fs/promises"); describe("image-handler.ts", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("getMimeTypeForImage", () => { it("should return correct MIME type for .jpg", () => { expect(getMimeTypeForImage("test.jpg")).toBe("image/jpeg"); expect(getMimeTypeForImage("/path/to/test.jpg")).toBe("image/jpeg"); }); it("should return correct MIME type for .jpeg", () => { expect(getMimeTypeForImage("test.jpeg")).toBe("image/jpeg"); }); it("should return correct MIME type for .png", () => { expect(getMimeTypeForImage("test.png")).toBe("image/png"); }); it("should return correct MIME type for .gif", () => { expect(getMimeTypeForImage("test.gif")).toBe("image/gif"); }); it("should return correct MIME type for .webp", () => { expect(getMimeTypeForImage("test.webp")).toBe("image/webp"); }); it("should be case-insensitive", () => { expect(getMimeTypeForImage("test.PNG")).toBe("image/png"); expect(getMimeTypeForImage("test.JPG")).toBe("image/jpeg"); expect(getMimeTypeForImage("test.GIF")).toBe("image/gif"); expect(getMimeTypeForImage("test.WEBP")).toBe("image/webp"); }); it("should default to image/png for unknown extensions", () => { expect(getMimeTypeForImage("test.unknown")).toBe("image/png"); expect(getMimeTypeForImage("test.txt")).toBe("image/png"); expect(getMimeTypeForImage("test")).toBe("image/png"); }); it("should handle paths with multiple dots", () => { expect(getMimeTypeForImage("my.image.file.jpg")).toBe("image/jpeg"); }); }); describe("readImageAsBase64", () => { it("should read image and return base64 data", async () => { const mockBuffer = Buffer.from(pngBase64Fixture, "base64"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); const result = await readImageAsBase64("/path/to/test.png"); expect(result).toMatchObject({ base64: pngBase64Fixture, mimeType: "image/png", filename: "test.png", originalPath: "/path/to/test.png", }); expect(fs.readFile).toHaveBeenCalledWith("/path/to/test.png"); }); it("should handle different image formats", async () => { const mockBuffer = Buffer.from("jpeg-data"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); const result = await readImageAsBase64("/path/to/photo.jpg"); expect(result.mimeType).toBe("image/jpeg"); expect(result.filename).toBe("photo.jpg"); expect(result.base64).toBe(mockBuffer.toString("base64")); }); it("should extract filename from path", async () => { const mockBuffer = Buffer.from("data"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); const result = await readImageAsBase64("/deep/nested/path/image.webp"); expect(result.filename).toBe("image.webp"); }); it("should throw error if file cannot be read", async () => { vi.mocked(fs.readFile).mockRejectedValue(new Error("File not found")); await expect(readImageAsBase64("/nonexistent.png")).rejects.toThrow( "File not found" ); }); }); describe("convertImagesToContentBlocks", () => { it("should convert single image to content block", async () => { const mockBuffer = Buffer.from(pngBase64Fixture, "base64"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); const result = await convertImagesToContentBlocks(["/path/test.png"]); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ type: "image", source: { type: "base64", media_type: "image/png", data: pngBase64Fixture, }, }); }); it("should convert multiple images to content blocks", async () => { const mockBuffer = Buffer.from("test-data"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); const result = await convertImagesToContentBlocks([ "/a.png", "/b.jpg", "/c.webp", ]); expect(result).toHaveLength(3); expect(result[0].source.media_type).toBe("image/png"); expect(result[1].source.media_type).toBe("image/jpeg"); expect(result[2].source.media_type).toBe("image/webp"); }); it("should resolve relative paths with workDir", async () => { const mockBuffer = Buffer.from("data"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); await convertImagesToContentBlocks(["relative.png"], "/work/dir"); // Use path-agnostic check since Windows uses backslashes const calls = vi.mocked(fs.readFile).mock.calls; expect(calls[0][0]).toMatch(/relative\.png$/); expect(calls[0][0]).toContain("work"); expect(calls[0][0]).toContain("dir"); }); it("should handle absolute paths without workDir", async () => { const mockBuffer = Buffer.from("data"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); await convertImagesToContentBlocks(["/absolute/path.png"]); expect(fs.readFile).toHaveBeenCalledWith("/absolute/path.png"); }); it("should continue processing on individual image errors", async () => { vi.mocked(fs.readFile) .mockResolvedValueOnce(Buffer.from("ok1")) .mockRejectedValueOnce(new Error("Failed")) .mockResolvedValueOnce(Buffer.from("ok2")); const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const result = await convertImagesToContentBlocks([ "/a.png", "/b.png", "/c.png", ]); expect(result).toHaveLength(2); // Only successful images expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); it("should return empty array for empty input", async () => { const result = await convertImagesToContentBlocks([]); expect(result).toEqual([]); }); it("should handle undefined workDir", async () => { const mockBuffer = Buffer.from("data"); vi.mocked(fs.readFile).mockResolvedValue(mockBuffer); const result = await convertImagesToContentBlocks(["/test.png"], undefined); expect(result).toHaveLength(1); expect(fs.readFile).toHaveBeenCalledWith("/test.png"); }); }); describe("formatImagePathsForPrompt", () => { it("should format single image path as bulleted list", () => { const result = formatImagePathsForPrompt(["/path/image.png"]); expect(result).toContain("\n\nAttached images:"); expect(result).toContain("- /path/image.png"); }); it("should format multiple image paths as bulleted list", () => { const result = formatImagePathsForPrompt([ "/path/a.png", "/path/b.jpg", "/path/c.webp", ]); expect(result).toContain("Attached images:"); expect(result).toContain("- /path/a.png"); expect(result).toContain("- /path/b.jpg"); expect(result).toContain("- /path/c.webp"); }); it("should return empty string for empty array", () => { const result = formatImagePathsForPrompt([]); expect(result).toBe(""); }); it("should start with double newline", () => { const result = formatImagePathsForPrompt(["/test.png"]); expect(result.startsWith("\n\n")).toBe(true); }); it("should handle paths with special characters", () => { const result = formatImagePathsForPrompt(["/path/with spaces/image.png"]); expect(result).toContain("- /path/with spaces/image.png"); }); }); });