From 9841cb50bfe1bee42c32255e3603f7b7c46a4e7a Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Thu, 19 Mar 2026 01:17:18 -0700 Subject: [PATCH] Add notification surface regression test --- ...erminalControllerSocketSecurityTests.swift | 138 +++++++++++++++++- 1 file changed, 135 insertions(+), 3 deletions(-) diff --git a/cmuxTests/TerminalControllerSocketSecurityTests.swift b/cmuxTests/TerminalControllerSocketSecurityTests.swift index 3ff8ce80..16224347 100644 --- a/cmuxTests/TerminalControllerSocketSecurityTests.swift +++ b/cmuxTests/TerminalControllerSocketSecurityTests.swift @@ -148,6 +148,70 @@ final class TerminalControllerSocketSecurityTests: XCTestCase { XCTAssertEqual(payload["has_ssh_options"] as? Bool, true) } + func testNotificationCreateUsesExplicitSurfaceIDWhenProvided() async throws { + let socketPath = makeSocketPath("notify-surface") + let manager = TabManager() + let store = TerminalNotificationStore.shared + let appDelegate = AppDelegate.shared ?? AppDelegate() + + let originalTabManager = appDelegate.tabManager + let originalNotificationStore = appDelegate.notificationStore + + store.replaceNotificationsForTesting([]) + store.configureNotificationDeliveryHandlerForTesting { _, _ in } + appDelegate.tabManager = manager + appDelegate.notificationStore = store + + defer { + store.replaceNotificationsForTesting([]) + store.resetNotificationDeliveryHandlerForTesting() + appDelegate.tabManager = originalTabManager + appDelegate.notificationStore = originalNotificationStore + } + + guard let workspace = manager.selectedWorkspace, + let focusedPanelId = workspace.focusedPanelId else { + XCTFail("Expected selected workspace with a focused panel") + return + } + guard let targetPanel = workspace.newTerminalSplit(from: focusedPanelId, orientation: .horizontal) else { + XCTFail("Expected split panel to be created") + return + } + workspace.focusPanel(focusedPanelId) + + TerminalController.shared.start( + tabManager: manager, + socketPath: socketPath, + accessMode: .allowAll + ) + try waitForSocket(at: socketPath) + + let response = try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + let response = try self.sendV2Request( + method: "notification.create", + params: [ + "workspace_id": workspace.id.uuidString, + "surface_id": targetPanel.id.uuidString, + "title": "Targeted" + ], + to: socketPath + ) + continuation.resume(returning: response) + } catch { + continuation.resume(throwing: error) + } + } + } + + let result = try XCTUnwrap(response["result"] as? [String: Any]) + XCTAssertEqual(result["surface_id"] as? String, targetPanel.id.uuidString) + XCTAssertTrue(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: targetPanel.id)) + XCTAssertFalse(store.hasUnreadNotification(forTabId: workspace.id, surfaceId: focusedPanelId)) + } + private func waitForSocket(at path: String, timeout: TimeInterval = 2.0) throws { let expectation = XCTNSPredicateExpectation( predicate: NSPredicate { _, _ in @@ -212,7 +276,75 @@ final class TerminalControllerSocketSecurityTests: XCTestCase { return responses } - private func writeLine(_ command: String, to fd: Int32) throws { + private nonisolated func sendV2Request( + method: String, + params: [String: Any], + to socketPath: String + ) throws -> [String: Any] { + let fd = try connect(to: socketPath) + defer { Darwin.close(fd) } + + let payload: [String: Any] = [ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params + ] + let data = try JSONSerialization.data(withJSONObject: payload) + guard let line = String(data: data, encoding: .utf8) else { + throw NSError(domain: NSCocoaErrorDomain, code: 0, userInfo: [ + NSLocalizedDescriptionKey: "Failed to encode JSON-RPC request" + ]) + } + try writeLine(line, to: fd) + + let responseLine = try readLine(from: fd) + let responseData = Data(responseLine.utf8) + return try XCTUnwrap( + try JSONSerialization.jsonObject(with: responseData) as? [String: Any], + "Expected JSON-RPC response object" + ) + } + + private nonisolated func connect(to socketPath: String) throws -> Int32 { + let fd = Darwin.socket(AF_UNIX, SOCK_STREAM, 0) + guard fd >= 0 else { + throw posixError("socket(AF_UNIX)") + } + + var addr = sockaddr_un() + addr.sun_family = sa_family_t(AF_UNIX) + + let bytes = Array(socketPath.utf8) + let maxPathLen = MemoryLayout.size(ofValue: addr.sun_path) + guard bytes.count < maxPathLen else { + Darwin.close(fd) + throw NSError(domain: NSPOSIXErrorDomain, code: Int(ENAMETOOLONG)) + } + + withUnsafeMutablePointer(to: &addr.sun_path) { pathPtr in + let cPath = UnsafeMutableRawPointer(pathPtr).assumingMemoryBound(to: CChar.self) + cPath.initialize(repeating: 0, count: maxPathLen) + for (index, byte) in bytes.enumerated() { + cPath[index] = CChar(bitPattern: byte) + } + } + + let addrLen = socklen_t(MemoryLayout.size + bytes.count + 1) + let connectResult = withUnsafePointer(to: &addr) { ptr -> Int32 in + ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPtr in + Darwin.connect(fd, sockaddrPtr, addrLen) + } + } + guard connectResult == 0 else { + let error = posixError("connect(\(socketPath))") + Darwin.close(fd) + throw error + } + return fd + } + + private nonisolated func writeLine(_ command: String, to fd: Int32) throws { let payload = Array((command + "\n").utf8) var offset = 0 while offset < payload.count { @@ -226,7 +358,7 @@ final class TerminalControllerSocketSecurityTests: XCTestCase { } } - private func readLine(from fd: Int32) throws -> String { + private nonisolated func readLine(from fd: Int32) throws -> String { var buffer = [UInt8](repeating: 0, count: 1) var data = Data() @@ -248,7 +380,7 @@ final class TerminalControllerSocketSecurityTests: XCTestCase { return line } - private func posixError(_ operation: String) -> NSError { + private nonisolated func posixError(_ operation: String) -> NSError { NSError( domain: NSPOSIXErrorDomain, code: Int(errno),