Peekaboo/tests/integration/peekaboo-cli-integration.test.ts
Peter Steinberger 4e5e15c5a5 Prepare for 1.0.0-beta.11 release
- Update version to 1.0.0-beta.11 in package.json and Swift version file
- Update CHANGELOG.md with today's date
- Fix test expectations for new error message format
- Build universal Swift binary with latest changes

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-07 22:56:33 +01:00

264 lines
9 KiB
TypeScript

import path from "path";
import fs from "fs/promises";
import os from "os";
import { Logger } from "pino";
import { vi } from "vitest";
import {
imageToolHandler,
listToolHandler,
imageToolSchema,
listToolSchema,
} from "../../src/tools"; // Adjusted import path for schemas
import { initializeSwiftCliPath } from "../../src/utils/peekaboo-cli";
import { Result } from "@modelcontextprotocol/sdk/types.js"; // Corrected SDK import path and type
// Define a more specific type for content items used in Peekaboo
interface PeekabooContentItem {
type: string;
text?: string;
imageUrl?: string;
data?: any;
}
interface PeekabooWindowItem {
app_name?: string; // Swift CLI might use app_name
owningApplication?: string;
kCGWindowOwnerName?: string; // For flexibility
window_title?: string; // Swift CLI might use window_title
windowName?: string;
windowID?: number; // Made optional to reflect reality
window_id?: number; // Allow for Swift CLI variant
windowLevel?: number; // Make optional
isOnScreen?: boolean; // Make optional
is_on_screen?: boolean; // Allow for Swift CLI variant
bounds?: {
// Make optional
X: number;
Y: number;
Width: number;
Height: number;
};
window_index?: number; // Added based on log
// Add any other potential fields observed from Swift CLI output
[key: string]: any; // Allow other fields to be present
}
// Ensure local TestToolResponse interface is removed or commented out
// interface TestToolResponse {
// isError?: boolean;
// content?: Array<{ type: string; text?: string; imageUrl?: string; data?: any }>;
// application_list?: Array<any>;
// saved_files?: Array<{ path: string; data?: string }>;
// _meta?: { backend_error_code?: string; [key: string]: any };
// [key: string]: any;
// }
// Initialize Swift CLI path (assuming 'peekaboo' binary is at project root)
const packageRootDir = path.resolve(__dirname, "..", ".."); // Adjust path from tests/integration to project root
initializeSwiftCliPath(packageRootDir);
const mockLogger: Logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
fatal: vi.fn(),
child: vi.fn().mockReturnThis(),
flush: vi.fn(),
level: "info",
levels: { values: { info: 30 }, labels: { "30": "info" } },
} as unknown as Logger; // Still using unknown for simplicity if full mock is too verbose
describe("Swift CLI Integration Tests", () => {
describe("listToolHandler", () => {
it("should return server_status correctly", async () => {
const args = listToolSchema.parse({ item_type: "server_status" });
const response: Result = await listToolHandler(args, {
logger: mockLogger,
});
expect(response.isError).not.toBe(true);
expect(response.content).toBeDefined();
// Ensure content is an array and has at least one item before accessing it
if (
response.content &&
Array.isArray(response.content) &&
response.content.length > 0
) {
const firstContentItem = response.content[0] as PeekabooContentItem;
expect(firstContentItem.type).toBe("text");
expect(firstContentItem.text).toContain("Peekaboo MCP");
} else {
fail(
"Response content was not in the expected format for server_status",
);
}
});
it("should call Swift CLI for running_applications and return a structured response", async () => {
const args = listToolSchema.parse({ item_type: "running_applications" });
const response: Result = await listToolHandler(args, {
logger: mockLogger,
});
if (response.isError) {
console.error(
"listToolHandler running_applications error:",
JSON.stringify(response),
);
}
expect(response.isError).not.toBe(true);
if (!response.isError) {
expect(response).toHaveProperty("application_list");
expect((response as any).application_list).toBeInstanceOf(Array);
// Optionally, check if at least one app is returned if any are expected to be running
if ((response as any).application_list.length === 0) {
console.warn(
"listToolHandler for running_applications returned an empty list.",
);
}
}
}, 15000);
it("should list windows for a known application (Finder) without details by default", async () => {
const args = listToolSchema.parse({
item_type: "application_windows",
app: "Finder",
// No include_window_details passed
});
const response: Result = await listToolHandler(args, {
logger: mockLogger,
});
if (response.isError) {
console.error(
"listToolHandler Finder windows error response:",
JSON.stringify(response),
);
}
expect(response.isError).not.toBe(true);
if (!response.isError) {
expect(response).toHaveProperty("window_list");
expect(response).toHaveProperty("target_application_info");
const targetAppInfo = (response as any).target_application_info;
expect(targetAppInfo).toBeDefined();
expect(targetAppInfo.app_name).toBe("Finder");
const windowList = (response as any)
.window_list as PeekabooWindowItem[];
expect(windowList).toBeInstanceOf(Array);
if (windowList.length > 0) {
const firstWindow = windowList[0];
// console.log('First window object from Finder (no details requested):', JSON.stringify(firstWindow, null, 2));
expect(firstWindow).toHaveProperty("window_title"); // Expect basic info
expect(firstWindow).toHaveProperty("window_index"); // Expect basic info
// Should NOT have detailed info unless requested
expect(firstWindow.windowID).toBeUndefined();
expect(firstWindow.window_id).toBeUndefined();
expect(firstWindow.bounds).toBeUndefined();
} else {
console.warn(
"listToolHandler for Finder windows returned an empty list. This might be normal.",
);
}
}
}, 15000);
it("should return an error when listing windows for a non-existent application", async () => {
const nonExistentApp = "DefinitelyNotAnApp123ABC";
const args = listToolSchema.parse({
item_type: "application_windows",
app: nonExistentApp,
});
const response: Result = await listToolHandler(args, {
logger: mockLogger,
});
expect(response.isError).toBe(true);
if (
response.content &&
Array.isArray(response.content) &&
response.content.length > 0
) {
const firstContentItem = response.content[0] as PeekabooContentItem;
// Expect the generic failure message from the handler when Swift CLI fails
expect(firstContentItem.text?.toLowerCase()).toMatch(
/list operation failed: (swift cli execution failed|an unknown error occurred)/i,
);
}
}, 15000);
});
describe("imageToolHandler", () => {
let tempImagePath: string;
beforeEach(() => {
tempImagePath = path.join(
os.tmpdir(),
`peekaboo-test-image-${Date.now()}.png`,
);
});
afterEach(async () => {
try {
await fs.unlink(tempImagePath);
} catch (error) {
// Ignore
}
});
it("should attempt to capture screen and save to a file", async () => {
const args = imageToolSchema.parse({
mode: "screen",
path: tempImagePath,
format: "png",
return_data: false,
});
const response: Result = await imageToolHandler(args, {
logger: mockLogger,
});
if (response.isError) {
let errorText = "";
if (
response.content &&
Array.isArray(response.content) &&
response.content.length > 0
) {
const firstContentItem = response.content[0] as PeekabooContentItem;
errorText = firstContentItem.text?.toLowerCase() ?? "";
}
const metaErrorCode = (response._meta as any)?.backend_error_code;
// console.log('Image tool error response:', JSON.stringify(response));
expect(
errorText.includes("permission") ||
errorText.includes("denied") ||
metaErrorCode === "PERMISSION_DENIED_SCREEN_RECORDING" ||
errorText.includes("capture failed"),
).toBeTruthy();
await expect(fs.access(tempImagePath)).rejects.toThrow();
} else {
expect(response.isError).toBeUndefined();
expect(response).toHaveProperty("saved_files");
const successResponse = response as Result & {
saved_files?: { path: string }[];
};
expect(successResponse.saved_files).toBeInstanceOf(Array);
if (
successResponse.saved_files &&
successResponse.saved_files.length > 0
) {
expect(successResponse.saved_files[0]?.path).toBe(tempImagePath);
}
await expect(fs.access(tempImagePath)).resolves.toBeUndefined();
}
}, 20000);
});
});