import { describe, it, expect, beforeEach, vi } from "vitest";
import { imageToolHandler } from "../../../src/tools/image";
import { buildSwiftCliArgs, resolveImagePath } from "../../../src/utils/image-cli-args";
import { executeSwiftCli, readImageAsBase64 } from "../../../src/utils/peepit-cli";
import { mockSwiftCli } from "../../mocks/peepit-cli.mock";
import { pino } from "pino";
import { ImageInput } from "../../../src/types";
import * as fs from "fs/promises";
import * as path from "path";
// Mock the Swift CLI utility
vi.mock("../../../src/utils/peepit-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(),
};
});
// Mock image-analysis module
vi.mock("../../../src/utils/image-analysis", () => ({
performAutomaticAnalysis: vi.fn(),
}));
// Mock AI providers
vi.mock("../../../src/utils/ai-providers", () => ({
parseAIProviders: vi.fn(),
analyzeImageWithProvider: vi.fn(),
}));
import { performAutomaticAnalysis } from "../../../src/utils/image-analysis";
import { parseAIProviders } from "../../../src/utils/ai-providers";
const mockExecuteSwiftCli = executeSwiftCli as vi.MockedFunction<typeof executeSwiftCli>;
const mockReadImageAsBase64 = readImageAsBase64 as vi.MockedFunction<typeof readImageAsBase64>;
const mockPerformAutomaticAnalysis = performAutomaticAnalysis as vi.MockedFunction<typeof performAutomaticAnalysis>;
const mockParseAIProviders = parseAIProviders as vi.MockedFunction<typeof parseAIProviders>;
const mockFsRm = fs.rm as vi.MockedFunction<typeof fs.rm>;
const mockResolveImagePath = resolveImagePath as vi.MockedFunction<typeof resolveImagePath>;
const mockLogger = pino({ level: "silent" });
const mockContext = { logger: mockLogger };
const MOCK_TEMP_DIR = "/tmp";
const MOCK_TEMP_IMAGE_DIR = "/tmp/peepit-img-XXXXXX";
const MOCK_SAVED_FILE_PATH = "/tmp/peepit-img-XXXXXX/capture.png";
describe("Image Tool - Edge Cases", () => {
beforeEach(() => {
vi.clearAllMocks();
mockFsRm.mockResolvedValue(undefined);
process.env.PEEPIT_AI_PROVIDERS = "";
});
describe("Whitespace trimming in app_target", () => {
it("should trim leading and trailing whitespace from app_target", async () => {
mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_IMAGE_DIR,
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
});
const mockResponse = mockSwiftCli.captureImage("Spotify", {
path: MOCK_SAVED_FILE_PATH,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ app_target: " Spotify " },
mockContext,
);
// Check that the Swift CLI was called with trimmed app name
const callArgs = mockExecuteSwiftCli.mock.calls[0][0];
const appIndex = callArgs.indexOf("--app");
expect(callArgs[appIndex + 1]).toBe("Spotify"); // Should be trimmed
});
it("should trim whitespace in window specifier format", async () => {
mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_IMAGE_DIR,
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
});
const mockResponse = mockSwiftCli.captureImage("Safari", {});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ app_target: " Safari :WINDOW_TITLE:Apple" },
mockContext,
);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining([
"--app", "Safari",
"--mode", "window",
"--window-title", "Apple"
]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
});
describe("Format parameter case-insensitivity and aliases", () => {
it("should handle uppercase PNG format", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
mockResolveImagePath.mockResolvedValue({
effectivePath: "/tmp/test.png",
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: "/tmp/test.png",
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
const parsedInput = imageToolSchema.parse({ format: "PNG", path: "/tmp/test.png" });
await imageToolHandler(parsedInput, mockContext);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--format", "png"]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should handle mixed case JPG format", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
mockResolveImagePath.mockResolvedValue({
effectivePath: "/tmp/test.jpg",
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: "/tmp/test.jpg",
format: "jpg",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
const parsedInput = imageToolSchema.parse({ format: "JpG", path: "/tmp/test.jpg" });
await imageToolHandler(parsedInput, mockContext);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--format", "jpg"]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should handle 'jpeg' as alias for 'jpg'", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
mockResolveImagePath.mockResolvedValue({
effectivePath: "/tmp/test.jpg",
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: "/tmp/test.jpg",
format: "jpg",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
const parsedInput = imageToolSchema.parse({ format: "jpeg", path: "/tmp/test.jpg" });
await imageToolHandler(parsedInput, mockContext);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--format", "jpg"]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should handle uppercase 'JPEG' alias", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
mockResolveImagePath.mockResolvedValue({
effectivePath: "/tmp/test.jpg",
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: "/tmp/test.jpg",
format: "jpg",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
const parsedInput = imageToolSchema.parse({ format: "JPEG", path: "/tmp/test.jpg" });
await imageToolHandler(parsedInput, mockContext);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--format", "jpg"]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should handle 'DATA' in uppercase", async () => {
const { imageToolSchema } = await import("../../../src/types/index.js");
mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_IMAGE_DIR,
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
});
const mockResponse = mockSwiftCli.captureImage("Safari", {
path: MOCK_SAVED_FILE_PATH,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
mockReadImageAsBase64.mockResolvedValue("base64data");
const parsedInput = imageToolSchema.parse({ format: "DATA", app_target: "Safari" });
await imageToolHandler(parsedInput, mockContext);
// Should be processed as 'data' format
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--format", "png"]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
});
describe("Empty question to analyze", () => {
it("should skip analysis for empty string question", async () => {
mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_IMAGE_DIR,
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: MOCK_SAVED_FILE_PATH,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
const result = await imageToolHandler(
{ question: "" },
mockContext,
);
// Empty question is falsy, so analysis is skipped
expect(mockPerformAutomaticAnalysis).not.toHaveBeenCalled();
// No analysis should be performed
expect(result.analysis_text).toBeUndefined();
expect(result.model_used).toBeUndefined();
// Should just capture the image
expect(result.saved_files).toEqual(mockResponse.data.saved_files);
});
it("should handle whitespace-only question", async () => {
mockResolveImagePath.mockResolvedValue({
effectivePath: MOCK_TEMP_IMAGE_DIR,
tempDirUsed: MOCK_TEMP_IMAGE_DIR,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: MOCK_SAVED_FILE_PATH,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
mockReadImageAsBase64.mockResolvedValue("base64data");
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockPerformAutomaticAnalysis.mockResolvedValue({
analysisText: "No response from Ollama",
modelUsed: "ollama/llava:latest",
});
process.env.PEEPIT_AI_PROVIDERS = "ollama/llava:latest";
const result = await imageToolHandler(
{ question: " " },
mockContext,
);
expect(mockPerformAutomaticAnalysis).toHaveBeenCalledWith(
"base64data",
" ",
mockLogger,
"ollama/llava:latest",
);
expect(result.analysis_text).toBe("No response from Ollama");
});
});
describe("Screen index parsing edge cases", () => {
it("should handle float screen indices by parsing as integer", async () => {
const args = buildSwiftCliArgs({ app_target: "screen:1.5" }, undefined, undefined, mockLogger);
// Should parse 1.5 as 1
expect(args).toEqual(
expect.arrayContaining(["--mode", "screen", "--screen-index", "1"]),
);
});
it("should handle hex screen indices as 0", async () => {
const args = buildSwiftCliArgs({ app_target: "screen:0x1" }, undefined, undefined, mockLogger);
// parseInt("0x1", 10) returns 0, so it's actually valid and parsed as screen 0
expect(args).toEqual(
expect.arrayContaining(["--mode", "screen", "--screen-index", "0"]),
);
});
it("should handle negative screen indices as invalid", async () => {
const loggerWarnSpy = vi.spyOn(mockLogger, "warn");
const args = buildSwiftCliArgs({ app_target: "screen:-1" }, undefined, undefined, mockLogger);
expect(args).toEqual(
expect.arrayContaining(["--mode", "screen"]),
);
expect(args).not.toContain("--screen-index");
expect(loggerWarnSpy).toHaveBeenCalledWith(
expect.objectContaining({ screenIndex: "-1" }),
"Invalid screen index '-1' in app_target, capturing all screens.",
);
});
it("should handle very large screen indices", async () => {
const args = buildSwiftCliArgs({ app_target: "screen:999999" }, undefined, undefined, mockLogger);
// Large numbers should be passed through
expect(args).toEqual(
expect.arrayContaining(["--mode", "screen", "--screen-index", "999999"]),
);
});
it("should handle screen index with leading zeros", async () => {
const args = buildSwiftCliArgs({ app_target: "screen:001" }, undefined, undefined, mockLogger);
// Should parse 001 as 1
expect(args).toEqual(
expect.arrayContaining(["--mode", "screen", "--screen-index", "1"]),
);
});
});
describe("Special filesystem characters in filenames", () => {
it("should allow pipe character in filename", async () => {
const pathWithPipe = "/tmp/test|file.png";
mockResolveImagePath.mockResolvedValue({
effectivePath: pathWithPipe,
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: pathWithPipe,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ path: pathWithPipe },
mockContext,
);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--path", pathWithPipe]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should allow colon character in filename", async () => {
const pathWithColon = "/tmp/test:file.png";
mockResolveImagePath.mockResolvedValue({
effectivePath: pathWithColon,
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: pathWithColon,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ path: pathWithColon },
mockContext,
);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--path", pathWithColon]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should allow asterisk character in filename", async () => {
const pathWithAsterisk = "/tmp/test*file.png";
mockResolveImagePath.mockResolvedValue({
effectivePath: pathWithAsterisk,
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: pathWithAsterisk,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ path: pathWithAsterisk },
mockContext,
);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--path", pathWithAsterisk]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should handle multiple special characters in filename", async () => {
const complexPath = "/tmp/test|file:with*special.png";
mockResolveImagePath.mockResolvedValue({
effectivePath: complexPath,
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: complexPath,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ path: complexPath },
mockContext,
);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--path", complexPath]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
it("should handle spaces in path", async () => {
const pathWithSpaces = "/tmp/my folder/test file.png";
mockResolveImagePath.mockResolvedValue({
effectivePath: pathWithSpaces,
tempDirUsed: undefined,
});
const mockResponse = mockSwiftCli.captureImage("screen", {
path: pathWithSpaces,
format: "png",
});
mockExecuteSwiftCli.mockResolvedValue(mockResponse);
await imageToolHandler(
{ path: pathWithSpaces },
mockContext,
);
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
expect.arrayContaining(["--path", pathWithSpaces]),
mockLogger,
expect.objectContaining({ timeout: expect.any(Number) })
);
});
});
describe("buildSwiftCliArgs edge cases", () => {
it("should handle window title with colons", async () => {
const args = buildSwiftCliArgs({
app_target: "Chrome:WINDOW_TITLE:https://example.com:8080"
}, undefined);
expect(args).toEqual(
expect.arrayContaining([
"--app", "Chrome",
"--mode", "window",
"--window-title", "https://example.com:8080"
]),
);
});
it("should handle malformed window specifier", async () => {
const loggerWarnSpy = vi.spyOn(mockLogger, "warn");
const args = buildSwiftCliArgs({
app_target: "Safari:InvalidSpecifier"
}, undefined, undefined, mockLogger);
expect(args).toEqual(
expect.arrayContaining([
"--app", "Safari:InvalidSpecifier",
"--mode", "multi"
]),
);
expect(loggerWarnSpy).toHaveBeenCalledWith(
expect.objectContaining({ app_target: "Safari:InvalidSpecifier" }),
"Malformed window specifier, treating as app name",
);
});
it("should handle unknown window specifier type", async () => {
const loggerWarnSpy = vi.spyOn(mockLogger, "warn");
const args = buildSwiftCliArgs({
app_target: "Safari:UNKNOWN_TYPE:value"
}, undefined, undefined, mockLogger);
expect(args).toEqual(
expect.arrayContaining([
"--app", "Safari",
"--mode", "window"
]),
);
expect(loggerWarnSpy).toHaveBeenCalledWith(
expect.objectContaining({ specifierType: "UNKNOWN_TYPE" }),
"Unknown window specifier type, defaulting to main window",
);
});
});
});