mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-04-27 15:07:41 +00:00
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:
parent
e74796f7e3
commit
94060963d0
2 changed files with 965 additions and 0 deletions
442
tests/unit/tools/analyze-edge-cases.test.ts
Normal file
442
tests/unit/tools/analyze-edge-cases.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
523
tests/unit/tools/image-edge-cases.test.ts
Normal file
523
tests/unit/tools/image-edge-cases.test.ts
Normal 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue