mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-31 10:25:47 +00:00
- 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>
264 lines
9 KiB
TypeScript
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);
|
|
});
|
|
});
|