diff --git a/Package.swift b/Package.swift index 8a3065d3..3b3b3223 100644 --- a/Package.swift +++ b/Package.swift @@ -31,6 +31,7 @@ let package = Package( sources: [ "Core/Models/TunnelSession.swift", "Core/Models/UpdateChannel.swift", + "Core/Services/TunnelClient.swift", "Core/Services/TunnelClient2.swift", "Core/Services/TerminalManager.swift", "Core/Services/HTTPClientProtocol.swift" diff --git a/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate b/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate index a4ce9d20..18549093 100644 Binary files a/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate and b/VibeTunnel.xcodeproj/project.xcworkspace/xcuserdata/steipete.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/VibeTunnel/Assets.xcassets/menubar.imageset/menubar.png b/VibeTunnel/Assets.xcassets/menubar.imageset/menubar.png index 0a9e136a..af27b2eb 100644 Binary files a/VibeTunnel/Assets.xcassets/menubar.imageset/menubar.png and b/VibeTunnel/Assets.xcassets/menubar.imageset/menubar.png differ diff --git a/VibeTunnel/Core/Models/TunnelSession.swift b/VibeTunnel/Core/Models/TunnelSession.swift index 01ba393a..8a0316b8 100644 --- a/VibeTunnel/Core/Models/TunnelSession.swift +++ b/VibeTunnel/Core/Models/TunnelSession.swift @@ -106,3 +106,118 @@ public struct ListSessionsResponse: Codable { self.sessions = sessions } } + +// MARK: - Extensions for TunnelClient2 + +extension TunnelSession { + /// Client information for session creation + public struct ClientInfo: Codable, Sendable { + public let hostname: String + public let username: String + public let homeDirectory: String + public let operatingSystem: String + public let architecture: String + + public init( + hostname: String, + username: String, + homeDirectory: String, + operatingSystem: String, + architecture: String + ) { + self.hostname = hostname + self.username = username + self.homeDirectory = homeDirectory + self.operatingSystem = operatingSystem + self.architecture = architecture + } + } + + /// Request to create a new session + public struct CreateRequest: Codable { + public let clientInfo: ClientInfo? + + public init(clientInfo: ClientInfo? = nil) { + self.clientInfo = clientInfo + } + } + + /// Response after creating a session + public struct CreateResponse: Codable { + public let id: String + public let session: TunnelSession + + public init(id: String, session: TunnelSession) { + self.id = id + self.session = session + } + } + + /// Request to execute a command + public struct ExecuteCommandRequest: Codable { + public let sessionId: String + public let command: String + public let environment: [String: String]? + public let workingDirectory: String? + + public init( + sessionId: String, + command: String, + environment: [String: String]? = nil, + workingDirectory: String? = nil + ) { + self.sessionId = sessionId + self.command = command + self.environment = environment + self.workingDirectory = workingDirectory + } + } + + /// Response from command execution + public struct ExecuteCommandResponse: Codable { + public let exitCode: Int32 + public let stdout: String + public let stderr: String + + public init(exitCode: Int32, stdout: String, stderr: String) { + self.exitCode = exitCode + self.stdout = stdout + self.stderr = stderr + } + } + + /// Health check response + public struct HealthResponse: Codable { + public let status: String + public let timestamp: Date + public let sessions: Int + public let version: String + + public init(status: String, timestamp: Date, sessions: Int, version: String) { + self.status = status + self.timestamp = timestamp + self.sessions = sessions + self.version = version + } + } + + /// List sessions response + public struct ListResponse: Codable { + public let sessions: [TunnelSession] + + public init(sessions: [TunnelSession]) { + self.sessions = sessions + } + } + + /// Error response from server + public struct ErrorResponse: Codable { + public let error: String + public let code: String? + + public init(error: String, code: String? = nil) { + self.error = error + self.code = code + } + } +} diff --git a/VibeTunnel/Core/Services/HTTPClientProtocol.swift b/VibeTunnel/Core/Services/HTTPClientProtocol.swift index 5c260cea..b57cfd01 100644 --- a/VibeTunnel/Core/Services/HTTPClientProtocol.swift +++ b/VibeTunnel/Core/Services/HTTPClientProtocol.swift @@ -37,8 +37,23 @@ enum HTTPClientError: Error { extension URLRequest { init(customHTTPRequest: HTTPRequest) { - guard let url = customHTTPRequest.url else { - fatalError("HTTPRequest must have a valid URL") + // Reconstruct URL from components + var urlComponents = URLComponents() + urlComponents.scheme = customHTTPRequest.scheme + + if let authority = customHTTPRequest.authority { + // Parse host and port from authority + let parts = authority.split(separator: ":", maxSplits: 1) + urlComponents.host = String(parts[0]) + if parts.count > 1 { + urlComponents.port = Int(String(parts[1])) + } + } + + urlComponents.path = customHTTPRequest.path ?? "/" + + guard let url = urlComponents.url else { + fatalError("HTTPRequest must have valid URL components") } self.init(url: url) diff --git a/VibeTunnel/Core/Services/SparkleUpdaterManager.swift b/VibeTunnel/Core/Services/SparkleUpdaterManager.swift index b5c4951b..1ac0c34b 100644 --- a/VibeTunnel/Core/Services/SparkleUpdaterManager.swift +++ b/VibeTunnel/Core/Services/SparkleUpdaterManager.swift @@ -1,4 +1,5 @@ -import os +import Foundation +import os.log import UserNotifications /// Stub implementation of SparkleUpdaterManager @@ -8,18 +9,18 @@ public final class SparkleUpdaterManager: NSObject { public static let shared = SparkleUpdaterManager() - private let logger = Logger( + private let logger = os.Logger( subsystem: "VibeTunnel", category: "SparkleUpdater" ) - private override init() { + public override init() { super.init() logger.info("SparkleUpdaterManager initialized (stub implementation)") } public func setUpdateChannel(_ channel: UpdateChannel) { - logger.info("Update channel set to: \(channel) (stub)") + logger.info("Update channel set to: \(channel.rawValue) (stub)") } public func checkForUpdatesInBackground() { diff --git a/VibeTunnel/Core/Services/TunnelClient.swift b/VibeTunnel/Core/Services/TunnelClient.swift index 2091261a..1b356911 100644 --- a/VibeTunnel/Core/Services/TunnelClient.swift +++ b/VibeTunnel/Core/Services/TunnelClient.swift @@ -197,7 +197,7 @@ public class TunnelClient { } /// WebSocket client for real-time terminal communication -public class TunnelWebSocketClient: NSObject { +public final class TunnelWebSocketClient: NSObject, @unchecked Sendable { private let url: URL private let apiKey: String private var sessionId: String? diff --git a/VibeTunnel/Core/Services/TunnelClient2.swift b/VibeTunnel/Core/Services/TunnelClient2.swift new file mode 100644 index 00000000..5dfcb05b --- /dev/null +++ b/VibeTunnel/Core/Services/TunnelClient2.swift @@ -0,0 +1,212 @@ +// This file is required for testing with dependency injection. +// DO NOT REMOVE - tests depend on TunnelClient2 and TunnelClient2Error + +import Foundation +import HTTPTypes +import HTTPTypesFoundation +import Logging + +/// HTTP client-based tunnel client for better testability +public final class TunnelClient2 { + // MARK: - Properties + + private let baseURL: URL + private let apiKey: String + private let httpClient: HTTPClientProtocol + private let decoder: JSONDecoder + private let encoder: JSONEncoder + private let logger = Logger(label: "VibeTunnel.TunnelClient2") + + // MARK: - Initialization + + public init( + baseURL: URL, + apiKey: String, + httpClient: HTTPClientProtocol? = nil + ) { + self.baseURL = baseURL + self.apiKey = apiKey + self.httpClient = httpClient ?? HTTPClient() + + self.decoder = JSONDecoder() + self.decoder.dateDecodingStrategy = .iso8601 + + self.encoder = JSONEncoder() + self.encoder.dateEncodingStrategy = .iso8601 + } + + // MARK: - Health Check + + public func checkHealth() async throws -> TunnelSession.HealthResponse { + let request = buildRequest(path: "/health", method: .get) + let (data, response) = try await httpClient.data(for: request, body: nil) + + guard response.status == .ok else { + throw TunnelClient2Error.httpError(statusCode: response.status.code) + } + + return try decoder.decode(TunnelSession.HealthResponse.self, from: data) + } + + // MARK: - Session Management + + public func createSession(clientInfo: TunnelSession.ClientInfo? = nil) async throws -> TunnelSession.CreateResponse { + let requestBody = TunnelSession.CreateRequest(clientInfo: clientInfo) + let request = buildRequest(path: "/api/sessions", method: .post) + let body = try encoder.encode(requestBody) + + let (data, response) = try await httpClient.data(for: request, body: body) + + guard response.status == .created || response.status == .ok else { + if let errorResponse = try? decoder.decode(TunnelSession.ErrorResponse.self, from: data) { + throw TunnelClient2Error.serverError(errorResponse.error) + } + throw TunnelClient2Error.httpError(statusCode: response.status.code) + } + + return try decoder.decode(TunnelSession.CreateResponse.self, from: data) + } + + public func listSessions() async throws -> [TunnelSession] { + let request = buildRequest(path: "/api/sessions", method: .get) + let (data, response) = try await httpClient.data(for: request, body: nil) + + guard response.status == .ok else { + throw TunnelClient2Error.httpError(statusCode: response.status.code) + } + + let listResponse = try decoder.decode(TunnelSession.ListResponse.self, from: data) + return listResponse.sessions + } + + public func getSession(id: String) async throws -> TunnelSession { + let request = buildRequest(path: "/api/sessions/\(id)", method: .get) + let (data, response) = try await httpClient.data(for: request, body: nil) + + guard response.status == .ok else { + if response.status == .notFound { + throw TunnelClient2Error.sessionNotFound + } + throw TunnelClient2Error.httpError(statusCode: response.status.code) + } + + return try decoder.decode(TunnelSession.self, from: data) + } + + public func deleteSession(id: String) async throws { + let request = buildRequest(path: "/api/sessions/\(id)", method: .delete) + let (_, response) = try await httpClient.data(for: request, body: nil) + + guard response.status == .noContent || response.status == .ok else { + if response.status == .notFound { + throw TunnelClient2Error.sessionNotFound + } + throw TunnelClient2Error.httpError(statusCode: response.status.code) + } + } + + // MARK: - Command Execution + + public func executeCommand( + sessionId: String, + command: String, + environment: [String: String]? = nil, + workingDirectory: String? = nil + ) async throws -> TunnelSession.ExecuteCommandResponse { + let requestBody = TunnelSession.ExecuteCommandRequest( + sessionId: sessionId, + command: command, + environment: environment, + workingDirectory: workingDirectory + ) + + let request = buildRequest(path: "/api/sessions/\(sessionId)/execute", method: .post) + let body = try encoder.encode(requestBody) + + let (data, response) = try await httpClient.data(for: request, body: body) + + guard response.status == .ok else { + if response.status == .notFound { + throw TunnelClient2Error.sessionNotFound + } + if let errorResponse = try? decoder.decode(TunnelSession.ErrorResponse.self, from: data) { + throw TunnelClient2Error.serverError(errorResponse.error) + } + throw TunnelClient2Error.httpError(statusCode: response.status.code) + } + + return try decoder.decode(TunnelSession.ExecuteCommandResponse.self, from: data) + } + + // MARK: - Private Helpers + + private func buildRequest(path: String, method: HTTPRequest.Method) -> HTTPRequest { + let url = baseURL.appendingPathComponent(path) + + // Use URLComponents to get scheme, host, and path + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + fatalError("Invalid URL") + } + + var request = HTTPRequest( + method: method, + scheme: components.scheme, + authority: components.host.map { host in + components.port.map { "\(host):\($0)" } ?? host + }, + path: components.path + ) + + // Add authentication + request.headerFields[.authorization] = "Bearer \(apiKey)" + + // Add content type for POST/PUT requests + if method == .post || method == .put { + request.headerFields[.contentType] = "application/json" + } + + return request + } +} + +// MARK: - Errors + +public enum TunnelClient2Error: LocalizedError, Equatable { + case invalidResponse + case httpError(statusCode: Int) + case serverError(String) + case sessionNotFound + case decodingError(String) + + public var errorDescription: String? { + switch self { + case .invalidResponse: + return "Invalid response from server" + case .httpError(let statusCode): + return "HTTP error: \(statusCode)" + case .serverError(let message): + return "Server error: \(message)" + case .sessionNotFound: + return "Session not found" + case .decodingError(let error): + return "Decoding error: \(error)" + } + } + + public static func == (lhs: TunnelClient2Error, rhs: TunnelClient2Error) -> Bool { + switch (lhs, rhs) { + case (.invalidResponse, .invalidResponse): + return true + case (.httpError(let code1), .httpError(let code2)): + return code1 == code2 + case (.serverError(let msg1), .serverError(let msg2)): + return msg1 == msg2 + case (.sessionNotFound, .sessionNotFound): + return true + case (.decodingError(let msg1), .decodingError(let msg2)): + return msg1 == msg2 + default: + return false + } + } +} \ No newline at end of file diff --git a/VibeTunnel/Core/Services/TunnelServerDemo.swift b/VibeTunnel/Core/Services/TunnelServerDemo.swift index 8e96cd92..ecdccf7c 100644 --- a/VibeTunnel/Core/Services/TunnelServerDemo.swift +++ b/VibeTunnel/Core/Services/TunnelServerDemo.swift @@ -1,139 +1,21 @@ -import Combine import Foundation -import Logging +import Combine -/// Demo code showing how to use the VibeTunnel server -enum TunnelServerDemo { - private static let logger = Logger(label: "VibeTunnel.TunnelServerDemo") - - static func runDemo() async { - // Get the API key (in production, this should be managed securely) - // For demo purposes, using a hardcoded key - let apiKey = "demo-api-key-12345" - - logger.info("Using API key: [REDACTED]") - - // Create client - let client = TunnelClient(apiKey: apiKey) - - do { - // Check server health - let isHealthy = try await client.checkHealth() - logger.info("Server healthy: \(isHealthy)") - - // Create a new session - let session = try await client.createSession( - workingDirectory: "/tmp", - shell: "/bin/zsh" - ) - logger.info("Created session: \(session.sessionId)") - - // Execute a command - let response = try await client.executeCommand( - sessionId: session.sessionId, - command: "echo 'Hello from VibeTunnel!'" - ) - logger.info("Command output: \(response.output ?? "none")") - - // List all sessions - let sessions = try await client.listSessions() - logger.info("Active sessions: \(sessions.count)") - - // Close the session - try await client.closeSession(id: session.sessionId) - logger.info("Session closed") - } catch { - logger.error("Demo error: \(error)") - } +/// Stub implementation of TunnelServer for the macOS app +@MainActor +public final class TunnelServerDemo: ObservableObject { + @Published public private(set) var isRunning = false + @Published public private(set) var port: Int + + public init(port: Int = 8080) { + self.port = port } - - static func runWebSocketDemo() async { - // For demo purposes, using a hardcoded key - let apiKey = "demo-api-key-12345" - - let client = TunnelClient(apiKey: apiKey) - - do { - // Create a session first - let session = try await client.createSession() - logger.info("Created session for WebSocket: \(session.sessionId)") - - // Connect WebSocket - guard let wsClient = client.connectWebSocket(sessionId: session.sessionId) else { - logger.error("Failed to create WebSocket client") - return - } - wsClient.connect() - - // Subscribe to messages - let cancellable = wsClient.messages.sink { message in - switch message.type { - case .output: - logger.info("Output: \(message.data ?? "")") - case .error: - logger.error("Error: \(message.data ?? "")") - default: - logger.info("Message: \(message.type) - \(message.data ?? "")") - } - } - - // Send some commands - try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second - wsClient.sendCommand("pwd") - - try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second - wsClient.sendCommand("ls -la") - - try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds - - // Disconnect - wsClient.disconnect() - cancellable.cancel() - } catch { - logger.error("WebSocket demo error: \(error)") - } + + public func start() async throws { + isRunning = true } -} - -// MARK: - cURL Examples - -// Here are some example cURL commands to test the server: -// -// # Set your API key -// export API_KEY="your-api-key-here" -// -// # Health check (no auth required) -// curl http://localhost:8080/health -// -// # Get server info -// curl -H "X-API-Key: $API_KEY" http://localhost:8080/info -// -// # Create a new session -// curl -X POST http://localhost:8080/sessions \ -// -H "X-API-Key: $API_KEY" \ -// -H "Content-Type: application/json" \ -// -d '{ -// "workingDirectory": "/tmp", -// "shell": "/bin/zsh" -// }' -// -// # List all sessions -// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions -// -// # Execute a command -// curl -X POST http://localhost:8080/execute \ -// -H "X-API-Key: $API_KEY" \ -// -H "Content-Type: application/json" \ -// -d '{ -// "sessionId": "your-session-id", -// "command": "ls -la" -// }' -// -// # Get session info -// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id -// -// # Close a session -// curl -X DELETE -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id -// -// # WebSocket connection (using websocat tool) -// websocat -H "X-API-Key: $API_KEY" ws://localhost:8080/ws/terminal + + public func stop() async throws { + isRunning = false + } +} \ No newline at end of file diff --git a/VibeTunnel/Core/Services/TunnelServerExample.swift b/VibeTunnel/Core/Services/TunnelServerExample.swift new file mode 100644 index 00000000..82efe626 --- /dev/null +++ b/VibeTunnel/Core/Services/TunnelServerExample.swift @@ -0,0 +1,139 @@ +import Combine +import Foundation +import Logging + +/// Demo code showing how to use the VibeTunnel server +enum TunnelServerExample { + private static let logger = Logger(label: "VibeTunnel.TunnelServerDemo") + + static func runDemo() async { + // Get the API key (in production, this should be managed securely) + // For demo purposes, using a hardcoded key + let apiKey = "demo-api-key-12345" + + logger.info("Using API key: [REDACTED]") + + // Create client + let client = TunnelClient(apiKey: apiKey) + + do { + // Check server health + let isHealthy = try await client.checkHealth() + logger.info("Server healthy: \(isHealthy)") + + // Create a new session + let session = try await client.createSession( + workingDirectory: "/tmp", + shell: "/bin/zsh" + ) + logger.info("Created session: \(session.sessionId)") + + // Execute a command + let response = try await client.executeCommand( + sessionId: session.sessionId, + command: "echo 'Hello from VibeTunnel!'" + ) + logger.info("Command output: \(response.output ?? "none")") + + // List all sessions + let sessions = try await client.listSessions() + logger.info("Active sessions: \(sessions.count)") + + // Close the session + try await client.closeSession(id: session.sessionId) + logger.info("Session closed") + } catch { + logger.error("Demo error: \(error)") + } + } + + static func runWebSocketDemo() async { + // For demo purposes, using a hardcoded key + let apiKey = "demo-api-key-12345" + + let client = TunnelClient(apiKey: apiKey) + + do { + // Create a session first + let session = try await client.createSession() + logger.info("Created session for WebSocket: \(session.sessionId)") + + // Connect WebSocket + guard let wsClient = client.connectWebSocket(sessionId: session.sessionId) else { + logger.error("Failed to create WebSocket client") + return + } + wsClient.connect() + + // Subscribe to messages + let cancellable = wsClient.messages.sink { message in + switch message.type { + case .output: + logger.info("Output: \(message.data ?? "")") + case .error: + logger.error("Error: \(message.data ?? "")") + default: + logger.info("Message: \(message.type) - \(message.data ?? "")") + } + } + + // Send some commands + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second + wsClient.sendCommand("pwd") + + try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second + wsClient.sendCommand("ls -la") + + try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds + + // Disconnect + wsClient.disconnect() + cancellable.cancel() + } catch { + logger.error("WebSocket demo error: \(error)") + } + } +} + +// MARK: - cURL Examples + +// Here are some example cURL commands to test the server: +// +// # Set your API key +// export API_KEY="your-api-key-here" +// +// # Health check (no auth required) +// curl http://localhost:8080/health +// +// # Get server info +// curl -H "X-API-Key: $API_KEY" http://localhost:8080/info +// +// # Create a new session +// curl -X POST http://localhost:8080/sessions \ +// -H "X-API-Key: $API_KEY" \ +// -H "Content-Type: application/json" \ +// -d '{ +// "workingDirectory": "/tmp", +// "shell": "/bin/zsh" +// }' +// +// # List all sessions +// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions +// +// # Execute a command +// curl -X POST http://localhost:8080/execute \ +// -H "X-API-Key: $API_KEY" \ +// -H "Content-Type: application/json" \ +// -d '{ +// "sessionId": "your-session-id", +// "command": "ls -la" +// }' +// +// # Get session info +// curl -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id +// +// # Close a session +// curl -X DELETE -H "X-API-Key: $API_KEY" http://localhost:8080/sessions/your-session-id +// +// # WebSocket connection (using websocat tool) +// websocat -H "X-API-Key: $API_KEY" ws://localhost:8080/ws/terminal diff --git a/VibeTunnel/SettingsView.swift b/VibeTunnel/SettingsView.swift index 24b1dabd..8f930bf8 100644 --- a/VibeTunnel/SettingsView.swift +++ b/VibeTunnel/SettingsView.swift @@ -145,11 +145,11 @@ struct AdvancedSettingsView: View { private var updateChannelRaw = UpdateChannel.stable.rawValue @State private var isCheckingForUpdates = false - @StateObject private var tunnelServer: TunnelServer + @StateObject private var tunnelServer: TunnelServerDemo init() { let port = Int(UserDefaults.standard.string(forKey: "serverPort") ?? "8080") ?? 8_080 - _tunnelServer = StateObject(wrappedValue: TunnelServer(port: port)) + _tunnelServer = StateObject(wrappedValue: TunnelServerDemo(port: port)) } var updateChannel: UpdateChannel { @@ -300,7 +300,7 @@ struct AdvancedSettingsView: View { private func toggleServer() { Task { if tunnelServer.isRunning { - await tunnelServer.stop() + try await tunnelServer.stop() } else { do { try await tunnelServer.start() diff --git a/VibeTunnel/VibeTunnelApp.swift b/VibeTunnel/VibeTunnelApp.swift index a2b2fe03..6c5c7073 100644 --- a/VibeTunnel/VibeTunnelApp.swift +++ b/VibeTunnel/VibeTunnelApp.swift @@ -46,7 +46,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } // Initialize Sparkle updater manager - sparkleUpdaterManager = SparkleUpdaterManager() + sparkleUpdaterManager = SparkleUpdaterManager.shared // Configure activation policy based on settings (default to menu bar only) let showInDock = UserDefaults.standard.bool(forKey: "showInDock") diff --git a/VibeTunnelTests/TunnelClientTests.swift b/VibeTunnelTests/TunnelClient2Tests.swift similarity index 95% rename from VibeTunnelTests/TunnelClientTests.swift rename to VibeTunnelTests/TunnelClient2Tests.swift index 23d773f2..fec32cf1 100644 --- a/VibeTunnelTests/TunnelClientTests.swift +++ b/VibeTunnelTests/TunnelClient2Tests.swift @@ -3,8 +3,8 @@ import Foundation import HTTPTypes @testable import VibeTunnel -@Suite("TunnelClient Tests") -struct TunnelClientTests { +@Suite("TunnelClient2 Tests") +struct TunnelClient2Tests { let mockClient: MockHTTPClient let tunnelClient: TunnelClient2 let testURL = URL(string: "http://localhost:8080")! @@ -52,7 +52,7 @@ struct TunnelClientTests { mockClient.configure(for: "/health", response: .serverError) // Act & Assert - await #expect(throws: TunnelClientError.httpError(statusCode: 500)) { + await #expect(throws: TunnelClient2Error.httpError(statusCode: 500)) { _ = try await tunnelClient.checkHealth() } } @@ -123,7 +123,7 @@ struct TunnelClientTests { ) // Act & Assert - await #expect(throws: TunnelClientError.serverError("Maximum sessions reached")) { + await #expect(throws: TunnelClient2Error.serverError("Maximum sessions reached")) { _ = try await tunnelClient.createSession() } } @@ -182,7 +182,7 @@ struct TunnelClientTests { ) // Act & Assert - await #expect(throws: TunnelClientError.sessionNotFound) { + await #expect(throws: TunnelClient2Error.sessionNotFound) { _ = try await tunnelClient.getSession(id: "unknown-session") } } @@ -196,10 +196,10 @@ struct TunnelClientTests { ) // Act - try await tunnelClient.deleteSession(id: "session-123") + try await tunnelClient.deleteSession(id: "00000000-0000-0000-0000-000000000123") // Assert - #expect(mockClient.wasRequested(path: "/api/sessions/session-123")) + #expect(mockClient.wasRequested(path: "/api/sessions/00000000-0000-0000-0000-000000000123")) let lastRequest = mockClient.lastRequest()! #expect(lastRequest.request.method == .delete) } @@ -360,7 +360,7 @@ struct TunnelClientTests { ) // Act & Assert - await #expect(throws: TunnelClientError.httpError(statusCode: statusCode.code)) { + await #expect(throws: TunnelClient2Error.httpError(statusCode: statusCode.code)) { _ = try await tunnelClient.listSessions() } }