app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 4544abe63f25ee09fc4c779d4b4023fe86a19f9c
parent 1432018c9315751b20ff11b067521fcaa7d5264d
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 20:22:59 +0000

fix(runtime): render safe native startup failures

- catch native core construction failures at the Compose root
- display approved FFI messages without exposing internal exceptions
- preserve one-time application initialization across recomposition
- verify generic startup failures redact their original detail

Diffstat:
Mapp/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt | 15+++++++++++++++
Mapp/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt | 12+++++++++++-
Mapp/desktop/src/test/kotlin/org/radroots/studio/application/RadrootsApplicationTest.kt | 17+++++++++++++++++
3 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt @@ -47,6 +47,21 @@ data class StudioUiActions( ) @Composable +fun StartupFailureScreen(problem: String) { + Column( + modifier = Modifier + .fillMaxSize() + .background(WindowBackgroundColor) + .padding(24.dp) + .testTag("startup-failure"), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + BasicText("radroots") + BasicText(problem, Modifier.testTag("startup-problem")) + } +} + +@Composable fun StudioScreen( model: StudioUiModel, actions: StudioUiActions, diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt @@ -9,8 +9,10 @@ import java.awt.datatransfer.StringSelection import kotlinx.coroutines.CoroutineScope import org.radroots.studio.accounts.ui.StudioScreen import org.radroots.studio.accounts.ui.StudioUiActions +import org.radroots.studio.accounts.ui.StartupFailureScreen import org.radroots.studio.accounts.ui.toUiModel import org.radroots.studio.ffi.StudioAppCore +import org.radroots.studio.ffi.StudioException internal typealias StudioStoreFactory = (CoroutineScope) -> StudioAppStore @@ -19,7 +21,15 @@ fun RadrootsApplication( storeFactory: StudioStoreFactory = ::createStudioAppStore, ) { val scope = rememberCoroutineScope() - val store = remember { storeFactory(scope) } + val storeResult = remember { runCatching { storeFactory(scope) } } + val store = storeResult.getOrNull() + if (store == null) { + val error = storeResult.exceptionOrNull() + val message = (error as? StudioException.Failure)?.safeMessage + ?: "The application could not start." + StartupFailureScreen(message) + return + } DisposableEffect(store) { onDispose(store::close) diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/application/RadrootsApplicationTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/application/RadrootsApplicationTest.kt @@ -9,6 +9,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -52,6 +54,21 @@ class RadrootsApplicationTest { assertEquals(1, factoryCalls) assertEquals(true, gateway?.closed) } + + @OptIn(ExperimentalTestApi::class) + @Test + fun applicationRendersSafeStartupFailureWithoutLeakingInternalMessage() = + runComposeUiTest { + setContent { + RadrootsApplication { + error("sensitive internal startup detail") + } + } + + onNodeWithTag("startup-failure").assertIsDisplayed() + onNodeWithText("The application could not start.").assertIsDisplayed() + onAllNodesWithText("sensitive internal startup detail").assertCountEquals(0) + } } private class ApplicationGateway : StudioCoreGateway {