diff --git a/VibeTunnelTests/SessionIdHandlingTests.swift b/VibeTunnelTests/SessionIdHandlingTests.swift new file mode 100644 index 00000000..8afe2bde --- /dev/null +++ b/VibeTunnelTests/SessionIdHandlingTests.swift @@ -0,0 +1,166 @@ +import Testing +import Foundation +@testable import VibeTunnel + +@Suite("Session ID Handling Tests", .tags(.sessionManagement)) +struct SessionIdHandlingTests { + + // MARK: - Session ID Format Validation + + @Test("Session IDs must be valid UUIDs", arguments: [ + "a37ea008c-41f6-412f-bbba-f28f091267ce", // Valid UUID + "00000000-0000-0000-0000-000000000000", // Valid nil UUID + "550e8400-e29b-41d4-a716-446655440000" // Valid UUID v4 + ]) + func testValidSessionIdFormat(sessionId: String) { + #expect(UUID(uuidString: sessionId) != nil) + } + + @Test("Invalid session ID formats are rejected", arguments: [ + "session_1234567890_abc123", // Old format from Swift server + "e blob-http://127.0.0.1:4020/a37ea008c", // Corrupted format from bug + "not-a-uuid", // Random string + "", // Empty string + "123", // Too short + ]) + func testInvalidSessionIdFormat(sessionId: String) { + #expect(UUID(uuidString: sessionId) == nil) + } + + // MARK: - Session ID Comparison Tests + + @Test("Session IDs are case-insensitive for UUID comparison") + func testSessionIdCaseInsensitivity() { + let id1 = "A37EA008C-41F6-412F-BBBA-F28F091267CE" + let id2 = "a37ea008c-41f6-412f-bbba-f28f091267ce" + + let uuid1 = UUID(uuidString: id1) + let uuid2 = UUID(uuidString: id2) + + #expect(uuid1 == uuid2) + } + + // MARK: - Real-World Scenario Tests + + @Test("Parse session ID from various server responses") + func testParseSessionIdFromResponses() throws { + // Test parsing session ID from different response formats + + struct SessionResponse: Codable { + let sessionId: String + } + + // Test cases representing different server response formats + let testCases: [(json: String, expectedId: String?)] = [ + // Correct format (what we fixed the server to return) + (json: #"{"sessionId":"a37ea008c-41f6-412f-bbba-f28f091267ce"}"#, + expectedId: "a37ea008c-41f6-412f-bbba-f28f091267ce"), + + // Old incorrect format (what Swift server used to return) + (json: #"{"sessionId":"session_1234567890_abc123"}"#, + expectedId: "session_1234567890_abc123"), // Would fail UUID validation + + // Empty response + (json: #"{}"#, expectedId: nil) + ] + + for testCase in testCases { + let data = testCase.json.data(using: .utf8)! + + if let expectedId = testCase.expectedId { + let response = try JSONDecoder().decode(SessionResponse.self, from: data) + #expect(response.sessionId == expectedId) + } else { + #expect(throws: Error.self) { + _ = try JSONDecoder().decode(SessionResponse.self, from: data) + } + } + } + } + + // MARK: - URL Path Construction Tests + + @Test("Session ID URL encoding") + func testSessionIdUrlEncoding() { + // Ensure session IDs are properly encoded in URLs + let sessionId = "a37ea008c-41f6-412f-bbba-f28f091267ce" + let baseURL = "http://localhost:4020" + + let inputURL = "\(baseURL)/api/sessions/\(sessionId)/input" + let expectedURL = "http://localhost:4020/api/sessions/a37ea008c-41f6-412f-bbba-f28f091267ce/input" + + #expect(inputURL == expectedURL) + + // Verify URL is valid + #expect(URL(string: inputURL) != nil) + } + + @Test("Corrupted session ID in URL causes invalid URL") + func testCorruptedSessionIdInUrl() { + // The bug showed a corrupted ID like "e blob-http://127.0.0.1:4020/uuid" + let corruptedId = "e blob-http://127.0.0.1:4020/a37ea008c-41f6-412f-bbba-f28f091267ce" + let baseURL = "http://localhost:4020" + + // This would create an invalid URL due to spaces and special characters + let invalidURL = "\(baseURL)/api/sessions/\(corruptedId)/input" + + // URL should be parseable but semantically wrong + if let url = URL(string: invalidURL) { + // The path would be malformed + #expect(url.path.contains(" ")) + } + } + + // MARK: - Session List Parsing Tests + + @Test("Parse tty-fwd session list response") + func testParseTtyFwdSessionList() throws { + // Test parsing the JSON response from tty-fwd --list-sessions + let ttyFwdResponse = """ + { + "a37ea008c-41f6-412f-bbba-f28f091267ce": { + "cmdline": ["zsh"], + "cwd": "/Users/test", + "name": "zsh", + "pid": 12345, + "status": "running", + "started_at": "2024-01-15T10:30:00Z", + "stdin": "/path/to/stdin", + "stream-out": "/path/to/stream-out" + } + } + """ + + let data = ttyFwdResponse.data(using: .utf8)! + let sessions = try JSONDecoder().decode([String: TtyFwdSession].self, from: data) + + // Verify the session ID is a proper UUID + #expect(sessions.count == 1) + let sessionId = sessions.keys.first! + #expect(UUID(uuidString: sessionId) != nil) + + // Verify we can look up the session by its ID + let session = sessions[sessionId] + #expect(session != nil) + #expect(session?.status == "running") + } +} + +// MARK: - Regression Test for Specific Bug + +@Test(.bug("Session ID mismatch causing 404 on input endpoint")) +func testSessionIdMismatchBugFixed() async throws { + // This test documents the specific bug that was fixed: + // 1. Swift server generated: "session_1234567890_abc123" + // 2. tty-fwd generated: "a37ea008c-41f6-412f-bbba-f28f091267ce" + // 3. Client used Swift's ID for input: /api/sessions/session_1234567890_abc123/input + // 4. Server looked up session in tty-fwd's list and found nothing → 404 + + // The fix ensures: + // - Swift server captures tty-fwd's UUID from stdout + // - Returns that UUID to the client + // - All subsequent operations use the correct UUID + + // This test serves as documentation of the bug and its fix + #expect(true) +} \ No newline at end of file diff --git a/VibeTunnelTests/TunnelServerTests.swift b/VibeTunnelTests/TunnelServerTests.swift new file mode 100644 index 00000000..0fa46447 --- /dev/null +++ b/VibeTunnelTests/TunnelServerTests.swift @@ -0,0 +1,142 @@ +import Testing +import Foundation +import HTTPTypes +import Hummingbird +import HummingbirdCore +import NIOCore +@testable import VibeTunnel + +@Suite("TunnelServer Tests") +struct TunnelServerTests { + + // MARK: - Session ID Capture Tests + + @Test("Create session captures UUID from tty-fwd stdout") + func testCreateSessionCapturesSessionId() async throws { + // This test validates that the server correctly captures the session ID + // from tty-fwd's stdout instead of using its own generated name. + // This is critical for the fix we implemented to prevent 404 errors + // when sending input to sessions. + + // Note: This is a unit test that would require mocking TTYForwardManager + // For now, we'll document the expected behavior + + // Expected behavior: + // 1. Server calls tty-fwd with --session-name argument + // 2. tty-fwd prints a UUID to stdout (e.g., "a37ea008c-41f6-412f-bbba-f28f091267ce") + // 3. Server captures this UUID from the pipe + // 4. Server returns this UUID in the response, NOT the session name + + // This ensures the session ID used by clients matches what tty-fwd expects + #expect(true) // Placeholder - would need TTYForwardManager mock + } + + @Test("Create session handles missing session ID from stdout") + func testCreateSessionHandlesMissingSessionId() async throws { + // Test that server properly handles the case where tty-fwd + // doesn't print a session ID to stdout within the timeout period + + // Expected behavior: + // 1. Server waits up to 3 seconds for session ID + // 2. If no ID received, returns error response with appropriate message + // 3. Client receives clear error about session creation failure + + #expect(true) // Placeholder - would need TTYForwardManager mock + } + + // MARK: - API Endpoint Tests + + @Test("Session input endpoint validates session existence") + func testSessionInputValidation() async throws { + // This test validates the /api/sessions/:sessionId/input endpoint + // which was returning 404 due to session ID mismatch + + // Expected behavior: + // 1. Endpoint receives session ID in URL parameter + // 2. Calls tty-fwd --list-sessions to verify session exists + // 3. Returns 404 if session not found in tty-fwd's list + // 4. Returns 400 if session exists but is not running + // 5. Returns 410 if session process is dead + // 6. Successfully sends input if session is valid and running + + #expect(true) // Placeholder - would need full server setup + } + + // MARK: - Error Response Tests + + @Test("Error responses are properly formatted JSON") + func testErrorResponseFormat() async throws { + // Test that all error responses follow consistent JSON format + let server = TunnelServer(port: 0) // Use port 0 for testing + + // Test various error response methods + let errorCases = [ + ("Not found", HTTPResponse.Status.notFound), + ("Bad request", HTTPResponse.Status.badRequest), + ("Internal error", HTTPResponse.Status.internalServerError) + ] + + for (message, status) in errorCases { + // Note: errorResponse is private, so we can't test directly + // In a real test, we'd make HTTP requests to trigger these errors + #expect(status.code >= 400) + } + } + + // MARK: - Integration Test Scenarios + + @Test("Full session lifecycle with correct ID") + func testSessionLifecycle() async throws { + // This integration test validates the complete fix: + // 1. Create session and get UUID from tty-fwd + // 2. List sessions and verify the UUID appears + // 3. Send input using the UUID + // 4. Kill session using the UUID + // 5. Cleanup session using the UUID + + // All operations should succeed without 404 errors + // because we're using the correct session ID throughout + + #expect(true) // Placeholder - would need running server + } + + @Test(.tags(.regression), "Session ID mismatch bug does not regress") + func testSessionIdMismatchRegression() async throws { + // Regression test for the bug where Swift server returned + // its own session name instead of tty-fwd's UUID + + // This test ensures: + // 1. Server NEVER returns a session ID like "session_timestamp_partialUUID" + // 2. Server ALWAYS returns a proper UUID format + // 3. The returned session ID can be used for subsequent operations + + #expect(true) // Placeholder - would need full setup + } +} + +// MARK: - Test Tags + +extension Tag { + @Tag static var regression: Self + @Tag static var sessionManagement: Self + @Tag static var apiEndpoints: Self +} + +// MARK: - Test Helpers + +private extension TunnelServerTests { + // Helper to validate UUID format + func isValidUUID(_ string: String) -> Bool { + UUID(uuidString: string) != nil + } + + // Helper to parse error response + func parseErrorResponse(from data: Data) throws -> String? { + struct ErrorResponse: Codable { + let error: String + } + + let response = try JSONDecoder().decode(ErrorResponse.self, from: data) + return response.error + } +} \ No newline at end of file