From a7cef78725003a528363e44143e64aeb39d00047 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Thu, 5 Mar 2026 21:31:13 -0800 Subject: [PATCH] Revert "Fix notification unread persistence when workspaces regain focus (#971)" (#992) This reverts commit 5f43a3fc32045cf63cd4bab97befe8f0756c6c16. --- Sources/AppDelegate.swift | 36 +++++ Sources/TabManager.swift | 12 +- Sources/TerminalNotificationStore.swift | 43 +++-- cmuxTests/CmuxWebViewKeyEquivalentTests.swift | 151 ------------------ tests/test_focus_notification_dismiss.py | 12 +- tests/test_notifications.py | 95 ++++------- tests_v2/test_focus_notification_dismiss.py | 12 +- tests_v2/test_notifications.py | 95 ++++------- 8 files changed, 140 insertions(+), 316 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 88b76d11..95452827 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -1838,6 +1838,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let tab = tabManager.tabs.first(where: { $0.id == tabId }) { tab.triggerNotificationFocusFlash(panelId: surfaceId, requiresSplit: false, shouldFocus: false) } + notificationStore.markRead(forTabId: tabId, surfaceId: surfaceId) } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { @@ -8167,6 +8168,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) #endif + if let notificationId, let store = notificationStore { + markReadIfFocused( + notificationId: notificationId, + tabId: tabId, + surfaceId: surfaceId, + tabManager: context.tabManager, + notificationStore: store + ) + } + #if DEBUG recordMultiWindowNotificationFocusIfNeeded( windowId: context.windowId, @@ -8220,6 +8231,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) #endif + if let notificationId, let store = notificationStore { + markReadIfFocused( + notificationId: notificationId, + tabId: tabId, + surfaceId: surfaceId, + tabManager: tabManager, + notificationStore: store + ) + } #if DEBUG if ProcessInfo.processInfo.environment["CMUX_UI_TEST_JUMP_UNREAD_SETUP"] == "1" { writeJumpUnreadTestData(["jumpUnreadOpenInFallback": "1", "jumpUnreadOpenResult": "1"]) @@ -8279,6 +8299,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent NSRunningApplication.current.activate(options: [.activateAllWindows, .activateIgnoringOtherApps]) } + private func markReadIfFocused( + notificationId: UUID, + tabId: UUID, + surfaceId: UUID?, + tabManager: TabManager, + notificationStore: TerminalNotificationStore + ) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + guard tabManager.selectedTabId == tabId else { return } + if let surfaceId { + guard tabManager.focusedSurfaceId(for: tabId) == surfaceId else { return } + } + notificationStore.markRead(id: notificationId) + } + } + #if DEBUG private func recordMultiWindowNotificationOpenFailureIfNeeded( tabId: UUID, diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 27a23f3f..96e7bdc4 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -599,7 +599,7 @@ class TabManager: ObservableObject { self.focusSelectedTabPanel(previousTabId: previousTabId) self.updateWindowTitleForSelectedTab() if let selectedTabId = self.selectedTabId { - self.flashFocusedPanelIfUnreadAndActive(tabId: selectedTabId) + self.markFocusedPanelReadIfActive(tabId: selectedTabId) } #if DEBUG let dtMs = self.debugWorkspaceSwitchStartTime > 0 @@ -671,7 +671,7 @@ class TabManager: ObservableObject { guard let self else { return } guard let tabId = notification.userInfo?[GhosttyNotificationKey.tabId] as? UUID else { return } guard let surfaceId = notification.userInfo?[GhosttyNotificationKey.surfaceId] as? UUID else { return } - flashPanelIfUnreadAndActive(tabId: tabId, panelId: surfaceId) + markPanelReadOnFocusIfActive(tabId: tabId, panelId: surfaceId) } }) @@ -1596,16 +1596,16 @@ class TabManager: ObservableObject { selectedTabId != pendingTabId } - private func flashFocusedPanelIfUnreadAndActive(tabId: UUID) { + private func markFocusedPanelReadIfActive(tabId: UUID) { let shouldSuppressFlash = suppressFocusFlash suppressFocusFlash = false guard !shouldSuppressFlash else { return } guard AppFocusState.isAppActive() else { return } guard let panelId = focusedPanelId(for: tabId) else { return } - flashPanelIfUnreadAndActive(tabId: tabId, panelId: panelId) + markPanelReadOnFocusIfActive(tabId: tabId, panelId: panelId) } - private func flashPanelIfUnreadAndActive(tabId: UUID, panelId: UUID) { + private func markPanelReadOnFocusIfActive(tabId: UUID, panelId: UUID) { guard selectedTabId == tabId else { return } guard !suppressFocusFlash else { return } guard AppFocusState.isAppActive() else { return } @@ -1614,6 +1614,7 @@ class TabManager: ObservableObject { if let tab = tabs.first(where: { $0.id == tabId }) { tab.triggerNotificationFocusFlash(panelId: panelId, requiresSplit: false, shouldFocus: false) } + notificationStore.markRead(forTabId: tabId, surfaceId: panelId) } private func enqueuePanelTitleUpdate(tabId: UUID, panelId: UUID, title: String) { @@ -1739,6 +1740,7 @@ class TabManager: ObservableObject { guard let notificationStore = AppDelegate.shared?.notificationStore else { return } guard notificationStore.hasUnreadNotification(forTabId: tabId, surfaceId: targetPanelId) else { return } tab.triggerNotificationFocusFlash(panelId: targetPanelId, requiresSplit: false, shouldFocus: true) + notificationStore.markRead(forTabId: tabId, surfaceId: targetPanelId) } } diff --git a/Sources/TerminalNotificationStore.swift b/Sources/TerminalNotificationStore.swift index 1f1dac18..5bb768cb 100644 --- a/Sources/TerminalNotificationStore.swift +++ b/Sources/TerminalNotificationStore.swift @@ -833,9 +833,16 @@ final class TerminalNotificationStore: ObservableObject { let isFocusedSurface = surfaceId == nil || focusedSurfaceId == surfaceId let isFocusedPanel = isActiveTab && isFocusedSurface let isAppFocused = AppFocusState.isAppFocused() - let suppressNativeDelivery = isAppFocused && isFocusedPanel + if isAppFocused && isFocusedPanel { + if !idsToClear.isEmpty { + notifications = updated + center.removeDeliveredNotificationsOffMain(withIdentifiers: idsToClear) + center.removePendingNotificationRequestsOffMain(withIdentifiers: idsToClear) + } + return + } - if WorkspaceAutoReorderSettings.isEnabled() && !suppressNativeDelivery { + if WorkspaceAutoReorderSettings.isEnabled() { AppDelegate.shared?.tabManager?.moveTabToTop(tabId) } @@ -855,11 +862,7 @@ final class TerminalNotificationStore: ObservableObject { center.removeDeliveredNotificationsOffMain(withIdentifiers: idsToClear) center.removePendingNotificationRequestsOffMain(withIdentifiers: idsToClear) } - if suppressNativeDelivery { - Self.runNotificationCustomCommand(notification) - } else { - scheduleUserNotification(notification) - } + scheduleUserNotification(notification) } func markRead(id: UUID) { @@ -990,7 +993,10 @@ final class TerminalNotificationStore: ObservableObject { guard let self, authorized else { return } let content = UNMutableNotificationContent() - content.title = Self.notificationDisplayTitle(notification) + let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String + ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String + ?? "cmux" + content.title = notification.title.isEmpty ? appName : notification.title content.subtitle = notification.subtitle content.body = notification.body content.sound = NotificationSoundSettings.sound() @@ -1013,27 +1019,16 @@ final class TerminalNotificationStore: ObservableObject { if let error { NSLog("Failed to schedule notification: \(error)") } else { - Self.runNotificationCustomCommand(notification) + NotificationSoundSettings.runCustomCommand( + title: content.title, + subtitle: content.subtitle, + body: content.body + ) } } } } - nonisolated private static func notificationDisplayTitle(_ notification: TerminalNotification) -> String { - let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String - ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String - ?? "cmux" - return notification.title.isEmpty ? appName : notification.title - } - - nonisolated private static func runNotificationCustomCommand(_ notification: TerminalNotification) { - NotificationSoundSettings.runCustomCommand( - title: notificationDisplayTitle(notification), - subtitle: notification.subtitle, - body: notification.body - ) - } - private func ensureAuthorization( origin: AuthorizationRequestOrigin, _ completion: @escaping (Bool) -> Void diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index e06895aa..71d7ed96 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -6909,8 +6909,6 @@ final class NotificationDockBadgeTests: XCTestCase { } override func tearDown() { - AppFocusState.overrideIsFocused = nil - AppDelegate.shared = nil TerminalNotificationStore.shared.resetNotificationSettingsPromptHooksForTesting() TerminalNotificationStore.shared.replaceNotificationsForTesting([]) super.tearDown() @@ -7414,155 +7412,6 @@ final class NotificationDockBadgeTests: XCTestCase { XCTAssertEqual(store.latestNotification(forTabId: tabB)?.id, notificationBUnread.id) } - func testFocusedTabNotificationIsStoredWhenNativeDeliveryIsSuppressed() { - let store = TerminalNotificationStore.shared - store.replaceNotificationsForTesting([]) - - let appDelegate = AppDelegate() - let tabManager = TabManager() - appDelegate.tabManager = tabManager - AppDelegate.shared = appDelegate - AppFocusState.overrideIsFocused = true - - guard let tabId = tabManager.selectedTabId else { - XCTFail("Expected selected tab for notification test") - return - } - - store.addNotification( - tabId: tabId, - surfaceId: nil, - title: "Needs input", - subtitle: "", - body: "agent requires user action" - ) - - XCTAssertEqual(store.unreadCount(forTabId: tabId), 1) - guard let latest = store.latestNotification(forTabId: tabId) else { - XCTFail("Expected notification to be stored for focused tab") - return - } - XCTAssertEqual(latest.tabId, tabId) - XCTAssertEqual(latest.title, "Needs input") - XCTAssertEqual(latest.body, "agent requires user action") - XCTAssertFalse(latest.isRead) - } - - func testApplicationDidBecomeActiveDoesNotMarkFocusedNotificationRead() { - let store = TerminalNotificationStore.shared - let appDelegate = AppDelegate() - let tabManager = TabManager() - appDelegate.tabManager = tabManager - appDelegate.notificationStore = store - AppDelegate.shared = appDelegate - AppFocusState.overrideIsFocused = true - - guard let tabId = tabManager.selectedTabId, - let surfaceId = tabManager.focusedSurfaceId(for: tabId) else { - XCTFail("Expected selected tab and focused surface for activation test") - return - } - - let notification = TerminalNotification( - id: UUID(), - tabId: tabId, - surfaceId: surfaceId, - title: "Unread", - subtitle: "", - body: "should persist across app activation", - createdAt: Date(), - isRead: false - ) - store.replaceNotificationsForTesting([notification]) - - appDelegate.applicationDidBecomeActive( - Notification(name: NSApplication.didBecomeActiveNotification) - ) - - XCTAssertTrue(store.hasUnreadNotification(forTabId: tabId, surfaceId: surfaceId)) - XCTAssertFalse(store.notifications[0].isRead) - } - - func testSelectingWorkspaceDoesNotMarkFocusedNotificationRead() { - let store = TerminalNotificationStore.shared - let appDelegate = AppDelegate() - let tabManager = TabManager() - appDelegate.tabManager = tabManager - appDelegate.notificationStore = store - AppDelegate.shared = appDelegate - AppFocusState.overrideIsFocused = true - - guard let originalTabId = tabManager.selectedTabId, - let originalSurfaceId = tabManager.focusedSurfaceId(for: originalTabId) else { - XCTFail("Expected selected tab and focused surface for workspace selection test") - return - } - guard let originalWorkspace = tabManager.tabs.first(where: { $0.id == originalTabId }) else { - XCTFail("Expected original workspace for workspace selection test") - return - } - - let notification = TerminalNotification( - id: UUID(), - tabId: originalTabId, - surfaceId: originalSurfaceId, - title: "Unread", - subtitle: "", - body: "should persist across workspace selection", - createdAt: Date(), - isRead: false - ) - store.replaceNotificationsForTesting([notification]) - - _ = tabManager.addWorkspace(select: true) - tabManager.selectWorkspace(originalWorkspace) - - let drained = expectation(description: "workspace selection side effects drained") - DispatchQueue.main.async { drained.fulfill() } - wait(for: [drained], timeout: 1.0) - - XCTAssertEqual(tabManager.selectedTabId, originalTabId) - XCTAssertTrue(store.hasUnreadNotification(forTabId: originalTabId, surfaceId: originalSurfaceId)) - XCTAssertFalse(store.notifications[0].isRead) - } - - func testNotificationFocusNavigationDoesNotMarkNotificationRead() { - let store = TerminalNotificationStore.shared - let appDelegate = AppDelegate() - let tabManager = TabManager() - appDelegate.tabManager = tabManager - appDelegate.notificationStore = store - AppDelegate.shared = appDelegate - AppFocusState.overrideIsFocused = true - - guard let tabId = tabManager.selectedTabId, - let surfaceId = tabManager.focusedSurfaceId(for: tabId) else { - XCTFail("Expected selected tab and focused surface for notification focus test") - return - } - - let notification = TerminalNotification( - id: UUID(), - tabId: tabId, - surfaceId: surfaceId, - title: "Unread", - subtitle: "", - body: "should persist after notification focus", - createdAt: Date(), - isRead: false - ) - store.replaceNotificationsForTesting([notification]) - - tabManager.focusTabFromNotification(tabId, surfaceId: surfaceId) - - let drained = expectation(description: "notification focus drained") - DispatchQueue.main.async { drained.fulfill() } - wait(for: [drained], timeout: 1.0) - - XCTAssertTrue(store.hasUnreadNotification(forTabId: tabId, surfaceId: surfaceId)) - XCTAssertFalse(store.notifications[0].isRead) - } - func testNotificationIndexesUpdateAfterReadAndClearMutations() { let tab = UUID() let surfaceUnread = UUID() diff --git a/tests/test_focus_notification_dismiss.py b/tests/test_focus_notification_dismiss.py index 14cef434..d7569b65 100755 --- a/tests/test_focus_notification_dismiss.py +++ b/tests/test_focus_notification_dismiss.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -E2E: focusing a panel preserves its notification and triggers a flash. +E2E: focusing a panel clears its notification and triggers a flash. Note: This uses the socket focus command (no assistive access needed). """ @@ -74,12 +74,8 @@ def main() -> int: client.send("x") time.sleep(0.2) - if wait_for_notification(client, surface_id, is_read=True, timeout=2.0): - print("FAIL: Notification became read after focus") - return 1 - items = client.list_notifications() - if not any(item["surface_id"] == surface_id and not item["is_read"] for item in items): - print("FAIL: Notification did not remain present and unread after focus") + if not wait_for_notification(client, surface_id, is_read=True, timeout=2.0): + print("FAIL: Notification did not become read after focus") return 1 final_flash = client.flash_count(term_b) @@ -97,7 +93,7 @@ def main() -> int: except Exception: pass - print("PASS: Focus preserves notification and flashes panel") + print("PASS: Focus clears notification and flashes panel") return 0 except (cmuxError, RuntimeError) as exc: print(f"FAIL: {exc}") diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 23b4bf10..1ac25c4b 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -58,15 +58,6 @@ def wait_for_flash_count(client: cmux, surface: str, minimum: int = 1, timeout: return last -def wait_for_current_workspace(client: cmux, expected: str, timeout: float = 2.0) -> bool: - start = time.time() - while time.time() - start < timeout: - if client.current_workspace() == expected: - return True - time.sleep(0.05) - return client.current_workspace() == expected - - def ensure_two_surfaces(client: cmux) -> list[tuple[int, str, bool]]: surfaces = client.list_surfaces() if len(surfaces) < 2: @@ -224,8 +215,8 @@ def test_rxvt_notification_osc777(client: cmux) -> TestResult: return result -def test_preserve_unread_on_focus_change(client: cmux) -> TestResult: - result = TestResult("Preserve Unread On Panel Focus") +def test_mark_read_on_focus_change(client: cmux) -> TestResult: + result = TestResult("Mark Read On Panel Focus") try: client.clear_notifications() client.reset_flash_counts() @@ -238,88 +229,81 @@ def test_preserve_unread_on_focus_change(client: cmux) -> TestResult: client.set_app_focus(False) client.notify_surface(other[0], "focusread") - items = wait_for_notifications(client, 1) - target = next((n for n in items if n["surface_id"] == other[1]), None) - if target is None or target["is_read"]: - result.failure("Expected unread notification for target surface before focus") - return result + time.sleep(0.1) client.set_app_focus(True) client.focus_surface(other[0]) - count = wait_for_flash_count(client, other[1], minimum=1, timeout=2.0) - if count < 1: - result.failure("Expected flash on panel focus") - return result + time.sleep(0.1) items = client.list_notifications() target = next((n for n in items if n["surface_id"] == other[1]), None) if target is None: result.failure("Expected notification for target surface") - elif target["is_read"]: - result.failure("Expected notification to remain unread on focus") + elif not target["is_read"]: + result.failure("Expected notification to be marked read on focus") else: - result.success("Notification persisted across panel focus") + count = wait_for_flash_count(client, other[1], minimum=1, timeout=2.0) + if count < 1: + result.failure("Expected flash on panel focus dismissal") + else: + result.success("Notification marked read on focus") except Exception as e: result.failure(f"Exception: {e}") return result -def test_preserve_unread_on_app_active(client: cmux) -> TestResult: - result = TestResult("Preserve Unread On App Active") +def test_mark_read_on_app_active(client: cmux) -> TestResult: + result = TestResult("Mark Read On App Active") try: client.clear_notifications() client.set_app_focus(False) client.notify("activate") - items = wait_for_notifications(client, 1) + time.sleep(0.1) + + items = client.list_notifications() if not items or items[0]["is_read"]: result.failure("Expected unread notification before activation") return result client.simulate_app_active() - items = wait_for_notifications(client, 1) + time.sleep(0.1) + + items = client.list_notifications() if not items: result.failure("Expected notification to remain after activation") - elif items[0]["is_read"]: - result.failure("Expected notification to remain unread on app active") + elif not items[0]["is_read"]: + result.failure("Expected notification to be marked read on app active") else: - result.success("Notification persisted across app activation") + result.success("Notification marked read on app active") except Exception as e: result.failure(f"Exception: {e}") return result -def test_preserve_unread_on_tab_switch(client: cmux) -> TestResult: - result = TestResult("Preserve Unread On Tab Switch") +def test_mark_read_on_tab_switch(client: cmux) -> TestResult: + result = TestResult("Mark Read On Tab Switch") try: client.clear_notifications() client.set_app_focus(False) tab1 = client.current_workspace() client.notify("tabswitch") - items = wait_for_notifications(client, 1) - target = next((n for n in items if n["workspace_id"] == tab1), None) - if target is None or target["is_read"]: - result.failure("Expected unread notification for original tab before switching") - return result + time.sleep(0.1) tab2 = client.new_workspace() - if not wait_for_current_workspace(client, tab2): - result.failure("Expected new workspace to become selected") - return result + time.sleep(0.1) client.set_app_focus(True) client.select_workspace(tab1) - if not wait_for_current_workspace(client, tab1): - result.failure("Expected original workspace to become selected again") - return result + time.sleep(0.1) - items = wait_for_notifications(client, 1) + items = client.list_notifications() target = next((n for n in items if n["workspace_id"] == tab1), None) if target is None: result.failure("Expected notification for original tab") - elif target["is_read"]: - result.failure("Expected notification to remain unread on tab switch") + elif not target["is_read"]: + result.failure("Expected notification to be marked read on tab switch") else: - result.success("Notification persisted across tab switch") + result.success("Notification marked read on tab switch") except Exception as e: result.failure(f"Exception: {e}") return result @@ -387,20 +371,11 @@ def test_focus_on_notification_click(client: cmux) -> TestResult: result.failure("Expected notification surface to be focused") return result - items = client.list_notifications() - notification = next((n for n in items if n["surface_id"] == other[1]), None) - if notification is None: - result.failure("Expected notification to remain listed after notification click") - return result - if notification["is_read"]: - result.failure("Expected notification click to preserve unread state") - return result - count = wait_for_flash_count(client, other[1], minimum=1, timeout=2.0) if count < 1: result.failure(f"Expected flash count >= 1, got {count}") else: - result.success("Notification click focuses, flashes, and preserves unread state") + result.success("Notification click focuses and flashes panel") except Exception as e: result.failure(f"Exception: {e}") return result @@ -480,9 +455,9 @@ def run_tests() -> int: results.append(test_kitty_notification_simple(client)) results.append(test_kitty_notification_chunked(client)) results.append(test_rxvt_notification_osc777(client)) - results.append(test_preserve_unread_on_focus_change(client)) - results.append(test_preserve_unread_on_app_active(client)) - results.append(test_preserve_unread_on_tab_switch(client)) + results.append(test_mark_read_on_focus_change(client)) + results.append(test_mark_read_on_app_active(client)) + results.append(test_mark_read_on_tab_switch(client)) results.append(test_flash_on_tab_switch(client)) results.append(test_focus_on_notification_click(client)) results.append(test_restore_focus_on_tab_switch(client)) diff --git a/tests_v2/test_focus_notification_dismiss.py b/tests_v2/test_focus_notification_dismiss.py index 14cef434..d7569b65 100755 --- a/tests_v2/test_focus_notification_dismiss.py +++ b/tests_v2/test_focus_notification_dismiss.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -E2E: focusing a panel preserves its notification and triggers a flash. +E2E: focusing a panel clears its notification and triggers a flash. Note: This uses the socket focus command (no assistive access needed). """ @@ -74,12 +74,8 @@ def main() -> int: client.send("x") time.sleep(0.2) - if wait_for_notification(client, surface_id, is_read=True, timeout=2.0): - print("FAIL: Notification became read after focus") - return 1 - items = client.list_notifications() - if not any(item["surface_id"] == surface_id and not item["is_read"] for item in items): - print("FAIL: Notification did not remain present and unread after focus") + if not wait_for_notification(client, surface_id, is_read=True, timeout=2.0): + print("FAIL: Notification did not become read after focus") return 1 final_flash = client.flash_count(term_b) @@ -97,7 +93,7 @@ def main() -> int: except Exception: pass - print("PASS: Focus preserves notification and flashes panel") + print("PASS: Focus clears notification and flashes panel") return 0 except (cmuxError, RuntimeError) as exc: print(f"FAIL: {exc}") diff --git a/tests_v2/test_notifications.py b/tests_v2/test_notifications.py index 23b4bf10..1ac25c4b 100644 --- a/tests_v2/test_notifications.py +++ b/tests_v2/test_notifications.py @@ -58,15 +58,6 @@ def wait_for_flash_count(client: cmux, surface: str, minimum: int = 1, timeout: return last -def wait_for_current_workspace(client: cmux, expected: str, timeout: float = 2.0) -> bool: - start = time.time() - while time.time() - start < timeout: - if client.current_workspace() == expected: - return True - time.sleep(0.05) - return client.current_workspace() == expected - - def ensure_two_surfaces(client: cmux) -> list[tuple[int, str, bool]]: surfaces = client.list_surfaces() if len(surfaces) < 2: @@ -224,8 +215,8 @@ def test_rxvt_notification_osc777(client: cmux) -> TestResult: return result -def test_preserve_unread_on_focus_change(client: cmux) -> TestResult: - result = TestResult("Preserve Unread On Panel Focus") +def test_mark_read_on_focus_change(client: cmux) -> TestResult: + result = TestResult("Mark Read On Panel Focus") try: client.clear_notifications() client.reset_flash_counts() @@ -238,88 +229,81 @@ def test_preserve_unread_on_focus_change(client: cmux) -> TestResult: client.set_app_focus(False) client.notify_surface(other[0], "focusread") - items = wait_for_notifications(client, 1) - target = next((n for n in items if n["surface_id"] == other[1]), None) - if target is None or target["is_read"]: - result.failure("Expected unread notification for target surface before focus") - return result + time.sleep(0.1) client.set_app_focus(True) client.focus_surface(other[0]) - count = wait_for_flash_count(client, other[1], minimum=1, timeout=2.0) - if count < 1: - result.failure("Expected flash on panel focus") - return result + time.sleep(0.1) items = client.list_notifications() target = next((n for n in items if n["surface_id"] == other[1]), None) if target is None: result.failure("Expected notification for target surface") - elif target["is_read"]: - result.failure("Expected notification to remain unread on focus") + elif not target["is_read"]: + result.failure("Expected notification to be marked read on focus") else: - result.success("Notification persisted across panel focus") + count = wait_for_flash_count(client, other[1], minimum=1, timeout=2.0) + if count < 1: + result.failure("Expected flash on panel focus dismissal") + else: + result.success("Notification marked read on focus") except Exception as e: result.failure(f"Exception: {e}") return result -def test_preserve_unread_on_app_active(client: cmux) -> TestResult: - result = TestResult("Preserve Unread On App Active") +def test_mark_read_on_app_active(client: cmux) -> TestResult: + result = TestResult("Mark Read On App Active") try: client.clear_notifications() client.set_app_focus(False) client.notify("activate") - items = wait_for_notifications(client, 1) + time.sleep(0.1) + + items = client.list_notifications() if not items or items[0]["is_read"]: result.failure("Expected unread notification before activation") return result client.simulate_app_active() - items = wait_for_notifications(client, 1) + time.sleep(0.1) + + items = client.list_notifications() if not items: result.failure("Expected notification to remain after activation") - elif items[0]["is_read"]: - result.failure("Expected notification to remain unread on app active") + elif not items[0]["is_read"]: + result.failure("Expected notification to be marked read on app active") else: - result.success("Notification persisted across app activation") + result.success("Notification marked read on app active") except Exception as e: result.failure(f"Exception: {e}") return result -def test_preserve_unread_on_tab_switch(client: cmux) -> TestResult: - result = TestResult("Preserve Unread On Tab Switch") +def test_mark_read_on_tab_switch(client: cmux) -> TestResult: + result = TestResult("Mark Read On Tab Switch") try: client.clear_notifications() client.set_app_focus(False) tab1 = client.current_workspace() client.notify("tabswitch") - items = wait_for_notifications(client, 1) - target = next((n for n in items if n["workspace_id"] == tab1), None) - if target is None or target["is_read"]: - result.failure("Expected unread notification for original tab before switching") - return result + time.sleep(0.1) tab2 = client.new_workspace() - if not wait_for_current_workspace(client, tab2): - result.failure("Expected new workspace to become selected") - return result + time.sleep(0.1) client.set_app_focus(True) client.select_workspace(tab1) - if not wait_for_current_workspace(client, tab1): - result.failure("Expected original workspace to become selected again") - return result + time.sleep(0.1) - items = wait_for_notifications(client, 1) + items = client.list_notifications() target = next((n for n in items if n["workspace_id"] == tab1), None) if target is None: result.failure("Expected notification for original tab") - elif target["is_read"]: - result.failure("Expected notification to remain unread on tab switch") + elif not target["is_read"]: + result.failure("Expected notification to be marked read on tab switch") else: - result.success("Notification persisted across tab switch") + result.success("Notification marked read on tab switch") except Exception as e: result.failure(f"Exception: {e}") return result @@ -387,20 +371,11 @@ def test_focus_on_notification_click(client: cmux) -> TestResult: result.failure("Expected notification surface to be focused") return result - items = client.list_notifications() - notification = next((n for n in items if n["surface_id"] == other[1]), None) - if notification is None: - result.failure("Expected notification to remain listed after notification click") - return result - if notification["is_read"]: - result.failure("Expected notification click to preserve unread state") - return result - count = wait_for_flash_count(client, other[1], minimum=1, timeout=2.0) if count < 1: result.failure(f"Expected flash count >= 1, got {count}") else: - result.success("Notification click focuses, flashes, and preserves unread state") + result.success("Notification click focuses and flashes panel") except Exception as e: result.failure(f"Exception: {e}") return result @@ -480,9 +455,9 @@ def run_tests() -> int: results.append(test_kitty_notification_simple(client)) results.append(test_kitty_notification_chunked(client)) results.append(test_rxvt_notification_osc777(client)) - results.append(test_preserve_unread_on_focus_change(client)) - results.append(test_preserve_unread_on_app_active(client)) - results.append(test_preserve_unread_on_tab_switch(client)) + results.append(test_mark_read_on_focus_change(client)) + results.append(test_mark_read_on_app_active(client)) + results.append(test_mark_read_on_tab_switch(client)) results.append(test_flash_on_tab_switch(client)) results.append(test_focus_on_notification_click(client)) results.append(test_restore_focus_on_tab_switch(client))