test: Add comprehensive edge case tests for image and analyze tools

Added tests for:
- Whitespace trimming in app_target parameter
- Format parameter case-insensitivity and aliases
- Empty question handling (skips analysis for empty strings)
- Screen index parsing edge cases (float, hex, negative values)
- Special filesystem characters in filenames (|, :, *)
- Analyze tool edge cases (empty questions, error handling)
- Provider configuration edge cases
- Very long questions and special characters in responses

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2025-06-08 06:50:10 +01:00
parent e74796f7e3
commit 94060963d0
2 changed files with 965 additions and 0 deletions

View file

@ -0,0 +1,442 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { analyzeToolHandler } from "../../../src/tools/analyze";
import { readImageAsBase64 } from "../../../src/utils/peekaboo-cli";
import {
parseAIProviders,
analyzeImageWithProvider,
determineProviderAndModel
} from "../../../src/utils/ai-providers";
import { pino } from "pino";
import * as fs from "fs/promises";
import * as path from "path";
// Mock peekaboo-cli
vi.mock("../../../src/utils/peekaboo-cli", () => ({
readImageAsBase64: vi.fn(),
}));
// Mock AI providers
vi.mock("../../../src/utils/ai-providers", () => ({
parseAIProviders: vi.fn(),
analyzeImageWithProvider: vi.fn(),
determineProviderAndModel: vi.fn(),
}));
const mockReadImageAsBase64 = readImageAsBase64 as vi.MockedFunction<typeof readImageAsBase64>;
const mockParseAIProviders = parseAIProviders as vi.MockedFunction<typeof parseAIProviders>;
const mockAnalyzeImageWithProvider = analyzeImageWithProvider as vi.MockedFunction<typeof analyzeImageWithProvider>;
const mockDetermineProviderAndModel = determineProviderAndModel as vi.MockedFunction<typeof determineProviderAndModel>;
const mockLogger = pino({ level: "silent" });
const mockContext = { logger: mockLogger };
describe("Analyze Tool - Edge Cases", () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.PEEKABOO_AI_PROVIDERS = "ollama/llava:latest";
});
describe("Empty question handling", () => {
it("should handle empty string question", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
// Mock Ollama returning "No response from Ollama" for empty question
mockAnalyzeImageWithProvider.mockResolvedValue("No response from Ollama");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: ""
},
mockContext,
);
expect(mockAnalyzeImageWithProvider).toHaveBeenCalledWith(
{ provider: "ollama", model: "llava:latest" },
"/tmp/test.png",
"base64imagedata",
"",
mockLogger,
);
expect(result.content[0].type).toBe("text");
expect(result.content[0].text).toBe("No response from Ollama");
expect(result.model_used).toBe("ollama/llava:latest");
});
it("should handle whitespace-only question", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue("No response from Ollama");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: " "
},
mockContext,
);
expect(mockAnalyzeImageWithProvider).toHaveBeenCalledWith(
{ provider: "ollama", model: "llava:latest" },
"/tmp/test.png",
"base64imagedata",
" ",
mockLogger,
);
expect(result.content[0].text).toBe("No response from Ollama");
});
it("should handle question with only newlines", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue("No response from Ollama");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "\n\n\n"
},
mockContext,
);
expect(mockAnalyzeImageWithProvider).toHaveBeenCalledWith(
{ provider: "ollama", model: "llava:latest" },
"/tmp/test.png",
"base64imagedata",
"\n\n\n",
mockLogger,
);
expect(result.content[0].text).toBe("No response from Ollama");
});
});
describe("Model provider edge cases", () => {
it("should handle when no AI providers are configured", async () => {
process.env.PEEKABOO_AI_PROVIDERS = "";
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe(
"AI analysis not configured on this server. Set the PEEKABOO_AI_PROVIDERS environment variable."
);
expect(result.isError).toBe(true);
});
it("should handle whitespace in PEEKABOO_AI_PROVIDERS", async () => {
process.env.PEEKABOO_AI_PROVIDERS = " ";
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe(
"AI analysis not configured on this server. Set the PEEKABOO_AI_PROVIDERS environment variable."
);
expect(result.isError).toBe(true);
});
it("should handle when no providers are operational", async () => {
process.env.PEEKABOO_AI_PROVIDERS = "ollama/llava:latest";
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
// No provider is operational
mockDetermineProviderAndModel.mockResolvedValue({
provider: null,
model: null
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe(
"No configured AI providers are currently operational."
);
expect(result.isError).toBe(true);
});
});
describe("File handling edge cases", () => {
it("should reject unsupported image formats", async () => {
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.txt",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe(
"Unsupported image format: .txt. Supported formats: .png, .jpg, .jpeg, .webp"
);
expect(result.isError).toBe(true);
});
it("should handle file read errors", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockRejectedValue(new Error("File not found"));
const result = await analyzeToolHandler(
{
image_path: "/tmp/nonexistent.png",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe(
"Failed to read image file: File not found"
);
expect(result.isError).toBe(true);
});
it("should handle the silent fallback 'path' parameter", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue("This is an image");
const result = await analyzeToolHandler(
{
path: "/tmp/test.png", // Using path instead of image_path
question: "What is this?"
},
mockContext,
);
expect(mockReadImageAsBase64).toHaveBeenCalledWith("/tmp/test.png");
expect(result.content[0].text).toBe("This is an image");
});
});
describe("Very long questions", () => {
it("should handle extremely long questions", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
const veryLongQuestion = "A".repeat(10000); // 10k character question
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue("Analysis of the image");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: veryLongQuestion
},
mockContext,
);
expect(mockAnalyzeImageWithProvider).toHaveBeenCalledWith(
{ provider: "ollama", model: "llava:latest" },
"/tmp/test.png",
"base64imagedata",
veryLongQuestion,
mockLogger,
);
expect(result.content[0].text).toBe("Analysis of the image");
});
});
describe("Special characters in responses", () => {
it("should handle responses with special formatting characters", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue(
"This image contains:\n- Item 1\n- Item 2\n\nWith **bold** and *italic* text"
);
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What's in the image?"
},
mockContext,
);
expect(result.content[0].text).toBe(
"This image contains:\n- Item 1\n- Item 2\n\nWith **bold** and *italic* text"
);
});
it("should handle responses with unicode characters", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue("This image contains: 🐱 猫 القط");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What's in the image?"
},
mockContext,
);
expect(result.content[0].text).toBe("This image contains: 🐱 猫 القط");
});
});
describe("Error handling edge cases", () => {
it("should handle analyzeImageWithProvider throwing an exception", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "ollama",
model: "llava:latest"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockRejectedValue(new Error("Connection timeout"));
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe("AI analysis failed: Connection timeout");
expect(result.isError).toBe(true);
expect(result._meta?.backend_error_code).toBe("AI_PROVIDER_ERROR");
});
it("should handle unexpected errors gracefully", async () => {
// Make parseAIProviders throw to trigger the catch block
mockParseAIProviders.mockImplementation(() => {
throw new Error("Unexpected error");
});
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What is this?"
},
mockContext,
);
expect(result.content[0].text).toBe("Unexpected error: Unexpected error");
expect(result.isError).toBe(true);
});
});
describe("Provider config edge cases", () => {
it("should handle explicit provider config", async () => {
mockParseAIProviders.mockReturnValue([
{ provider: "ollama", model: "llava:latest" },
{ provider: "openai", model: "gpt-4-vision-preview" }
]);
mockDetermineProviderAndModel.mockResolvedValue({
provider: "openai",
model: "gpt-4-vision-preview"
});
mockReadImageAsBase64.mockResolvedValue("base64imagedata");
mockAnalyzeImageWithProvider.mockResolvedValue("Analysis from OpenAI");
const result = await analyzeToolHandler(
{
image_path: "/tmp/test.png",
question: "What is this?",
provider_config: {
type: "openai",
model: "gpt-4-vision-preview"
}
},
mockContext,
);
expect(result.model_used).toBe("openai/gpt-4-vision-preview");
expect(result.content[0].text).toBe("Analysis from OpenAI");
});
});
});

View file

@ -0,0 +1,523 @@
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/peekaboo-cli";
import { mockSwiftCli } from "../../mocks/peekaboo-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/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(),
};
});
// 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/peekaboo-img-XXXXXX";
const MOCK_SAVED_FILE_PATH = "/tmp/peekaboo-img-XXXXXX/capture.png";
describe("Image Tool - Edge Cases", () => {
beforeEach(() => {
vi.clearAllMocks();
mockFsRm.mockResolvedValue(undefined);
process.env.PEEKABOO_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,
);
});
});
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,
);
});
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,
);
});
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,
);
});
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,
);
});
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,
);
});
});
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.PEEKABOO_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,
);
});
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,
);
});
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,
);
});
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,
);
});
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,
);
});
});
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",
);
});
});
});