commit f0e823fad95bb5b4cd8adb6eb4896a57872e7800
parent c551ff5c6c674bf806ab49c0fde3c935f2191e2a
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 00:12:02 +0000
runtime: finalize removal and startup safety
- derive credential deletion impact from the local binding
- retain cascade-backed account-owned data cleanup
- convert missing or invalid icon resources into safe UI failure
- cover binding-aware impact and resource decoding failure
Diffstat:
3 files changed, 73 insertions(+), 21 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/desktop/Main.kt b/app/desktop/src/main/kotlin/org/radroots/studio/desktop/Main.kt
@@ -9,6 +9,7 @@ import java.awt.Dimension
import java.awt.Taskbar
import javax.imageio.ImageIO
import org.radroots.studio.application.RadrootsApplication
+import org.radroots.studio.accounts.ui.StartupFailureScreen
private const val ApplicationName = "Radroots"
private const val InitialWindowWidth = 1284
@@ -21,9 +22,7 @@ private val isMacOs: Boolean =
.startsWith("Mac", ignoreCase = true)
fun main() {
- if (isMacOs) {
- configureMacOsApplication()
- }
+ val nativeStartupProblem = if (isMacOs) configureMacOsApplication() else null
application {
Window(
@@ -47,26 +46,33 @@ fun main() {
onDispose { }
}
- RadrootsApplication()
+ if (nativeStartupProblem == null) {
+ RadrootsApplication()
+ } else {
+ StartupFailureScreen(nativeStartupProblem)
+ }
}
}
}
-private fun configureMacOsApplication() {
+private fun configureMacOsApplication(): String? {
System.setProperty("apple.awt.application.name", ApplicationName)
System.setProperty("apple.awt.application.appearance", "system")
- if (!Taskbar.isTaskbarSupported()) return
+ if (!Taskbar.isTaskbarSupported()) return null
val taskbar = Taskbar.getTaskbar()
- if (!taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) return
+ if (!taskbar.isSupported(Taskbar.Feature.ICON_IMAGE)) return null
- val resource = checkNotNull(
- Thread.currentThread().contextClassLoader.getResource("icons/radroots.png"),
- ) {
- "Missing runtime app icon"
- }
- taskbar.iconImage = checkNotNull(ImageIO.read(resource)) {
- "Invalid runtime app icon"
- }
+ val icon = loadRuntimeIcon {
+ Thread.currentThread().contextClassLoader.getResourceAsStream("icons/radroots.png")
+ } ?: return "The application icon resource is unavailable."
+ return runCatching { taskbar.iconImage = icon }
+ .fold(
+ onSuccess = { null },
+ onFailure = { "The application icon could not be configured." },
+ )
}
+
+internal fun loadRuntimeIcon(openResource: () -> java.io.InputStream?): java.awt.Image? =
+ runCatching { openResource()?.use(ImageIO::read) }.getOrNull()
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/desktop/MainTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/desktop/MainTest.kt
@@ -0,0 +1,13 @@
+package org.radroots.studio.desktop
+
+import java.io.ByteArrayInputStream
+import kotlin.test.Test
+import kotlin.test.assertNull
+
+class MainTest {
+ @Test
+ fun missingOrInvalidRuntimeIconFailsSafely() {
+ assertNull(loadRuntimeIcon { null })
+ assertNull(loadRuntimeIcon { ByteArrayInputStream("not an image".encodeToByteArray()) })
+ }
+}
diff --git a/core/crates/application/src/app_core.rs b/core/crates/application/src/app_core.rs
@@ -141,15 +141,17 @@ impl AppCore {
now: UnixTimestamp,
) -> Result<RemovalConfirmationToken, SafeError> {
let mut state = self.lock_state();
- if !state
+ let Some(account) = state
.state_machine
.snapshot()
.accounts()
.iter()
- .any(|account| account.public_key() == public_key)
- {
+ .find(|account| account.public_key() == public_key)
+ else {
return Err(account_not_found());
- }
+ };
+ let deletes_local_credential = account.signer().availability()
+ != radroots_studio_domain::BindingAvailability::CredentialMissing;
let id = state.next_removal_token;
state.next_removal_token = id.checked_add(1).ok_or_else(invalid_application_state)?;
let revision = state.state_machine.snapshot().revision();
@@ -160,7 +162,7 @@ impl AppCore {
)
.ok_or_else(invalid_application_state)?;
let impact = RemovalImpact {
- deletes_local_credential: true,
+ deletes_local_credential,
signs_out: state
.state_machine
.snapshot()
@@ -241,7 +243,12 @@ const fn account_not_found() -> SafeError {
#[cfg(test)]
mod tests {
- use crate::{AppCore, AppLifecycle, RelayConfiguration};
+ use radroots_studio_domain::{
+ AccountCreatedAt, AccountIdentity, AccountSummary, BindingAvailability, LocalSignerBinding,
+ PublicKey, UnixTimestamp,
+ };
+
+ use crate::{AppCore, AppLifecycle, RelayConfiguration, StateTransition};
#[test]
fn bootstrap_is_idempotent_and_advances_only_once() {
@@ -264,4 +271,30 @@ mod tests {
assert_eq!(first.snapshot().revision().value(), 1);
assert_eq!(second.snapshot().revision().value(), 0);
}
+
+ #[test]
+ fn removal_impact_matches_missing_local_binding() {
+ let core = AppCore::in_memory(RelayConfiguration::default());
+ let public_key = PublicKey::from_bytes([9; 32]);
+ let account = AccountSummary::new(
+ AccountIdentity::derive(public_key).expect("identity"),
+ LocalSignerBinding::new(public_key, BindingAvailability::CredentialMissing),
+ None,
+ AccountCreatedAt::new(UnixTimestamp::from_seconds(1).expect("time")),
+ None,
+ )
+ .expect("account");
+ core.apply_transition(StateTransition::BootstrapRegistry {
+ accounts: vec![account],
+ selected: Some(public_key),
+ })
+ .expect("registry");
+
+ let removal = core
+ .issue_removal_token(public_key, UnixTimestamp::from_seconds(2).expect("time"))
+ .expect("removal");
+
+ assert!(!removal.impact().deletes_local_credential());
+ assert!(!removal.impact().signs_out());
+ }
}