Skip to main content
Glama
SpaceCommandTests.swift12.1 kB
import CoreGraphics import Foundation import Testing @testable import PeekabooAutomation @testable import PeekabooCLI @testable import PeekabooCore #if !PEEKABOO_SKIP_AUTOMATION // MARK: - Read-only scenarios @Suite( "Space Command Read Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationRead) ) struct SpaceCommandReadTests { @Test("space command exists in help") func spaceCommandInHelp() async throws { let output = try await self.runPeekaboo(["--help"]) #expect(output.contains("space")) #expect(output.contains("Manage macOS Spaces")) } @Test("space command help shows subcommands") func spaceCommandHelp() async throws { let output = try await self.runPeekaboo(["space", "--help"]) #expect(output.contains("Manage macOS Spaces (virtual desktops)")) #expect(output.contains("list")) #expect(output.contains("switch")) #expect(output.contains("move-window")) } @Test("space switch command help") func spaceSwitchHelp() async throws { let output = try await self.runPeekaboo(["space", "switch", "--help"]) #expect(output.contains("Switch to a different Space")) #expect(output.contains("--to")) } @Test("space list command") func spaceListCommand() async throws { let output = try await self.runPeekaboo(["space", "list"]) #expect(!output.isEmpty) } @Test("space list with JSON output") func spaceListJSON() async throws { let output = try await self.runPeekaboo(["space", "list", "--json-output"]) let response = try JSONDecoder().decode(SpaceListResponse.self, from: Data(output.utf8)) #expect(response.success) } @Test("space list detailed flag") func spaceListDetailed() async throws { let output = try await self.runPeekaboo(["space", "list", "--detailed"]) #expect(output.contains("Space")) } @Test("space switch requires --to parameter") func spaceSwitchRequiresDestination() { #expect(throws: (any Error).self) { try CLIOutputCapture.suppressStderr { _ = try SwitchSubcommand.parse([]) } } } @Test("space switch rejects non-numeric parameters") func spaceSwitchRejectsNonNumeric() { #expect(throws: (any Error).self) { try CLIOutputCapture.suppressStderr { _ = try SwitchSubcommand.parse(["--to", "abc"]) } } } @Test("space move-window requires app parameter") func spaceMoveWindowRequiresApp() { #expect(throws: (any Error).self) { try CLIOutputCapture.suppressStderr { _ = try MoveWindowSubcommand.parse(["--to", "2"]) } } } @Test("space move-window requires destination") func spaceMoveWindowRequiresDestination() { #expect(throws: (any Error).self) { try CLIOutputCapture.suppressStderr { _ = try MoveWindowSubcommand.parse(["--app", "Finder"]) } } } @Test("space move-window parses follow option") func spaceMoveWindowParsesFollowOption() throws { let command = try MoveWindowSubcommand.parse([ "--app", "Finder", "--to", "3", "--follow", ]) #expect(command.app == "Finder") #expect(command.to == 3) #expect(command.follow == true) } private func runPeekaboo(_ arguments: [String]) async throws -> String { let context = await self.makeTestContext() let result = try await InProcessCommandRunner.run( arguments, services: context.services, spaceService: context.spaceService ) return result.stdout } @MainActor func makeTestContext() -> (services: PeekabooServices, spaceService: any SpaceCommandSpaceService) { let applications = Self.testApplications() let windowsByApp = Self.windowsByApp() let services = TestServicesFactory.makePeekabooServices( applications: StubApplicationService(applications: applications, windowsByApp: windowsByApp), windows: StubWindowService(windowsByApp: windowsByApp), menu: StubMenuService(menusByApp: [:]), dialogs: StubDialogService(), screens: [] ) let spaceInfos = Self.spaceInfos() let windowSpaces = Self.windowSpaces(from: spaceInfos) let spaceService = StubSpaceService(spaces: spaceInfos, windowSpaces: windowSpaces) return (services, spaceService) } } extension SpaceCommandReadTests { fileprivate static func testApplications() -> [ServiceApplicationInfo] { [ ServiceApplicationInfo( processIdentifier: 101, bundleIdentifier: "com.apple.finder", name: "Finder", bundlePath: "/System/Library/CoreServices/Finder.app", isActive: true, isHidden: false, windowCount: 1 ), ServiceApplicationInfo( processIdentifier: 202, bundleIdentifier: "com.apple.TextEdit", name: "TextEdit", bundlePath: "/System/Applications/TextEdit.app", isActive: false, isHidden: false, windowCount: 1 ), ] } fileprivate static func windowsByApp() -> [String: [ServiceWindowInfo]] { [ "Finder": [self.finderWindow()], "TextEdit": [self.textEditWindow()], ] } fileprivate static func finderWindow() -> ServiceWindowInfo { ServiceWindowInfo( windowID: 1, title: "Finder Window", bounds: CGRect(x: 0, y: 0, width: 800, height: 600), isMinimized: false, isMainWindow: true, windowLevel: 0, alpha: 1.0, index: 0, spaceID: 1, spaceName: "Desktop 1", screenIndex: 0, screenName: "Built-in" ) } fileprivate static func textEditWindow() -> ServiceWindowInfo { ServiceWindowInfo( windowID: 2, title: "Document", bounds: CGRect(x: 100, y: 100, width: 700, height: 500), isMinimized: false, isMainWindow: true, windowLevel: 0, alpha: 1.0, index: 0, spaceID: 2, spaceName: "Desktop 2", screenIndex: 0, screenName: "Built-in" ) } fileprivate static func spaceInfos() -> [SpaceInfo] { [ SpaceInfo( id: 1, type: .user, isActive: true, displayID: 1, name: "Desktop 1", ownerPIDs: [101] ), SpaceInfo( id: 2, type: .user, isActive: false, displayID: 1, name: "Desktop 2", ownerPIDs: [202] ), ] } fileprivate static func windowSpaces(from infos: [SpaceInfo]) -> [Int: [SpaceInfo]] { [ 1: [infos[0]], 2: [infos[1]], ] } } // MARK: - Actions that mutate Spaces @Suite( "Space Command Action Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationActions) ) struct SpaceCommandActionTests { @Test("space switch with valid number") func spaceSwitchValid() async throws { let context = await self.makeSpaceContext() let result = try await self.runSpaceCommand([ "space", "switch", "--to", "1", "--json-output", ], context: context) #expect(result.exitStatus == 0) let response = try JSONDecoder().decode( SpaceActionResponse.self, from: Data(self.output(from: result).utf8) ) #expect(response.success) let switchCalls = await self.spaceState(context) { $0.switchCalls } #expect(switchCalls.contains(1)) } @Test("space move-window to current Space") func spaceMoveWindowToCurrent() async throws { let context = await self.makeSpaceContext() let result = try await self.runSpaceCommand([ "space", "move-window", "--app", "Finder", "--to-current", "--json-output", ], context: context) #expect(result.exitStatus == 0) let response = try JSONDecoder().decode( WindowSpaceActionResponse.self, from: Data(self.output(from: result).utf8) ) #expect(response.success) let moveCalls = await self.spaceState(context) { $0.moveToCurrentCalls } #expect(!moveCalls.isEmpty) } @Test("space move-window with follow option") func spaceMoveWindowWithFollow() async throws { let context = await self.makeSpaceContext() let result = try await self.runSpaceCommand([ "space", "move-window", "--app", "TextEdit", "--to", "1", "--follow", "--json-output", ], context: context) #expect(result.exitStatus == 0) let response = try JSONDecoder().decode( WindowSpaceActionResponse.self, from: Data(self.output(from: result).utf8) ) #expect(response.success) let moveCalls = await self.spaceState(context) { $0.moveWindowCalls } #expect(moveCalls.contains { $0.spaceID == 1 }) } private func runSpaceCommand( _ arguments: [String], context: SpaceHarnessContext ) async throws -> CommandRunResult { try await SpaceCommandEnvironment.withSpaceService(context.spaceService) { try await InProcessCommandRunner.run( arguments, services: context.services, spaceService: context.spaceService ) } } @MainActor private func makeSpaceContext() async -> SpaceHarnessContext { let base = SpaceCommandReadTests().makeTestContext() let spaces = await base.spaceService.getAllSpaces() let spaceService = StubSpaceService(spaces: spaces, windowSpaces: [:]) let services = base.services return SpaceHarnessContext(services: services, spaceService: spaceService) } private func output(from result: CommandRunResult) -> String { result.stdout.isEmpty ? result.stderr : result.stdout } private func spaceState<T: Sendable>( _ context: SpaceHarnessContext, _ operation: @MainActor (StubSpaceService) -> T ) async -> T { await MainActor.run { operation(context.spaceService) } } } private struct SpaceHarnessContext { let services: PeekabooServices let spaceService: StubSpaceService } // MARK: - Response types shared by tests private struct SpaceListResponse: Codable { let success: Bool let data: SpaceListData? let error: String? } private struct SpaceListData: Codable { let spaces: [SpaceData] } private struct SpaceData: Codable { let id: UInt64 let type: String let is_active: Bool? let display_id: UInt32? } private struct SpaceActionResponse: Codable { let success: Bool let data: SpaceActionData? let error: String? } private struct SpaceActionData: Codable { let action: String let success: Bool let space_id: UInt64 let space_number: Int } private struct WindowSpaceActionResponse: Codable { let success: Bool let data: WindowSpaceActionData? let error: String? } private struct WindowSpaceActionData: Codable { let action: String let success: Bool let window_id: UInt32 let window_title: String let space_id: UInt64? let space_number: Int? let moved_to_current: Bool? let followed: Bool? } #endif

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server