Skip to main content
Glama
AppCommandTests.swift11.5 kB
import Foundation import Testing @testable import PeekabooCLI @testable import PeekabooCore #if !PEEKABOO_SKIP_AUTOMATION @Suite("App Command Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationRead)) struct AppCommandTests { @Test("App command exists") func appCommandExists() { let config = AppCommand.commandDescription #expect(config.commandName == "app") #expect(config.abstract.contains("Control applications")) } @Test("App command has expected subcommands") func appSubcommands() { let subcommands = AppCommand.commandDescription.subcommands #expect(subcommands.count == 7) var subcommandNames: [String] = [] subcommandNames.reserveCapacity(subcommands.count) for descriptor in subcommands { let name = descriptor.commandDescription.commandName ?? "" subcommandNames.append(name) } #expect(subcommandNames.contains("launch")) #expect(subcommandNames.contains("quit")) #expect(subcommandNames.contains("hide")) #expect(subcommandNames.contains("unhide")) #expect(subcommandNames.contains("switch")) #expect(subcommandNames.contains("relaunch")) #expect(subcommandNames.contains("list")) } @Test("App launch command help") func appLaunchHelp() async throws { let output = try await runAppCommand(["app", "launch", "--help"]) #expect(output.contains("Launch an application")) #expect(output.contains("--bundle-id")) #expect(output.contains("--open")) #expect(output.contains("--wait-until-ready")) #expect(output.contains("--no-focus")) } @Test("App quit command validation") func appQuitValidation() async throws { // Test missing app/all await #expect(throws: (any Error).self) { _ = try await runAppCommand(["app", "quit"]) } // Test conflicting options await #expect(throws: (any Error).self) { _ = try await runAppCommand(["app", "quit", "--app", "Finder", "--all"]) } } @Test("App hide command validation") func appHideValidation() async throws { // Normal hide should work let output = try await runAppCommand(["app", "hide", "--app", "Finder", "--help"]) #expect(output.contains("Hide an application")) } @Test("App show command validation") func appShowValidation() async throws { // Test missing app/all await #expect(throws: (any Error).self) { _ = try await runAppCommand(["app", "unhide"]) } } @Test("App switch command validation") func appSwitchValidation() async throws { // Test missing to/cycle await #expect(throws: (any Error).self) { _ = try await runAppCommand(["app", "switch"]) } } @Test("App lifecycle flow") func appLifecycleFlow() { // This tests the logical flow of app lifecycle commands let launchCmd = ["app", "launch", "--app", "TextEdit", "--wait-until-ready"] let hideCmd = ["app", "hide", "--app", "TextEdit"] let showCmd = ["app", "unhide", "--app", "TextEdit"] let quitCmd = ["app", "quit", "--app", "TextEdit", "--json"] // Verify command structure is valid #expect(launchCmd.count > 3) #expect(hideCmd.count > 3) #expect(showCmd.count > 3) #expect(quitCmd.count > 3) } } // MARK: - App Command Integration Tests @Suite( "App Command Integration Tests", .serialized, .tags(.automation, .localOnly), .enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true" && !(ProcessInfo.processInfo.environment["PEEKABOO_CLI_PATH"] ?? "").isEmpty) ) struct AppCommandIntegrationTests { @Test("Launch TextEdit via external CLI") func launchApp() async throws { struct LaunchResult: Codable { let action: String let app_name: String let bundle_id: String let pid: Int32 let is_ready: Bool } let result = try ExternalCommandRunner.runPeekabooCLI( [ "app", "launch", "TextEdit", "--wait-until-ready", "--no-focus", "--json", ], allowedExitCodes: [0, 1] ) if result.exitStatus != 0 { let error = try ExternalCommandRunner.decodeJSONResponse(from: result, as: JSONResponse.self) if error.error?.code == ErrorCode.PERMISSION_ERROR_ACCESSIBILITY.rawValue { Issue.record("Accessibility permission required for app launch integration test") return } Issue.record("App launch failed: \(result.combinedOutput)") return } let response = try ExternalCommandRunner.decodeJSONResponse( from: result, as: CodableJSONResponse<LaunchResult>.self ) #expect(response.success == true) #expect(response.data.action == "launch") #expect(response.data.pid > 0) } @Test("Hide and unhide Finder via external CLI") func hideShowApp() async throws { struct UnhideResult: Codable { let action: String let app_name: String let bundle_id: String let activated: Bool } let hideResult = try ExternalCommandRunner.runPeekabooCLI( [ "app", "hide", "--app", "Finder", "--json", ], allowedExitCodes: [0, 1] ) if hideResult.exitStatus != 0 { let error = try ExternalCommandRunner.decodeJSONResponse(from: hideResult, as: JSONResponse.self) if error.error?.code == ErrorCode.PERMISSION_ERROR_ACCESSIBILITY.rawValue { Issue.record("Accessibility permission required for app hide/unhide integration test") return } Issue.record("App hide failed: \(hideResult.combinedOutput)") return } let unhideResult = try ExternalCommandRunner.runPeekabooCLI( [ "app", "unhide", "--app", "Finder", "--activate", "--json", ], allowedExitCodes: [0, 1] ) if unhideResult.exitStatus != 0 { let error = try ExternalCommandRunner.decodeJSONResponse(from: unhideResult, as: JSONResponse.self) if error.error?.code == ErrorCode.PERMISSION_ERROR_ACCESSIBILITY.rawValue { Issue.record("Accessibility permission required for app hide/unhide integration test") return } Issue.record("App unhide failed: \(unhideResult.combinedOutput)") return } let response = try ExternalCommandRunner.decodeJSONResponse( from: unhideResult, as: CodableJSONResponse<UnhideResult>.self ) #expect(response.success == true) #expect(response.data.action == "unhide") } } // MARK: - Shared Helpers private struct CommandFailure: Error { let status: Int32 let stderr: String } private func runAppCommand( _ args: [String], configure: (@MainActor (StubApplicationService) -> Void)? = nil ) async throws -> String { let (output, _) = try await runAppCommandWithService(args, configure: configure) return output } private func runAppCommandWithService( _ args: [String], configure: (@MainActor (StubApplicationService) -> Void)? = nil ) async throws -> (String, StubApplicationService) { let context = await MainActor.run { makeAppCommandContext() } if let configure { await MainActor.run { configure(context.applicationService) } } let result = try await InProcessCommandRunner.run(args, services: context.services) let output = result.stdout.isEmpty ? result.stderr : result.stdout if result.exitStatus != 0 { throw CommandFailure(status: result.exitStatus, stderr: output) } return (output, context.applicationService) } @MainActor private func makeAppCommandContext() -> AppCommandContext { let data = defaultAppCommandData() let applicationService = StubApplicationService(applications: data.applications, windowsByApp: data.windowsByApp) let windowService = StubWindowService(windowsByApp: data.windowsByApp) let services = TestServicesFactory.makePeekabooServices( applications: applicationService, windows: windowService ) return AppCommandContext(services: services, applicationService: applicationService) } private func appServiceState<T: Sendable>( _ service: StubApplicationService, _ operation: @MainActor (StubApplicationService) -> T ) async -> T { await MainActor.run { operation(service) } } private struct AppCommandContext { let services: PeekabooServices let applicationService: StubApplicationService } @MainActor private func defaultAppCommandData() -> (applications: [ServiceApplicationInfo], windowsByApp: [String: [ServiceWindowInfo]]) { let applications = AppCommandTests.defaultApplications() let windowsByApp = AppCommandTests.defaultWindowsByApp() return (applications, windowsByApp) } extension AppCommandTests { fileprivate static func defaultApplications() -> [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 defaultWindowsByApp() -> [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" ) } } #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