diff --git a/Apps/CLI/Sources/PeekabooCLI/Commands/AI/AgentCommand+Sessions.swift b/Apps/CLI/Sources/PeekabooCLI/Commands/AI/AgentCommand+Sessions.swift index 46996a341..6ed0ea13f 100644 --- a/Apps/CLI/Sources/PeekabooCLI/Commands/AI/AgentCommand+Sessions.swift +++ b/Apps/CLI/Sources/PeekabooCLI/Commands/AI/AgentCommand+Sessions.swift @@ -6,7 +6,6 @@ import PeekabooFoundation import Tachikoma import TauTUI -/// Temporary session info struct until PeekabooAgentService implements session management struct AgentSessionInfo: Codable { let id: String let task: String diff --git a/Apps/CLI/Tests/CLIAutomationTests/AgentResumeCLITests.swift b/Apps/CLI/Tests/CLIAutomationTests/AgentResumeCLITests.swift index c60817596..2081c7d1b 100644 --- a/Apps/CLI/Tests/CLIAutomationTests/AgentResumeCLITests.swift +++ b/Apps/CLI/Tests/CLIAutomationTests/AgentResumeCLITests.swift @@ -1,301 +1,85 @@ import Foundation -import PeekabooCore import Testing @testable import PeekabooCLI @Suite(.tags(.safe)) struct AgentResumeCLITests { - // MARK: - Command Line Argument Tests - @Test - func `AgentCommand has resume option`() throws { - // Verify that the AgentCommand struct has the resume property - // This is a compile-time test to ensure the property exists + func `Agent defaults to no task or resume selection`() throws { let command = try AgentCommand.parse([]) - // The resume property should be optional and default to nil - #expect(command.resume == false) - } - - @Test - func `AgentCommand task is optional for resume functionality`() throws { - // Verify that task is now optional to support resume without initial task - let command = try AgentCommand.parse([]) - - // Task should be optional #expect(command.task == nil) + #expect(!command.resume) + #expect(command.resumeSession == nil) } - // MARK: - Resume Command Validation Tests - - @Test - func `Resume validation handles empty session ID`() { - let resumeSessionId = "" - let shouldShowRecentSessions = resumeSessionId.isEmpty - #expect(shouldShowRecentSessions == true) - } - - @Test - func `Resume validation handles valid session ID`() { - let resumeSessionId = "valid-session-123" - let shouldShowRecentSessions = resumeSessionId.isEmpty - #expect(shouldShowRecentSessions == false) - } - - // MARK: - Error Message Tests - - @Test - func `Error messages are properly formatted`() throws { - // Test JSON error format - let jsonError = ["success": false, "error": "Session not found"] as [String: Any] - #expect(jsonError["success"] as? Bool == false) - #expect(jsonError["error"] as? String == "Session not found") - - // Test that error can be serialized to JSON - do { - let jsonData = try JSONSerialization.data(withJSONObject: jsonError, options: .prettyPrinted) - let jsonString = String(data: jsonData, encoding: .utf8) - #expect(jsonString != nil) - #expect(try #require(jsonString).contains("\"success\" : false")) - } catch { - #expect(Bool(false), "JSON serialization should not fail") - } - } - - // TODO: Rewrite these tests. - /* - @Test("Session data formats correctly for JSON output") - func sessionDataFormatsCorrectlyForJSON() async { - let manager = SessionManager.shared - let session = try! await manager.createSession(task: "JSON test task") - - await manager.addMessageToSession(sessionId: session.id, message: .init(role: .user, content: "JSON step")) - //await manager.setLastQuestion(sessionId: session.id, question: "JSON question?") - - let updatedSession = try! await manager.getSession(id: session.id)! - - // Format session data as it would be for JSON output - let sessionData: [String: Any] = [ - "id": updatedSession.id, - "task": updatedSession.summary, - "steps": updatedSession.messages.count, - "lastQuestion": "" as Any, - "createdAt": ISO8601DateFormatter().string(from: updatedSession.createdAt), - "lastActivityAt": ISO8601DateFormatter().string(from: updatedSession.updatedAt) - ] - - #expect(sessionData["id"] as? String == session.id) - #expect(sessionData["task"] as? String == "JSON test task") - #expect(sessionData["steps"] as? Int == 1) - #expect(sessionData["lastQuestion"] as? String == "") - - // Test serialization - do { - let jsonData = try JSONSerialization.data(withJSONObject: sessionData, options: .prettyPrinted) - let jsonString = String(data: jsonData, encoding: .utf8) - #expect(jsonString != nil) - #expect(jsonString!.contains("JSON test task")) - } catch { - #expect(Bool(false), "Session data should serialize to JSON") - } - - // Clean up - await manager.deleteSession(id: session.id) - } - */ - - // MARK: - Time Formatting Tests - - @Test - func `Time ago formatting works correctly`() { - let now = Date() - - // Test recent time (less than 1 minute) - let recent = now.addingTimeInterval(-30) // 30 seconds ago - let recentFormatted = self.formatTimeAgoForTest(recent, from: now) - #expect(recentFormatted == "just now") - - // Test minutes ago - let minutesAgo = now.addingTimeInterval(-90) // 1.5 minutes ago - let minutesFormatted = self.formatTimeAgoForTest(minutesAgo, from: now) - #expect(minutesFormatted == "1 minute ago") - - let multipleMinutesAgo = now.addingTimeInterval(-300) // 5 minutes ago - let multipleMinutesFormatted = self.formatTimeAgoForTest(multipleMinutesAgo, from: now) - #expect(multipleMinutesFormatted == "5 minutes ago") - - // Test hours ago - let hoursAgo = now.addingTimeInterval(-3900) // 1.08 hours ago - let hoursFormatted = self.formatTimeAgoForTest(hoursAgo, from: now) - #expect(hoursFormatted == "1 hour ago") - - let multipleHoursAgo = now.addingTimeInterval(-7200) // 2 hours ago - let multipleHoursFormatted = self.formatTimeAgoForTest(multipleHoursAgo, from: now) - #expect(multipleHoursFormatted == "2 hours ago") - - // Test days ago - let daysAgo = now.addingTimeInterval(-86500) // Just over 1 day ago - let daysFormatted = self.formatTimeAgoForTest(daysAgo, from: now) - #expect(daysFormatted == "1 day ago") - - let multipleDaysAgo = now.addingTimeInterval(-172_800) // 2 days ago - let multipleDaysFormatted = self.formatTimeAgoForTest(multipleDaysAgo, from: now) - #expect(multipleDaysFormatted == "2 days ago") - } - - /// Helper function to test time formatting logic - private func formatTimeAgoForTest(_ date: Date, from now: Date = Date()) -> String { - let interval = now.timeIntervalSince(date) - - if interval < 60 { - return "just now" - } else if interval < 3600 { - let minutes = Int(interval / 60) - return "\(minutes) minute\(minutes == 1 ? "" : "s") ago" - } else if interval < 86400 { - let hours = Int(interval / 3600) - return "\(hours) hour\(hours == 1 ? "" : "s") ago" - } else { - let days = Int(interval / 86400) - return "\(days) day\(days == 1 ? "" : "s") ago" - } - } - - // MARK: - Session Display Tests - - // TODO: Rewrite these tests. - /* - @Test("Session list formatting includes all required fields") - func sessionListFormattingIncludesAllRequiredFields() async { - let manager = SessionManager.shared - - // Create test sessions with different characteristics - let session1 = try! await manager.createSession(task: "Simple task") - let session2 = try! await manager.createSession(task: "Complex task with multiple steps") - let session3 = try! await manager.createSession(task: "Task with question") - - // Add different amounts of content - await manager.addMessageToSession(sessionId: session2.id, message: .init(role: .user, content: "Step 1")) - await manager.addMessageToSession(sessionId: session2.id, message: .init(role: .user, content: "Step 2")) - await manager.addMessageToSession(sessionId: session2.id, message: .init(role: .user, content: "Step 3")) - - let sessions = try! await manager.listSessions() - let testSessions = sessions.filter { [session1.id, session2.id, session3.id].contains($0.id) } - - #expect(testSessions.count == 3) - - // Verify each session has the required fields for display - for session in testSessions { - #expect(!session.id.isEmpty) - #expect(session.summary != nil) - #expect(session.createdAt <= Date()) - #expect(session.lastAccessedAt <= Date()) - } - - // Verify specific session characteristics - let simpleSession = testSessions.first { $0.id == session1.id } - #expect(simpleSession?.messageCount == 0) - - let complexSession = testSessions.first { $0.id == session2.id } - #expect(complexSession?.messageCount == 3) - - // Clean up - await manager.deleteSession(id: session1.id) - await manager.deleteSession(id: session2.id) - await manager.deleteSession(id: session3.id) - } - */ - - // MARK: - Resume Prompt Construction Tests - @Test - func `Resume prompt is constructed correctly`() { - _ = "Open TextEdit" // Original task - let continuationTask = "Now save the document" - - let expectedPrompt = "Continue with the original task. The user's response: \(continuationTask)" + func `Resume subcommand selects latest session without an ID`() throws { + let command = try AgentResumeSubcommand.parse([]) - #expect(expectedPrompt == "Continue with the original task. The user's response: Now save the document") - - // Test with different continuation tasks - let longContinuation = [ - "This is a very long continuation task", - "that includes multiple instructions", - "and complex requirements", - ].joined(separator: " ") - let longPrompt = "Continue with the original task. The user's response: \(longContinuation)" - - #expect(longPrompt.contains("very long continuation task")) + #expect(command.sessionId == nil) + #expect(!command.options.allowForeground) + #expect(command.options.model == nil) } - // MARK: - Configuration Integration Tests - @Test - func `Resume respects configuration settings`() { - // Test that resume functionality respects the same configuration as regular commands - let defaultModel = "gpt-5.6" - let defaultMaxSteps = 20 - - // These would be the defaults used in resume - #expect(defaultModel == "gpt-5.6") - #expect(defaultMaxSteps == 20) - - // Test that configuration override logic works - let configModel = "claude-opus-4-7" - let configMaxSteps = 30 - - let effectiveModel = configModel // Would be from config if available - let effectiveMaxSteps = configMaxSteps // Would be from config if available - - #expect(effectiveModel == "claude-opus-4-7") - #expect(effectiveMaxSteps == 30) + func `Resume subcommand retains exact ID and execution options`() throws { + let id = "12345678-1234-1234-1234-123456789abc" + let command = try AgentResumeSubcommand.parse([ + id, "--model", "ollama/test-model", "--max-steps", "12", "--allow-foreground", + ]) + + #expect(command.sessionId == id) + #expect(command.options.model == "ollama/test-model") + #expect(command.options.maxSteps == 12) + #expect(command.options.allowForeground) } - // MARK: - Edge Case Tests - - @Test - func `Resume handles special characters in task`() { - _ = "Task with \"quotes\" and 'apostrophes' and {brackets} and " // Special task - let continuationTask = "Continue with émojis 👻 and unicode ∆∇∫" - - let resumePrompt = "Continue with the original task. The user's response: \(continuationTask)" - - #expect(resumePrompt.contains("émojis 👻")) - #expect(resumePrompt.contains("unicode ∆∇∫")) + @Test(arguments: [ + "Continue with émojis 👻 and unicode ∆∇∫", + "Task with \"quotes\" and 'apostrophes' and {brackets} and ", + String(repeating: "Long continuation. ", count: 100), + ]) + func `Resume parsing preserves continuation text`(_ task: String) throws { + let command = try AgentCommand.parse([task, "--resume-session", "saved-session"]) + + #expect(command.task == task) + #expect(command.resumeSession == "saved-session") + #expect(!command.resume) } @Test - func `Resume handles very long tasks`() { - _ = String(repeating: "Very long task description. ", count: 100) // Long task - let longContinuation = String(repeating: "Long continuation. ", count: 50) - - let resumePrompt = "Continue with the original task. The user's response: \(longContinuation)" - - #expect(resumePrompt.count > 1000) // Should handle long text - #expect(resumePrompt.contains("Long continuation.")) + func `Session JSON uses the production projection and complete timestamps`() throws { + let command = AgentCommand() + let session = AgentSessionInfo( + id: "saved-session", + task: "Continue \"document\" 👻", + created: Date(timeIntervalSince1970: 1_700_000_000), + lastModified: Date(timeIntervalSince1970: 1_700_000_060), + messageCount: 4, + status: "active", + toolExecutionPolicy: "background_only" + ) + let data = try JSONSerialization.data(withJSONObject: command.sessionJSONObject(session)) + let json = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any]) + + #expect(json["id"] as? String == session.id) + #expect(json["task"] as? String == session.task) + #expect(json["createdAt"] as? String == "2023-11-14T22:13:20Z") + #expect(json["updatedAt"] as? String == "2023-11-14T22:14:20Z") + #expect(json["messageCount"] as? Int == 4) + #expect(json["status"] as? String == "active") + #expect(json["toolExecutionPolicy"] as? String == "background_only") } - // MARK: - Session ID Validation Tests - - @Test - func `Session ID validation works correctly`() { - // Test valid UUID format - let validUUID = UUID().uuidString - #expect(validUUID.count == 36) - #expect(validUUID.contains("-")) - - // Test short ID display (prefix 8 characters) - let shortID = String(validUUID.prefix(8)) - #expect(shortID.count == 8) - #expect(!shortID.contains("-")) - - // Test invalid session IDs - let emptyID = "" - let shortInvalidID = "abc" - let longInvalidID = "this-is-not-a-valid-uuid-format-at-all" + @Test(arguments: [ + (30.0, "just now"), (90.0, "1 minute ago"), (300.0, "5 minutes ago"), + (3900.0, "1 hour ago"), (7200.0, "2 hours ago"), + (86500.0, "1 day ago"), (172_800.0, "2 days ago"), + ]) + func `Session age uses the production formatter`(_ interval: TimeInterval, _ expected: String) { + let now = Date(timeIntervalSince1970: 1_700_000_000) - #expect(emptyID.isEmpty) - #expect(shortInvalidID.count < 36) - #expect(longInvalidID.count > 36) // Not a valid UUID format + #expect(PeekabooCLI.formatTimeAgo(now.addingTimeInterval(-interval), from: now) == expected) } } diff --git a/Apps/CLI/Tests/CLIAutomationTests/AgentResumeTests.swift b/Apps/CLI/Tests/CLIAutomationTests/AgentResumeTests.swift deleted file mode 100644 index 43b029a08..000000000 --- a/Apps/CLI/Tests/CLIAutomationTests/AgentResumeTests.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import Testing -@testable import PeekabooCLI -@testable import PeekabooCore - -@Suite(.tags(.safe)) -struct AgentResumeTests { - // MARK: - AgentSessionManager Tests - - // TODO: The SessionManager API has changed. These tests need to be rewritten. - /* - @available(macOS 14.0, *) @Test("AgentSessionManager creates session correctly") - func sessionManagerCreatesSession() async { - let manager = SessionManager() - let task = "Test task" - - let session = try! await manager.createSession(task: task) - - #expect(!session.id.isEmpty) - - let retrievedSession = try! await manager.getSession(id: session.id) - #expect(retrievedSession != nil) - #expect(retrievedSession?.summary == task) - #expect(retrievedSession?.messages.isEmpty == true) - - // Clean up - await manager.deleteSession(id: session.id) - } - - @available(macOS 14.0, *) @Test("AgentSessionManager adds steps correctly") - func sessionManagerAddsSteps() async { - let manager = SessionManager() - let session = try! await manager.createSession(task: "Test task") - - await manager.addMessageToSession( - sessionId: session.id, - message: .init(role: .user, content: "Test step") - ) - - let updatedSession = try! await manager.getSession(id: session.id) - #expect(updatedSession?.messages.count == 1) - #expect(updatedSession?.messages.first?.content.first?.text == "Test step") - - // Clean up - await manager.deleteSession(id: session.id) - } - - @available(macOS 14.0, *) @Test("AgentSessionManager retrieves recent sessions") - func sessionManagerRetrievesRecentSessions() async { - let manager = SessionManager() - - // Create multiple sessions - let session1 = try! await manager.createSession(task: "Task 1") - let session2 = try! await manager.createSession(task: "Task 2") - let session3 = try! await manager.createSession(task: "Task 3") - - // Add some steps to make them different - await manager.addMessageToSession(sessionId: session1.id, message: .init(role: .user, content: "Step 1")) - await manager.addMessageToSession(sessionId: session2.id, message: .init(role: .user, content: "Step 1")) - await manager.addMessageToSession(sessionId: session2.id, message: .init(role: .user, content: "Step 2")) - - let recentSessions = try! await manager.listSessions() - #expect(recentSessions.count >= 3) - - // Sessions should be ordered by last activity (most recent first) - let sessionIds = recentSessions.map { $0.id } - #expect(sessionIds.contains(session1.id)) - #expect(sessionIds.contains(session2.id)) - #expect(sessionIds.contains(session3.id)) - - // Clean up - await manager.deleteSession(id: session1.id) - await manager.deleteSession(id: session2.id) - await manager.deleteSession(id: session3.id) - } - - @available(macOS 14.0, *) @Test("AgentSessionManager handles nonexistent sessions") - func sessionManagerHandlesNonexistentSessions() async { - let manager = SessionManager() - let nonexistentId = "nonexistent-session-id" - - let session = try! await manager.getSession(id: nonexistentId) - #expect(session == nil) - } - - // MARK: - Session Persistence Tests - - @available(macOS 14.0, *) @Test("AgentSessionManager persists sessions to disk") - func sessionManagerPersistsSessions() async { - let manager = SessionManager() - let session = try! await manager.createSession(task: "Persistent task") - - await manager.addMessageToSession( - sessionId: session.id, - message: .init(role: .user, content: "Persistent step") - ) - - // Create a new manager instance to test persistence - let newManager = SessionManager() - - // Give it a moment to load sessions - try! await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - - let retrievedSession = try! await newManager.getSession(id: session.id) - #expect(retrievedSession != nil) - #expect(retrievedSession?.summary == "Persistent task") - #expect(retrievedSession?.messages.count == 1) - - // Clean up - await manager.deleteSession(id: session.id) - } - - // MARK: - AgentCommand Resume Logic Tests - - @available(macOS 14.0, *) @Test("AgentCommand shows recent sessions with empty resume") - func agentCommandShowsRecentSessions() async throws { - // Create a test session first - let manager = SessionManager() - let session = try! await manager.createSession(task: "Test session task") - await manager.addMessageToSession(sessionId: session.id, message: .init(role: .user, content: "Test step")) - - // Test showing recent sessions (we can't easily test the actual command execution, - // but we can test the data retrieval) - let recentSessions = try! await manager.listSessions() - #expect(recentSessions.count >= 1) - - let testSession = recentSessions.first { $0.id == session.id } - #expect(testSession != nil) - #expect(testSession?.summary == "Test session task") - #expect(testSession?.messageCount == 1) - - // Clean up - await manager.deleteSession(id: session.id) - } - - @available(macOS 14.0, *) @Test("AgentCommand validates session resumption") - func agentCommandValidatesSessionResumption() async { - let manager = SessionManager() - - // Test with nonexistent session - let nonexistentSession = try! await manager.getSession(id: "nonexistent-session") - #expect(nonexistentSession == nil) - - // Test with valid session - let session = try! await manager.createSession(task: "Valid session") - let validSession = try! await manager.getSession(id: session.id) - #expect(validSession != nil) - #expect(validSession?.id == session.id) - - // Clean up - await manager.deleteSession(id: session.id) - } - */ -} diff --git a/Apps/CLI/Tests/CLIAutomationTests/AgentShellCommandTests.swift b/Apps/CLI/Tests/CLIAutomationTests/AgentShellCommandTests.swift deleted file mode 100644 index 939d809fe..000000000 --- a/Apps/CLI/Tests/CLIAutomationTests/AgentShellCommandTests.swift +++ /dev/null @@ -1,192 +0,0 @@ -import Foundation -import Testing -@testable import PeekabooCLI - -#if !PEEKABOO_SKIP_AUTOMATION -// TODO: These tests need to be updated for the new agent architecture -/* - @Suite("Agent Shell Command Tests", .tags(.safe)) - struct AgentShellCommandTests { - @Test("Shell function is included in agent tools") - func shellFunctionExists() { - // Verify shell tool is created with correct parameters - let shellTool = OpenAIAgent.makePeekabooTool( - "shell", - "Execute shell commands (use for opening URLs with 'open', running CLI tools, etc)" - ) - - #expect(shellTool.type == "function") - #expect(shellTool.function.name == "peekaboo_shell") - #expect(shellTool.function - .description == "Execute shell commands (use for opening URLs with 'open', running CLI tools, etc)" - ) - - // Check parameters - let params = shellTool.function.parameters.dictionary - #expect(params["type"] as? String == "object") - - let properties = params["properties"] as? [String: Any] - #expect(properties != nil) - - let commandParam = properties?["command"] as? [String: Any] - #expect(commandParam?["type"] as? String == "string") - #expect(commandParam?["description"] as? String == - "Shell command to execute (e.g., 'open https://google.com', 'ls -la', 'echo Hello')" - ) - - let required = params["required"] as? [String] - #expect(required == ["command"]) - } - - @Test("Agent executor handles shell commands") - @available(macOS 14.0, *) - func executorHandlesShellCommand() async throws { - let executor = AgentExecutor(verbose: false) - - // Test echo command - let result = try await executor.executeFunction( - name: "peekaboo_shell", - arguments: """ - {"command": "echo 'Hello from shell'"} - """ - ) - - // Parse result - let data = Data(result.utf8) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - - #expect(json["success"] as? Bool == true) - - let resultData = json["data"] as? [String: Any] - #expect(resultData != nil) - #expect(resultData?["exit_code"] as? Int == 0) - #expect((resultData?["output"] as? String)?.contains("Hello from shell") == true) - } - - @Test("Shell command handles errors correctly") - @available(macOS 14.0, *) - func shellCommandErrorHandling() async throws { - let executor = AgentExecutor(verbose: false) - - // Test command that should fail - let result = try await executor.executeFunction( - name: "peekaboo_shell", - arguments: """ - {"command": "false"} - """ - ) - - // Parse result - let data = Data(result.utf8) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - - #expect(json["success"] as? Bool == false) - - let error = json["error"] as? [String: Any] - #expect(error != nil) - #expect(error?["code"] as? String == "SHELL_COMMAND_FAILED") - #expect((error?["message"] as? String)?.contains("exited with code") == true) - } - - @Test("Shell command respects timeout") - @available(macOS 14.0, *) - func shellCommandTimeout() async throws { - let executor = AgentExecutor(verbose: false) - - // Test command that would hang without timeout - let result = try await executor.executeFunction( - name: "peekaboo_shell", - arguments: """ - {"command": "sleep 5", "timeout": 1} - """ - ) - - // Parse result - let data = Data(result.utf8) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - - // Should fail due to timeout - #expect(json["success"] as? Bool == false) - - let error = json["error"] as? [String: Any] - #expect(error?["code"] as? String == "COMMAND_FAILED") - #expect((error?["message"] as? String)?.contains("timed out") == true) - } - - @Test("Shell command uses zsh") - @available(macOS 14.0, *) - func shellCommandUsesZsh() async throws { - let executor = AgentExecutor(verbose: false) - - // Test zsh-specific syntax - let result = try await executor.executeFunction( - name: "peekaboo_shell", - arguments: """ - {"command": "echo $ZSH_VERSION"} - """ - ) - - // Parse result - let data = Data(result.utf8) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - - #expect(json["success"] as? Bool == true) - - let resultData = json["data"] as? [String: Any] - let output = resultData?["output"] as? String ?? "" - - // Should have zsh version in output (not empty) - #expect(!output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - } - - @Test("Shell command handles complex commands") - @available(macOS 14.0, *) - func shellCommandComplexCommands() async throws { - let executor = AgentExecutor(verbose: false) - - // Test piping and multiple commands - let result = try await executor.executeFunction( - name: "peekaboo_shell", - arguments: """ - {"command": "echo 'test' | tr 'a-z' 'A-Z'"} - """ - ) - - // Parse result - let data = Data(result.utf8) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - - #expect(json["success"] as? Bool == true) - - let resultData = json["data"] as? [String: Any] - let output = resultData?["output"] as? String ?? "" - - #expect(output.trimmingCharacters(in: .whitespacesAndNewlines) == "TEST") - } - - @Test("Shell command validates required parameters") - @available(macOS 14.0, *) - func shellCommandParameterValidation() async throws { - let executor = AgentExecutor(verbose: false) - - // Test missing command parameter - let result = try await executor.executeFunction( - name: "peekaboo_shell", - arguments: """ - {} - """ - ) - - // Parse result - let data = Data(result.utf8) - let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] - - #expect(json["success"] as? Bool == false) - - let error = json["error"] as? [String: Any] - #expect(error?["code"] as? String == "INVALID_ARGUMENTS") - #expect((error?["message"] as? String)?.contains("Shell command requires") == true) - } - } - */ -#endif diff --git a/Apps/CLI/Tests/CLIAutomationTests/ScreenCaptureTests.swift b/Apps/CLI/Tests/CLIAutomationTests/ScreenCaptureTests.swift deleted file mode 100644 index 3bf6bf542..000000000 --- a/Apps/CLI/Tests/CLIAutomationTests/ScreenCaptureTests.swift +++ /dev/null @@ -1,210 +0,0 @@ -import AppKit -import CoreGraphics -import Foundation -import PeekabooCore -import PeekabooFoundation -import Testing -@testable import PeekabooCLI - -#if !PEEKABOO_SKIP_AUTOMATION -// TODO: ScreenCaptureTests commented out - API changes needed (ApplicationFinder, WindowManager missing) -/* - @Suite("ScreenCapture Tests", .serialized, .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationActions)) - struct ScreenCaptureTests { - - @Suite("Display Capture Tests", .tags(.localOnly)) - struct DisplayCaptureTests { - let tempDir: URL - - init() throws { - self.tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: self.tempDir, withIntermediateDirectories: true) - } - - @Test("Captures main display", .enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")) - @MainActor - func capturesMainDisplay() async throws { - let mainDisplayID = CGMainDisplayID() - let outputPath = self.tempDir.appendingPathComponent("main-display.png").path - - // Create screen capture service - let service = PeekabooServices().screenCapture - - // Capture display - let result = try await service.captureScreen(displayIndex: nil) - - // Save the image data to file - try result.imageData.write(to: URL(fileURLWithPath: outputPath)) - - #expect(FileManager.default.fileExists(atPath: outputPath)) - - // Verify it's a valid image - let data = try Data(contentsOf: URL(fileURLWithPath: outputPath)) - #expect(data.count > 1000) // Should be a reasonable size - - // Check PNG header - #expect(data.prefix(8) == Data([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) - } - - @Test("Captures in JPEG format", .enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")) - @MainActor - func capturesInJPEGFormat() async throws { - let mainDisplayID = CGMainDisplayID() - let outputPath = self.tempDir.appendingPathComponent("main-display.jpg").path - - // Create screen capture service - let service = PeekabooServices().screenCapture - - // Capture display - let result = try await service.captureScreen(displayIndex: nil) - - // Save the image data to file as JPEG - // Note: The service returns PNG data, so we need to convert it - if let image = NSImage(data: result.imageData), - let tiffData = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffData), - let jpegData = bitmap.representation(using: .jpeg, properties: [:]) { - try jpegData.write(to: URL(fileURLWithPath: outputPath)) - } - - #expect(FileManager.default.fileExists(atPath: outputPath)) - - // Verify it's a valid JPEG - let data = try Data(contentsOf: URL(fileURLWithPath: outputPath)) - #expect(data.prefix(3) == Data([0xFF, 0xD8, 0xFF])) - } - - @Test( - "Fails with invalid display ID", - .enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true") - ) - @MainActor - func failsWithInvalidDisplayID() async throws { - let invalidDisplayID: CGDirectDisplayID = 999_999 - let outputPath = self.tempDir.appendingPathComponent("invalid.png").path - - await #expect(throws: PeekabooError.self) { - let service = PeekabooServices().screenCapture - - // Try to capture with an invalid display index (very high number) - let _ = try await service.captureScreen(displayIndex: 999999) - } - } - } - - @Suite("Window Capture Tests", .tags(.localOnly)) - struct WindowCaptureTests { - let tempDir: URL - - init() throws { - self.tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: self.tempDir, withIntermediateDirectories: true) - } - - @Test("Captures window by ID", .enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true")) - @MainActor - func capturesWindowByID() async throws { - // First get a valid window ID from Finder - let apps = ApplicationFinder.getAllRunningApplications() - let finder = apps.first { $0.bundle_id == "com.apple.finder" } - let finderApp = try #require(finder) - - let windows = try WindowManager.getWindowsForApp(pid: finderApp.pid) - let window = try #require(windows.first) - - let outputPath = self.tempDir.appendingPathComponent("window.png").path - - let service = PeekabooServices().screenCapture - - // The new API uses app identifier and window index - let result = try await service.captureWindow( - appIdentifier: "com.apple.finder", - windowIndex: 0 - ) - - // Save the image data to file - try result.imageData.write(to: URL(fileURLWithPath: outputPath)) - - #expect(FileManager.default.fileExists(atPath: outputPath)) - - // Verify it's a valid image - let data = try Data(contentsOf: URL(fileURLWithPath: outputPath)) - #expect(data.count > 100) // Should have some content - } - - @Test( - "Fails with invalid window ID", - .enabled(if: ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] == "true") - ) - @MainActor - func failsWithInvalidWindowID() async throws { - await #expect(throws: PeekabooError.self) { - let service = PeekabooServices().screenCapture - - // Try to capture with an invalid app identifier - let _ = try await service.captureWindow( - appIdentifier: "com.invalid.nonexistent.app", - windowIndex: 0 - ) - } - } - } - - @Suite("Permission Error Detection", .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationActions)) - struct PermissionErrorDetectionTests { - @Test("Captures convert to permission errors when appropriate") - @MainActor - func capturesConvertToPermissionErrors() async { - // This test verifies the error conversion logic without requiring actual permissions - let tempPath = FileManager.default.temporaryDirectory - .appendingPathComponent("\(UUID().uuidString).png").path - - // When we don't have permissions, ScreenCaptureKit will throw specific errors - // This test would fail in CI but demonstrates the error handling path - if ProcessInfo.processInfo.environment["RUN_LOCAL_TESTS"] != "true" { - // Skip this test in CI - return - } - - // Attempt to capture without permissions should convert to our error type - do { - let service = PeekabooServices().screenCapture - - let _ = try await service.captureScreen(displayIndex: nil) - } catch let error as PeekabooError { - // If we get a PeekabooError, it should be a permission error - switch error { - case .screenRecordingPermissionDenied: - // Expected when permissions are not granted - break - default: - // Other errors are also valid (display not found, etc) - break - } - } catch { - // Non-PeekabooError means our error handling didn't work - Issue.record("Expected PeekabooError but got \(type(of: error))") - } - } - } - - @Suite("Capture Configuration", .tags(.automation), .enabled(if: CLITestEnvironment.runAutomationActions)) - struct CaptureConfigurationTests { - @Test("Default configuration includes cursor") - func defaultConfigurationIncludesCursor() { - // This is more of a documentation test to ensure our assumptions are correct - // The actual SCStreamConfiguration is created inside ScreenCapture methods - - // We expect: - // - configuration.showsCursor = true - // - configuration.backgroundColor = .black - // - configuration.shouldBeOpaque = true - - // These settings are hardcoded in ScreenCapture.swift - // This test serves as a reminder if we ever want to make them configurable - #expect(Bool(true)) // Configuration is hardcoded as expected - } - } - } - */ -#endif diff --git a/Apps/CLI/Tests/CLIAutomationTests/WaitForElementTests.swift b/Apps/CLI/Tests/CLIAutomationTests/WaitForElementTests.swift deleted file mode 100644 index a5672d7fa..000000000 --- a/Apps/CLI/Tests/CLIAutomationTests/WaitForElementTests.swift +++ /dev/null @@ -1,7 +0,0 @@ -import AppKit -import AXorcist -import Testing -@testable import PeekabooCLI - -// TODO: Re-enable WaitForElementTests once the wait logic is exposed via a public API. -// The old tests referenced the legacy automation cache; Peekaboo now uses snapshots for UI state caching. diff --git a/Core/PeekabooAutomationKit/Tests/PeekabooAutomationKitTests/PlaceholderTests.swift b/Core/PeekabooAutomationKit/Tests/PeekabooAutomationKitTests/PlaceholderTests.swift deleted file mode 100644 index ca5ecc468..000000000 --- a/Core/PeekabooAutomationKit/Tests/PeekabooAutomationKitTests/PlaceholderTests.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Testing - -struct PlaceholderTests { - @Test func placeholder() {} -} diff --git a/Core/PeekabooCore/Tests/PeekabooAgentRuntimeTests/AgentSessionManagerStorageTests.swift b/Core/PeekabooCore/Tests/PeekabooAgentRuntimeTests/AgentSessionManagerStorageTests.swift index 657502fca..1d6a14b03 100644 --- a/Core/PeekabooCore/Tests/PeekabooAgentRuntimeTests/AgentSessionManagerStorageTests.swift +++ b/Core/PeekabooCore/Tests/PeekabooAgentRuntimeTests/AgentSessionManagerStorageTests.swift @@ -158,6 +158,30 @@ struct AgentSessionManagerStorageTests { #expect(summaries.first { $0.id == "expired" }?.status == .expired) } + @Test + @MainActor + func `Session round trip preserves messages and deletion clears cached state`() async throws { + let root = try Self.makeTemporaryDirectory() + defer { try? FileManager.default.removeItem(at: root) } + let manager = try AgentSessionManager(sessionDirectory: root) + let session = Self.session( + id: "round-trip", + messages: [.user("Test session"), .assistant("Saved response")]) + try manager.saveSession(session) + + let reloaded = try AgentSessionManager(sessionDirectory: root) + let loaded = try #require(try await reloaded.loadSession(id: session.id)) + #expect(loaded.messages.count == 2) + #expect(loaded.messages == session.messages) + #expect(reloaded.listSessions().first?.messageCount == 2) + + try await reloaded.deleteSession(id: session.id) + #expect(try await reloaded.loadSession(id: session.id) == nil) + #expect(reloaded.listSessions().isEmpty) + #expect(!FileManager.default.fileExists(atPath: root.appendingPathComponent("round-trip.json").path)) + #expect(try await reloaded.loadSession(id: "missing-session") == nil) + } + private static func makeTemporaryDirectory() throws -> URL { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("AgentSessionManagerStorageTests-\(UUID().uuidString)", isDirectory: true) @@ -169,6 +193,7 @@ struct AgentSessionManagerStorageTests { id: String, status: String? = nil, totalTokens: Int = 0, + messages: [ModelMessage] = [.user("Test session")], createdAt: Date? = nil, updatedAt: Date? = nil) -> AgentSession { @@ -177,7 +202,7 @@ struct AgentSessionManagerStorageTests { return AgentSession( id: id, modelName: "test-model", - messages: [.user("Test session")], + messages: messages, metadata: SessionMetadata(totalTokens: totalTokens, customData: customData), createdAt: createdAt ?? now, updatedAt: updatedAt ?? now)