Files
automaker/apps/server/tests/unit/lib/image-handler.test.ts
Kacper dd58b70730 fix: resolve critical package issues and update imports
CRITICAL FIXES:
- Fix dependency-resolver ES module failure by reverting to CommonJS
  - Removed "type": "module" from package.json
  - Changed tsconfig.json module from "ESNext" to "commonjs"
  - Added exports field for better module resolution
  - Package now works correctly at runtime

- Fix Feature type incompatibility between server and UI
  - Added FeatureImagePath interface to @automaker/types
  - Made imagePaths property accept multiple formats
  - Added index signature for backward compatibility

HIGH PRIORITY FIXES:
- Remove duplicate model-resolver.ts from apps/server/src/lib/
  - Update sdk-options.ts to import from @automaker/model-resolver
  - Use @automaker/types for CLAUDE_MODEL_MAP and DEFAULT_MODELS

- Remove duplicate session types from apps/ui/src/types/
  - Deleted identical session.ts file
  - Use @automaker/types for session type definitions

- Update source file Feature imports
  - Fix create.ts and update.ts to import Feature from @automaker/types
  - Separate Feature type import from FeatureLoader class import

MEDIUM PRIORITY FIXES:
- Remove unused imports
  - Remove unused AbortError from agent-service.ts
  - Remove unused MessageSquare icon from kanban-card.tsx
  - Consolidate duplicate React imports in hotkey-button.tsx

- Update test file imports to use @automaker/* packages
  - Update 12 test files to import from @automaker/utils
  - Update 2 test files to import from @automaker/platform
  - Update 1 test file to import from @automaker/model-resolver
  - Update dependency-resolver.test.ts imports
  - Update providers/types imports to @automaker/types

VERIFICATION:
- Server builds successfully ✓
- All 6 shared packages build correctly ✓
- Test imports updated and verified ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 00:16:00 +01:00

232 lines
7.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import {
getMimeTypeForImage,
readImageAsBase64,
convertImagesToContentBlocks,
formatImagePathsForPrompt,
} from "@automaker/utils";
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");
});
});
});