mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-30 10:15:47 +00:00
Fixes issue where item_type: '' was not properly defaulting to the correct operation. Empty strings and whitespace-only strings now fall back to the proper default logic: - If app is provided: defaults to 'application_windows' - If no app: defaults to 'running_applications' 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1022 lines
31 KiB
TypeScript
1022 lines
31 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||
import { pino } from "pino";
|
||
import {
|
||
listToolHandler,
|
||
buildSwiftCliArgs,
|
||
ListToolInput,
|
||
listToolSchema,
|
||
} from "../../../src/tools/list";
|
||
import { executeSwiftCli } from "../../../src/utils/peekaboo-cli";
|
||
import { generateServerStatusString } from "../../../src/utils/server-status";
|
||
import fs from "fs/promises";
|
||
import {
|
||
ToolContext,
|
||
ApplicationListData,
|
||
WindowListData,
|
||
} from "../../../src/types/index.js";
|
||
|
||
// Mocks
|
||
vi.mock("../../../src/utils/peekaboo-cli");
|
||
vi.mock("../../../src/utils/server-status");
|
||
vi.mock("fs/promises");
|
||
vi.mock("fs", () => ({
|
||
existsSync: vi.fn(() => false),
|
||
accessSync: vi.fn(),
|
||
constants: {
|
||
X_OK: 1,
|
||
W_OK: 2,
|
||
},
|
||
}));
|
||
|
||
const mockExecuteSwiftCli = executeSwiftCli as vi.MockedFunction<
|
||
typeof executeSwiftCli
|
||
>;
|
||
const mockGenerateServerStatusString =
|
||
generateServerStatusString as vi.MockedFunction<
|
||
typeof generateServerStatusString
|
||
>;
|
||
const mockFsReadFile = fs.readFile as vi.MockedFunction<typeof fs.readFile>;
|
||
const mockFsAccess = fs.access as vi.MockedFunction<typeof fs.access>;
|
||
|
||
// Create a mock logger for tests
|
||
const mockLogger = pino({ level: "silent" });
|
||
const mockContext: ToolContext = { logger: mockLogger };
|
||
|
||
describe("List Tool", () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
// Mock fs.access to always succeed by default
|
||
mockFsAccess.mockResolvedValue(undefined);
|
||
// Mock fs.readFile to return a valid package.json
|
||
mockFsReadFile.mockResolvedValue(JSON.stringify({ version: "1.0.0" }));
|
||
});
|
||
|
||
describe("buildSwiftCliArgs", () => {
|
||
it("should return default args for running_applications", () => {
|
||
const input: ListToolInput = { item_type: "running_applications" };
|
||
expect(buildSwiftCliArgs(input)).toEqual(["list", "apps"]);
|
||
});
|
||
|
||
it("should return args for application_windows with app only", () => {
|
||
const input: ListToolInput = {
|
||
item_type: "application_windows",
|
||
app: "Safari",
|
||
};
|
||
expect(buildSwiftCliArgs(input)).toEqual([
|
||
"list",
|
||
"windows",
|
||
"--app",
|
||
"Safari",
|
||
]);
|
||
});
|
||
|
||
it("should return args for application_windows with app and details", () => {
|
||
const input: ListToolInput = {
|
||
item_type: "application_windows",
|
||
app: "Chrome",
|
||
include_window_details: ["bounds", "ids"],
|
||
};
|
||
expect(buildSwiftCliArgs(input)).toEqual([
|
||
"list",
|
||
"windows",
|
||
"--app",
|
||
"Chrome",
|
||
"--include-details",
|
||
"bounds,ids",
|
||
]);
|
||
});
|
||
|
||
it("should return args for application_windows with app and empty details", () => {
|
||
const input: ListToolInput = {
|
||
item_type: "application_windows",
|
||
app: "Finder",
|
||
include_window_details: [],
|
||
};
|
||
expect(buildSwiftCliArgs(input)).toEqual([
|
||
"list",
|
||
"windows",
|
||
"--app",
|
||
"Finder",
|
||
]);
|
||
});
|
||
|
||
it("should ignore app and include_window_details if item_type is not application_windows", () => {
|
||
const input: ListToolInput = {
|
||
item_type: "running_applications",
|
||
app: "ShouldBeIgnored",
|
||
include_window_details: ["bounds"],
|
||
};
|
||
expect(buildSwiftCliArgs(input)).toEqual(["list", "apps"]);
|
||
});
|
||
});
|
||
|
||
describe("listToolHandler", () => {
|
||
it("should list running applications", async () => {
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [
|
||
{
|
||
app_name: "Safari",
|
||
bundle_id: "com.apple.Safari",
|
||
pid: 1234,
|
||
is_active: true,
|
||
window_count: 2,
|
||
},
|
||
{
|
||
app_name: "Cursor",
|
||
bundle_id: "com.todesktop.230313mzl4w4u92",
|
||
pid: 5678,
|
||
is_active: false,
|
||
window_count: 1,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
||
["list", "apps"],
|
||
mockLogger,
|
||
expect.objectContaining({ timeout: expect.any(Number) })
|
||
);
|
||
expect(result.content[0].text).toContain("Found 2 running applications");
|
||
expect(result.content[0].text).toContain(
|
||
"Safari (com.apple.Safari) - PID: 1234 [ACTIVE] - Windows: 2",
|
||
);
|
||
expect(result.content[0].text).toContain(
|
||
"Cursor (com.todesktop.230313mzl4w4u92) - PID: 5678 - Windows: 1",
|
||
);
|
||
expect((result as any).application_list).toEqual(
|
||
mockSwiftResponse.applications,
|
||
);
|
||
});
|
||
|
||
it("should list application windows", async () => {
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: {
|
||
app_name: "Safari",
|
||
bundle_id: "com.apple.Safari",
|
||
pid: 1234,
|
||
},
|
||
windows: [
|
||
{
|
||
window_title: "Main Window",
|
||
window_id: 12345,
|
||
is_on_screen: true,
|
||
bounds: { x: 0, y: 0, width: 800, height: 600 },
|
||
},
|
||
{
|
||
window_title: "Secondary Window",
|
||
window_id: 12346,
|
||
is_on_screen: false,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "Safari",
|
||
include_window_details: ["ids", "bounds", "off_screen"],
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
||
[
|
||
"list",
|
||
"windows",
|
||
"--app",
|
||
"Safari",
|
||
"--include-details",
|
||
"ids,bounds,off_screen",
|
||
],
|
||
mockLogger,
|
||
expect.objectContaining({ timeout: expect.any(Number) })
|
||
);
|
||
expect(result.content[0].text).toContain(
|
||
"Found 2 windows for application: Safari (com.apple.Safari) - PID: 1234",
|
||
);
|
||
expect(result.content[0].text).toContain(
|
||
'1. "Main Window" [ID: 12345] [ON-SCREEN] [0,0 800×600]',
|
||
);
|
||
expect(result.content[0].text).toContain(
|
||
'2. "Secondary Window" [ID: 12346] [OFF-SCREEN]',
|
||
);
|
||
expect((result as any).window_list).toEqual(mockSwiftResponse.windows);
|
||
expect((result as any).target_application_info).toEqual(
|
||
mockSwiftResponse.target_application_info,
|
||
);
|
||
});
|
||
|
||
it("should handle server status", async () => {
|
||
// Mock generateServerStatusString since it's still used
|
||
mockGenerateServerStatusString.mockReturnValue(
|
||
"# Peekaboo MCP Server Status\nVersion: 1.2.3",
|
||
);
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "server_status",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
// Should NOT call executeSwiftCli for server_status anymore
|
||
expect(mockExecuteSwiftCli).not.toHaveBeenCalled();
|
||
|
||
// Should call generateServerStatusString with the version
|
||
expect(mockGenerateServerStatusString).toHaveBeenCalled();
|
||
|
||
// Check that the response contains expected sections
|
||
const statusText = result.content[0].text;
|
||
expect(statusText).toContain("# Peekaboo MCP Server Status");
|
||
expect(statusText).toContain("## Native Binary (Swift CLI) Status");
|
||
expect(statusText).toContain("## System Permissions");
|
||
expect(statusText).toContain("## Environment Configuration");
|
||
expect(statusText).toContain("## Configuration Issues");
|
||
expect(statusText).toContain("## System Information");
|
||
});
|
||
|
||
it("should handle Swift CLI errors", async () => {
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: false,
|
||
error: { message: "Application not found", code: "APP_NOT_FOUND" },
|
||
});
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
)) as { content: any[]; isError?: boolean; _meta?: any };
|
||
|
||
expect(result.content[0].text).toBe(
|
||
"List operation failed: Application not found",
|
||
);
|
||
expect(result.isError).toBe(true);
|
||
expect((result as any)._meta.backend_error_code).toBe("APP_NOT_FOUND");
|
||
});
|
||
|
||
it("should return a specific error if the app is not found", async () => {
|
||
// Arrange
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: false,
|
||
error: {
|
||
message: "The specified application ('Ciursor') is not running or could not be found.",
|
||
code: "SWIFT_CLI_APP_NOT_FOUND",
|
||
details: "Error: Application with name 'Ciursor' not found.",
|
||
},
|
||
});
|
||
const args = { item_type: "application_windows", app: "Ciursor" } as ListToolInput;
|
||
|
||
// Act
|
||
const result = await listToolHandler(args, mockContext);
|
||
|
||
// Assert
|
||
expect(result.isError).toBe(true);
|
||
expect(result.content[0].text).toBe(
|
||
"List operation failed: The specified application ('Ciursor') is not running or could not be found.\nError: Application with name 'Ciursor' not found."
|
||
);
|
||
});
|
||
|
||
it("should handle Swift CLI errors with no message or code", async () => {
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: false,
|
||
error: { message: "Unknown error", code: "UNKNOWN_SWIFT_ERROR" }, // Provide default message and code
|
||
});
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
)) as { content: any[]; isError?: boolean; _meta?: any };
|
||
|
||
expect(result.content[0].text).toBe(
|
||
"List operation failed: Unknown error",
|
||
);
|
||
expect(result.isError).toBe(true);
|
||
// Meta might or might not be undefined depending on the exact path, so let's check the code if present
|
||
if (result._meta) {
|
||
expect(result._meta.backend_error_code).toBe("UNKNOWN_SWIFT_ERROR");
|
||
} else {
|
||
// If no _meta, the code should still reflect the error object passed
|
||
// This case might need adjustment based on listToolHandler's exact logic for _meta creation
|
||
}
|
||
});
|
||
|
||
it("should handle unexpected errors during Swift CLI execution", async () => {
|
||
mockExecuteSwiftCli.mockRejectedValue(
|
||
new Error("Unexpected Swift execution error"),
|
||
);
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
)) as { content: any[]; isError?: boolean };
|
||
|
||
expect(result.content[0].text).toBe(
|
||
"Unexpected error: Unexpected Swift execution error",
|
||
);
|
||
expect(result.isError).toBe(true);
|
||
});
|
||
|
||
it("should handle unexpected errors during server status (fs.readFile fails)", async () => {
|
||
mockFsReadFile.mockRejectedValue(new Error("Cannot read package.json"));
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "server_status",
|
||
},
|
||
mockContext,
|
||
)) as { content: any[]; isError?: boolean };
|
||
|
||
expect(result.content[0].text).toBe(
|
||
"Unexpected error: Cannot read package.json",
|
||
);
|
||
expect(result.isError).toBe(true);
|
||
});
|
||
|
||
it("should include Swift CLI messages in the output for applications list", async () => {
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [
|
||
{
|
||
app_name: "TestApp",
|
||
bundle_id: "com.test.app",
|
||
pid: 111,
|
||
is_active: false,
|
||
window_count: 0,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: ["Warning: One app hidden.", "Info: Low memory."],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{ item_type: "running_applications" },
|
||
mockContext,
|
||
);
|
||
expect(result.content[0].text).toContain(
|
||
"Messages: Warning: One app hidden.; Info: Low memory.",
|
||
);
|
||
});
|
||
|
||
it("should include Swift CLI messages in the output for windows list", async () => {
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: { app_name: "TestApp", pid: 111 },
|
||
windows: [{ window_title: "TestWindow", window_id: 222 }],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: ["Note: Some windows might be minimized."],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{ item_type: "application_windows", app: "TestApp" },
|
||
mockContext,
|
||
);
|
||
expect(result.content[0].text).toContain(
|
||
"Messages: Note: Some windows might be minimized.",
|
||
);
|
||
});
|
||
|
||
it("should handle missing app parameter for application_windows", async () => {
|
||
// The Zod schema validation should catch this before the handler is called
|
||
// In real usage, this would throw a validation error
|
||
// For testing, we can simulate what would happen if validation was bypassed
|
||
expect(() => {
|
||
listToolSchema.parse({
|
||
item_type: "application_windows",
|
||
// missing app parameter
|
||
});
|
||
}).toThrow();
|
||
});
|
||
|
||
it("should handle empty applications list", async () => {
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain("Found 0 running applications");
|
||
expect((result as any).application_list).toEqual([]);
|
||
});
|
||
|
||
it("should handle empty windows list", async () => {
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: {
|
||
app_name: "Safari",
|
||
bundle_id: "com.apple.Safari",
|
||
pid: 1234,
|
||
},
|
||
windows: [],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "Safari",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain(
|
||
"Found 0 windows for application: Safari",
|
||
);
|
||
expect((result as any).window_list).toEqual([]);
|
||
});
|
||
|
||
it("should handle very long app names", async () => {
|
||
const longAppName = "A".repeat(256);
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [
|
||
{
|
||
app_name: longAppName,
|
||
bundle_id: "com.long.app",
|
||
pid: 9999,
|
||
is_active: false,
|
||
window_count: 1,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain(longAppName);
|
||
});
|
||
|
||
it("should handle special characters in app names", async () => {
|
||
const specialAppName = "App™ with © Special & Characters™";
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [
|
||
{
|
||
app_name: specialAppName,
|
||
bundle_id: "com.special.app",
|
||
pid: 1111,
|
||
is_active: true,
|
||
window_count: 2,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain(specialAppName);
|
||
});
|
||
|
||
it("should handle all window detail options", async () => {
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: {
|
||
app_name: "TestApp",
|
||
bundle_id: "com.test.app",
|
||
pid: 1234,
|
||
},
|
||
windows: [
|
||
{
|
||
window_title: "Test Window",
|
||
window_id: 12345,
|
||
window_index: 0,
|
||
is_on_screen: true,
|
||
bounds: { x: 100, y: 200, width: 800, height: 600 },
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "TestApp",
|
||
include_window_details: ["ids", "bounds", "off_screen"],
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
// Window numbering is 1-based in the output
|
||
expect(result.content[0].text).toContain('1. "Test Window"');
|
||
expect(result.content[0].text).toContain("[ID: 12345]");
|
||
expect(result.content[0].text).toContain("[100,200 800×600]");
|
||
expect(result.content[0].text).toContain("[ON-SCREEN]");
|
||
});
|
||
|
||
it("should handle windows with missing optional fields", async () => {
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: { app_name: "TestApp", pid: 1234 },
|
||
windows: [
|
||
{
|
||
window_title: "Minimal Window",
|
||
// All other fields are optional
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "TestApp",
|
||
include_window_details: ["ids", "bounds"],
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain('"Minimal Window"');
|
||
expect(result.content[0].text).not.toContain("[ID:"); // No ID present
|
||
expect(result.content[0].text).not.toContain("×"); // No bounds present
|
||
});
|
||
|
||
it("should handle malformed Swift CLI response for applications", async () => {
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: null, // Invalid data
|
||
});
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
)) as any;
|
||
|
||
expect(result.isError).toBe(true);
|
||
expect(result.content[0].text).toContain(
|
||
"Invalid response from list utility",
|
||
);
|
||
});
|
||
|
||
it("should handle malformed Swift CLI response for windows", async () => {
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: { windows: [] }, // Missing target_application_info
|
||
});
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "Safari",
|
||
},
|
||
mockContext,
|
||
)) as any;
|
||
|
||
expect(result.isError).toBe(true);
|
||
expect(result.content[0].text).toContain(
|
||
"Invalid response from list utility",
|
||
);
|
||
});
|
||
|
||
it("should handle very large PID values", async () => {
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [
|
||
{
|
||
app_name: "TestApp",
|
||
bundle_id: "com.test.app",
|
||
pid: Number.MAX_SAFE_INTEGER,
|
||
is_active: false,
|
||
window_count: 0,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain(
|
||
`PID: ${Number.MAX_SAFE_INTEGER}`,
|
||
);
|
||
});
|
||
|
||
it("should handle negative window count", async () => {
|
||
const mockSwiftResponse: ApplicationListData = {
|
||
applications: [
|
||
{
|
||
app_name: "BuggyApp",
|
||
bundle_id: "com.buggy.app",
|
||
pid: 1234,
|
||
is_active: false,
|
||
window_count: -1, // Shouldn't happen but testing edge case
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "running_applications",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain("Windows: -1");
|
||
});
|
||
|
||
it("should handle very long window titles", async () => {
|
||
const longTitle = "Window ".repeat(100);
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: { app_name: "TestApp", pid: 1234 },
|
||
windows: [
|
||
{
|
||
window_title: longTitle,
|
||
window_id: 12345,
|
||
},
|
||
],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "TestApp",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.content[0].text).toContain(longTitle);
|
||
});
|
||
|
||
it("should handle invalid version in package.json", async () => {
|
||
mockFsReadFile.mockResolvedValue('{ "not_version": "1.0.0" }');
|
||
mockGenerateServerStatusString.mockReturnValue(
|
||
"Peekaboo MCP Server v[unknown]\nStatus: Test",
|
||
);
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "server_status",
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(mockGenerateServerStatusString).toHaveBeenCalledWith("[unknown]");
|
||
expect(result.content[0].text).toContain("[unknown]");
|
||
});
|
||
|
||
it("should handle malformed package.json", async () => {
|
||
mockFsReadFile.mockRejectedValue(new Error("Cannot read package.json"));
|
||
|
||
const result = (await listToolHandler(
|
||
{
|
||
item_type: "server_status",
|
||
},
|
||
mockContext,
|
||
)) as any;
|
||
|
||
expect(result.isError).toBe(true);
|
||
expect(result.content[0].text).toContain("Unexpected error");
|
||
});
|
||
|
||
it("should handle empty window details array", async () => {
|
||
const mockSwiftResponse: WindowListData = {
|
||
target_application_info: { app_name: "TestApp", pid: 1234 },
|
||
windows: [{ window_title: "Test Window" }],
|
||
};
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: true,
|
||
data: mockSwiftResponse,
|
||
messages: [],
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "TestApp",
|
||
include_window_details: [],
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(mockExecuteSwiftCli).toHaveBeenCalledWith(
|
||
["list", "windows", "--app", "TestApp"],
|
||
mockLogger,
|
||
expect.objectContaining({ timeout: expect.any(Number) })
|
||
);
|
||
expect(result.content[0].text).toContain('"Test Window"');
|
||
});
|
||
|
||
it("should handle duplicate window detail options", async () => {
|
||
const input: ListToolInput = {
|
||
item_type: "application_windows",
|
||
app: "TestApp",
|
||
include_window_details: ["ids", "ids", "bounds", "bounds"], // Duplicates
|
||
};
|
||
|
||
const args = buildSwiftCliArgs(input);
|
||
expect(args).toEqual([
|
||
"list",
|
||
"windows",
|
||
"--app",
|
||
"TestApp",
|
||
"--include-details",
|
||
"ids,ids,bounds,bounds",
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe("listToolSchema validation", () => {
|
||
it("should succeed when item_type is 'running_applications' and 'include_window_details' is an empty array", () => {
|
||
const input = {
|
||
item_type: "running_applications",
|
||
include_window_details: [],
|
||
};
|
||
const result = listToolSchema.safeParse(input);
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
it("should fail when item_type is 'running_applications' and 'include_window_details' is not empty", () => {
|
||
const result = listToolSchema.safeParse({
|
||
item_type: "running_applications",
|
||
include_window_details: ["ids"],
|
||
});
|
||
expect(result.success).toBe(false);
|
||
if (!result.success) {
|
||
expect(result.error.flatten().fieldErrors.include_window_details).toEqual([
|
||
"'include_window_details' is only applicable when 'item_type' is 'application_windows' or when 'app' is provided.",
|
||
]);
|
||
}
|
||
});
|
||
|
||
it("should fail when item_type is 'server_status' and 'include_window_details' has values", () => {
|
||
const result = listToolSchema.safeParse({
|
||
item_type: "server_status",
|
||
include_window_details: ["bounds"],
|
||
});
|
||
expect(result.success).toBe(false);
|
||
if (!result.success) {
|
||
expect(result.error.flatten().fieldErrors.item_type).toEqual([
|
||
"'app' and 'include_window_details' not applicable for 'server_status'.",
|
||
]);
|
||
}
|
||
});
|
||
|
||
it("should succeed when item_type is 'server_status' and 'include_window_details' is empty", () => {
|
||
const result = listToolSchema.safeParse({
|
||
item_type: "server_status",
|
||
include_window_details: [],
|
||
});
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
it("should succeed when item_type is 'server_status' without extra parameters", () => {
|
||
const result = listToolSchema.safeParse({
|
||
item_type: "server_status",
|
||
});
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
it("should succeed when item_type is 'application_windows' and 'include_window_details' is provided", () => {
|
||
const input = {
|
||
item_type: "application_windows",
|
||
app: "Finder",
|
||
include_window_details: ["ids"],
|
||
};
|
||
const result = listToolSchema.safeParse(input);
|
||
expect(result.success).toBe(true);
|
||
});
|
||
|
||
it("should fail when item_type is 'application_windows' and 'app' is missing", () => {
|
||
const input = {
|
||
item_type: "application_windows",
|
||
include_window_details: ["ids"],
|
||
};
|
||
const result = listToolSchema.safeParse(input);
|
||
expect(result.success).toBe(false);
|
||
if (!result.success) {
|
||
expect(result.error.flatten().fieldErrors.app).toEqual([
|
||
"For 'application_windows', 'app' identifier is required.",
|
||
]);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("listToolHandler - Error message handling", () => {
|
||
it("should include error details for ambiguous app identifier", async () => {
|
||
// Mock Swift CLI returning ambiguous app error with details
|
||
mockExecuteSwiftCli.mockResolvedValue({
|
||
success: false,
|
||
error: {
|
||
message: "Multiple applications match identifier 'C'. Please be more specific.",
|
||
code: "AMBIGUOUS_APP_IDENTIFIER",
|
||
details: "Matches found: Calendar (com.apple.iCal), Console (com.apple.Console), Cursor (com.todesktop.230313mzl4w4u92)"
|
||
}
|
||
});
|
||
|
||
const result = await listToolHandler(
|
||
{
|
||
item_type: "application_windows",
|
||
app: "C"
|
||
},
|
||
mockContext,
|
||
);
|
||
|
||
expect(result.isError).toBe(true);
|
||
expect(result.content[0].type).toBe("text");
|
||
// Should include both the main message and the details
|
||
expect(result.content[0].text).toContain("Multiple applications match identifier 'C'");
|
||
expect(result.content[0].text).toContain("Matches found: Calendar (com.apple.iCal), Console (com.apple.Console), Cursor (com.todesktop.230313mzl4w4u92)");
|
||
});
|
||
});
|
||
|
||
describe("include_window_details array parsing", () => {
|
||
it("should parse JSON string array correctly", () => {
|
||
const input = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: '["ids", "bounds", "off_screen"]'
|
||
};
|
||
|
||
const result = listToolSchema.parse(input);
|
||
expect(result.include_window_details).toEqual(["ids", "bounds", "off_screen"]);
|
||
});
|
||
|
||
it("should handle comma-separated string", () => {
|
||
const input = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: "ids,bounds,off_screen"
|
||
};
|
||
|
||
const result = listToolSchema.parse(input);
|
||
expect(result.include_window_details).toEqual(["ids", "bounds", "off_screen"]);
|
||
});
|
||
|
||
it("should handle single string value", () => {
|
||
const input = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: "ids"
|
||
};
|
||
|
||
const result = listToolSchema.parse(input);
|
||
expect(result.include_window_details).toEqual(["ids"]);
|
||
});
|
||
|
||
it("should handle empty string as undefined", () => {
|
||
const input = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: ""
|
||
};
|
||
|
||
const result = listToolSchema.parse(input);
|
||
expect(result.include_window_details).toBeUndefined();
|
||
});
|
||
|
||
it("should handle proper array as-is", () => {
|
||
const input = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: ["ids", "bounds", "off_screen"]
|
||
};
|
||
|
||
const result = listToolSchema.parse(input);
|
||
expect(result.include_window_details).toEqual(["ids", "bounds", "off_screen"]);
|
||
});
|
||
|
||
it("should handle undefined/null values", () => {
|
||
const input1 = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: undefined
|
||
};
|
||
|
||
const input2 = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: null
|
||
};
|
||
|
||
const result1 = listToolSchema.parse(input1);
|
||
const result2 = listToolSchema.parse(input2);
|
||
|
||
expect(result1.include_window_details).toBeUndefined();
|
||
expect(result2.include_window_details).toBeUndefined();
|
||
});
|
||
|
||
it("should validate enum values after parsing", () => {
|
||
const input = {
|
||
item_type: "application_windows" as const,
|
||
app: "chrome",
|
||
include_window_details: '["ids", "invalid_option", "bounds"]'
|
||
};
|
||
|
||
expect(() => listToolSchema.parse(input)).toThrow();
|
||
});
|
||
});
|
||
|
||
describe("buildSwiftCliArgs empty string handling", () => {
|
||
it("should treat empty string item_type as undefined and default correctly", () => {
|
||
// Test case where item_type is empty string (should default to running_applications)
|
||
const inputWithEmptyString: ListToolInput = {
|
||
item_type: "",
|
||
include_window_details: [],
|
||
};
|
||
|
||
const args = buildSwiftCliArgs(inputWithEmptyString);
|
||
expect(args).toEqual(["list", "apps"]);
|
||
});
|
||
|
||
it("should treat empty string item_type with app as application_windows", () => {
|
||
// Test case where item_type is empty string but app is provided
|
||
const inputWithEmptyStringAndApp: ListToolInput = {
|
||
item_type: "",
|
||
app: "Safari",
|
||
include_window_details: [],
|
||
};
|
||
|
||
const args = buildSwiftCliArgs(inputWithEmptyStringAndApp);
|
||
expect(args).toEqual(["list", "windows", "--app", "Safari"]);
|
||
});
|
||
|
||
it("should treat whitespace-only item_type as undefined", () => {
|
||
// Test case where item_type is just whitespace
|
||
const inputWithWhitespace: ListToolInput = {
|
||
item_type: " ",
|
||
include_window_details: [],
|
||
};
|
||
|
||
const args = buildSwiftCliArgs(inputWithWhitespace);
|
||
expect(args).toEqual(["list", "apps"]);
|
||
});
|
||
});
|
||
});
|