Peekaboo/tests/unit/tools/list.test.ts
Peter Steinberger 9295dd1ed0 Fix server status test by properly mocking fs module
- Mock fs.readFile in beforeEach to return valid package.json
- Mock fs module with existsSync and accessSync for CLI checks

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-05-27 00:44:33 +02:00

789 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { 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 path from 'path'; // path is still used by the test itself for expect.stringContaining if needed, but not for mocking resolve/dirname
// import { fileURLToPath } from 'url'; // No longer needed
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,
},
}));
// Mock path and url functions to avoid import.meta.url issues in test environment
// jest.mock('url', () => ({ // REMOVED
// ...jest.requireActual('url'), // REMOVED
// fileURLToPath: jest.fn().mockReturnValue('/mocked/path/to/list.ts'), // REMOVED
// })); // REMOVED
// jest.mock('path', () => ({ // REMOVED
// ...jest.requireActual('path'), // REMOVED
// dirname: jest.fn((p) => jest.requireActual('path').dirname(p)), // REMOVED
// resolve: jest.fn((...paths) => { // REMOVED
// // If it's trying to resolve relative to the mocked list.ts, provide a specific mocked package.json path // REMOVED
// if (paths.length === 3 && paths[0] === '/mocked/path/to' && paths[1] === '..' && paths[2] === '..') { // REMOVED
// return '/mocked/path/package.json'; // REMOVED
// } // REMOVED
// return jest.requireActual('path').resolve(...paths); // Fallback to actual resolve // REMOVED
// }), // REMOVED
// })); // REMOVED
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(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(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 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.mockResolvedValue("{ invalid 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(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",
]);
});
});
});