Peekaboo/tests/unit/tools/invalid-format.test.ts
Peter Steinberger ab882069b4 fix: Add defensive validation for invalid image formats with automatic PNG fallback
Implements robust handling for invalid image formats (like 'bmp', 'gif', 'webp') that bypass schema validation:

- Added defensive format validation in image tool handler
- Automatic path correction to ensure file extensions match actual format used
- Warning messages in response when format fallback occurs
- Comprehensive unit and integration test coverage for edge cases

This ensures invalid formats automatically fall back to PNG as requested, preventing
Swift CLI rejection and incorrect file extensions in output paths.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-08 07:44:17 +01:00

136 lines
No EOL
4.6 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from "vitest";
import { imageToolHandler } from "../../../src/tools/image";
import { executeSwiftCli, readImageAsBase64 } from "../../../src/utils/peekaboo-cli";
import { resolveImagePath } from "../../../src/utils/image-cli-args";
import { mockSwiftCli } from "../../mocks/peekaboo-cli.mock";
import { pino } from "pino";
// Mock the Swift CLI utility
vi.mock("../../../src/utils/peekaboo-cli");
// Mock fs/promises
vi.mock("fs/promises");
// Mock image-cli-args module
vi.mock("../../../src/utils/image-cli-args", async () => {
const actual = await vi.importActual("../../../src/utils/image-cli-args");
return {
...actual,
resolveImagePath: vi.fn(),
};
});
const mockExecuteSwiftCli = executeSwiftCli as vi.MockedFunction<
typeof executeSwiftCli
>;
const mockResolveImagePath = resolveImagePath as vi.MockedFunction<typeof resolveImagePath>;
const mockLogger = pino({ level: "silent" });
const mockContext = { logger: mockLogger };
const MOCK_TEMP_DIR = "/tmp/peekaboo-img-XXXXXX";
const MOCK_SAVED_FILE_PATH = "/tmp/test_invalid_format.png";
describe("Invalid Format Handling", () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock resolveImagePath to return a temp directory
mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_DIR,
tempDirUsed: MOCK_TEMP_DIR,
});
});
it("should fallback invalid format 'bmp' to 'png' and use correct extension in filename", async () => {
// Import schema to test preprocessing
const { imageToolSchema } = await import("../../../src/types/index.js");
// Mock Swift CLI response with PNG format (fallback)
const mockResponse = mockSwiftCli.captureImage("screen", {
path: MOCK_SAVED_FILE_PATH,
format: "png",
});
// Ensure the saved file has .png extension, not .bmp
const savedFileWithCorrectExtension = {
...mockResponse.data.saved_files[0],
path: "/tmp/test_invalid_format.png", // Should be .png, not .bmp
};
const correctedResponse = {
...mockResponse,
data: {
...mockResponse.data,
saved_files: [savedFileWithCorrectExtension],
},
};
mockExecuteSwiftCli.mockResolvedValue(correctedResponse);
// Test with invalid format 'bmp' - schema should preprocess to 'png'
const parsedInput = imageToolSchema.parse({
format: "bmp",
path: "/tmp/test_invalid_format.bmp"
});
// Validate that schema preprocessing worked
expect(parsedInput.format).toBe("png");
const result = await imageToolHandler(parsedInput, mockContext);
expect(result.isError).toBeUndefined();
// Should have called Swift CLI with PNG format, not BMP
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--format", "png"]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) }),
);
// The saved file should have .png extension
expect(result.saved_files?.[0]?.path).toBe("/tmp/test_invalid_format.png");
expect(result.saved_files?.[0]?.path).not.toContain(".bmp");
// The result should not contain .bmp anywhere
const resultText = result.content[0]?.text || "";
expect(resultText).not.toContain(".bmp");
expect(resultText).toContain(".png");
});
it("should handle other invalid formats correctly", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
const invalidFormats = ["gif", "webp", "tiff", "xyz", "bmp"];
for (const invalidFormat of invalidFormats) {
const parsedInput = imageToolSchema.parse({ format: invalidFormat });
// All invalid formats should be preprocessed to 'png'
expect(parsedInput.format).toBe("png");
}
// Empty string should become undefined (which will use default)
const emptyInput = imageToolSchema.parse({ format: "" });
expect(emptyInput.format).toBeUndefined();
});
it("should preserve valid formats", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
const validFormats = [
{ input: "png", expected: "png" },
{ input: "PNG", expected: "png" }, // case insensitive
{ input: "jpg", expected: "jpg" },
{ input: "JPG", expected: "jpg" }, // case insensitive
{ input: "jpeg", expected: "jpg" }, // alias
{ input: "JPEG", expected: "jpg" }, // alias + case insensitive
{ input: "data", expected: "data" },
];
for (const { input, expected } of validFormats) {
const parsedInput = imageToolSchema.parse({ format: input });
expect(parsedInput.format).toBe(expected);
}
});
});