From 9d8856efea8c50a1d9496da697fbce2232267109 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 1 Jul 2026 12:53:32 +0300 Subject: [PATCH] fix(privacylock): provide CompositionLocals inside App() for tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failure: AppStateMachineTest called App() directly, bypassing the outer Window { CompositionLocalProvider } shell in Main.kt. DesktopMessagesLockGate read LocalMessagesLockState and hit the compositionLocalOf { error(...) } trap. Fix: construct the state holder + provide both LocalMessagesLockState and LocalPrivacyLockSettings INSIDE App() itself. Extracted the body of App() into a private AppInner() so the provider can wrap it cleanly. The outer providers are removed from Main.kt — no longer needed. Trade-off: MessagesLockState is now scoped to App() (via rememberCoroutineScope) instead of windowScope. That means it rebuilds on appRestartKey change, which is intentional — an app restart should reset the coroutines too. The seeded initial value is still read synchronously from prefs so the first composition sees the correct LockState (deep-link race fix preserved). Also fixes an unrelated `!!` warning on existingHash in SetPasswordDialog by using a safe smart-cast check. --- .../vitorpamplona/amethyst/desktop/Main.kt | 98 ++++++++++++++++--- .../desktop/security/SetPasswordDialog.kt | 2 +- 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index cfb810b772..6cdf9b9205 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -291,20 +291,9 @@ fun main() { // Callback set by App() for single pane navigation from MenuBar var navigateToScreen by remember { mutableStateOf<((DeckColumnType) -> Unit)?>(null) } - // Messages privacy lock: app-global settings + state holder. Initial - // LockState is seeded synchronously inside MessagesLockState from - // the java.util.prefs value so the first composition sees the correct - // state (deep-link race fix, plan §Security Hardening H1). - val privacyLockSettings = - remember { - com.vitorpamplona.amethyst.commons.privacylock - .PreferencesPrivacyLockSettings() - } - val messagesLockState = - remember { - com.vitorpamplona.amethyst.commons.privacylock - .MessagesLockState(privacyLockSettings, windowScope) - } + // Messages privacy lock CompositionLocals are provided inside App() + // itself (see App() around line ~700) so tests that call App() + // directly — bypassing this Main.kt Window shell — still get them. // Window title-bar / taskbar thumbnail icon. On macOS the source logo // is wrapped in a squircle so it matches every other dock icon; on @@ -617,8 +606,6 @@ fun main() { LocalWindowState provides windowState, LocalAwtWindow provides window, LocalIsImmersiveFullscreen provides immersiveFullscreenState, - com.vitorpamplona.amethyst.commons.privacylock.LocalMessagesLockState provides messagesLockState, - com.vitorpamplona.amethyst.desktop.security.LocalPrivacyLockSettings provides privacyLockSettings, ) { key(appRestartKey) { CompositionLocalProvider( @@ -696,6 +683,85 @@ fun App( val singlePaneState = remember { SinglePaneState() } val pinnedNavBarState = remember { PinnedNavBarState(workspaceManager).also { it.loadFromWorkspace() } } + // Messages privacy lock — app-global settings + state holder, scoped to + // App() so they survive appRestartKey rebuilds but rebuild on genuine app + // restart. Provided as CompositionLocals right here so both production + // (called from application { Window { App() } }) and tests (which call + // App() directly, bypassing outer providers) see them. + val appScope = rememberCoroutineScope() + val privacyLockSettings = + remember { + com.vitorpamplona.amethyst.commons.privacylock + .PreferencesPrivacyLockSettings() + } + val messagesLockState = + remember(privacyLockSettings) { + com.vitorpamplona.amethyst.commons.privacylock + .MessagesLockState(privacyLockSettings, appScope) + } + + CompositionLocalProvider( + com.vitorpamplona.amethyst.commons.privacylock.LocalMessagesLockState provides messagesLockState, + com.vitorpamplona.amethyst.desktop.security.LocalPrivacyLockSettings provides privacyLockSettings, + ) { + AppInner( + layoutMode = layoutMode, + onLayoutModeChange = onLayoutModeChange, + deckState = deckState, + workspaceManager = workspaceManager, + accountManager = accountManager, + showComposeDialog = showComposeDialog, + showAppDrawer = showAppDrawer, + onShowComposeDialog = onShowComposeDialog, + onShowReplyDialog = onShowReplyDialog, + onDismissComposeDialog = onDismissComposeDialog, + onDismissAppDrawer = onDismissAppDrawer, + onShowAppDrawer = onShowAppDrawer, + replyToNote = replyToNote, + showImportFollowListDialog = showImportFollowListDialog, + onShowImportFollowListDialog = onShowImportFollowListDialog, + onDismissImportFollowListDialog = onDismissImportFollowListDialog, + onRestartApp = onRestartApp, + torManager = torManager, + torTypeFlow = torTypeFlow, + externalPortFlow = externalPortFlow, + initialTorSettings = initialTorSettings, + onNavigateToScreen = onNavigateToScreen, + testOverrides = testOverrides, + singlePaneState = singlePaneState, + pinnedNavBarState = pinnedNavBarState, + ) + } +} + +@Composable +private fun AppInner( + layoutMode: LayoutMode, + onLayoutModeChange: (LayoutMode) -> Unit, + deckState: DeckState, + workspaceManager: WorkspaceManager, + accountManager: AccountManager, + showComposeDialog: Boolean, + showAppDrawer: Boolean, + onShowComposeDialog: () -> Unit, + onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit, + onDismissComposeDialog: () -> Unit, + onDismissAppDrawer: () -> Unit, + onShowAppDrawer: () -> Unit, + replyToNote: com.vitorpamplona.quartz.nip01Core.core.Event?, + showImportFollowListDialog: Boolean, + onShowImportFollowListDialog: () -> Unit, + onDismissImportFollowListDialog: () -> Unit, + onRestartApp: () -> Unit, + torManager: com.vitorpamplona.amethyst.commons.tor.ITorManager, + torTypeFlow: kotlinx.coroutines.flow.MutableStateFlow, + externalPortFlow: kotlinx.coroutines.flow.MutableStateFlow, + initialTorSettings: com.vitorpamplona.amethyst.commons.tor.TorSettings, + onNavigateToScreen: ((DeckColumnType) -> Unit) -> Unit, + testOverrides: LaunchTestOverrides?, + singlePaneState: SinglePaneState, + pinnedNavBarState: PinnedNavBarState, +) { // Register single pane navigation callback for MenuBar shortcuts LaunchedEffect(singlePaneState) { onNavigateToScreen { screen -> singlePaneState.navigate(screen) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/security/SetPasswordDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/security/SetPasswordDialog.kt index 39c50c426a..80840cd6d6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/security/SetPasswordDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/security/SetPasswordDialog.kt @@ -103,7 +103,7 @@ fun SetPasswordDialog( val submit: () -> Unit = { val currentOk = !isChange || - PasswordHasher.verify(current.toCharArray(), existingHash!!) + (existingHash != null && PasswordHasher.verify(current.toCharArray(), existingHash)) when { !currentOk -> { currentError = "Wrong password"