app

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

commit fd4af3103b24cf6d0b4b394a5791d158cf74cb4d
parent 199a904bf779b192c73121fcbfbafc80207c7e5c
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Apr 2026 23:01:14 +0000

test: cover startup hardening invariants

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/launchers/desktop/src/window.rs | 27+++++++++++++++++++++++++--
2 files changed, 180 insertions(+), 2 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -1187,6 +1187,9 @@ mod tests { SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; + use radroots_app_remote_signer::{ + RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerSessionRecord, + }; use radroots_app_sqlite::{AppSqliteStore, DatabaseTarget}; use radroots_app_state::{ AppStateRepositoryError, AppStateStore, AppStateStoreError, HomeRoute, @@ -1362,6 +1365,103 @@ mod tests { } #[test] + fn clean_startup_cleanup_allows_generate_key_phase_transition() { + let paths = temp_remote_signer_paths("generate_key_after_clean_cleanup"); + let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState { + state_store: AppStateStore::load(InMemoryAppStateRepository::default()) + .expect("in-memory state store should load"), + default_nostr_relay_url: "ws://127.0.0.1:8080".to_owned(), + shared_accounts_paths: None, + remote_signer_paths: Some(paths.clone()), + accounts_manager: None, + sqlite_store: Some( + AppSqliteStore::open(DatabaseTarget::InMemory) + .expect("in-memory sqlite store should open"), + ), + startup_issue: None, + }); + + assert!( + runtime + .clear_startup_pending_remote_signer_session() + .expect("clear pending should succeed") + ); + assert!(runtime.begin_generate_key_startup()); + assert_eq!( + runtime.summary().logged_out_startup.phase, + radroots_app_models::LoggedOutStartupPhase::GenerateKeyStarting + ); + + cleanup_remote_signer_paths(&paths); + } + + #[test] + fn pending_startup_signer_session_recovers_after_runtime_restart() { + let (runtime, paths) = bootstrapped_runtime("restart_pending_recovery"); + let pending_session = fixture_pending_session(); + + assert!( + runtime + .store_startup_pending_remote_signer_session(&pending_session) + .expect("store pending should succeed") + ); + + let restarted = restart_runtime(paths.clone()); + let restored = restarted + .load_startup_pending_remote_signer_session() + .expect("load pending should succeed") + .expect("pending session should recover after restart"); + + assert_eq!( + restarted.summary().logged_out_startup.phase, + radroots_app_models::LoggedOutStartupPhase::SignerEntry + ); + assert_eq!( + restored.record.client_account_id(), + pending_session.record.client_account_id() + ); + assert_eq!( + restored.record.signer_identity.id, + pending_session.record.signer_identity.id + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] + fn clearing_pending_startup_signer_session_prevents_restart_recovery() { + let (runtime, paths) = bootstrapped_runtime("restart_after_explicit_cancel"); + let pending_session = fixture_pending_session(); + + assert!( + runtime + .store_startup_pending_remote_signer_session(&pending_session) + .expect("store pending should succeed") + ); + assert!( + runtime + .clear_startup_pending_remote_signer_session() + .expect("clear pending should succeed") + ); + + let restarted = restart_runtime(paths.clone()); + + assert_eq!( + restarted.summary().logged_out_startup.phase, + radroots_app_models::LoggedOutStartupPhase::ContinuePrompt + ); + assert!( + restarted + .load_startup_pending_remote_signer_session() + .expect("load pending should succeed") + .is_none(), + "explicit cancel should leave no pending startup session to recover" + ); + + cleanup_bootstrapped_runtime_paths(&paths); + } + + #[test] fn replacing_today_agenda_is_shared_without_clobbering_home_shell() { let runtime = DesktopAppRuntime::from_state(DesktopAppRuntimeState { state_store: AppStateStore::load(InMemoryAppStateRepository::default()) @@ -2594,6 +2694,19 @@ mod tests { ) } + fn bootstrapped_runtime(label: &str) -> (DesktopAppRuntime, AppDesktopRuntimePaths) { + let paths = temp_desktop_runtime_paths(label); + let runtime = restart_runtime(paths.clone()); + (runtime, paths) + } + + fn restart_runtime(paths: AppDesktopRuntimePaths) -> DesktopAppRuntime { + DesktopAppRuntime::from_state( + DesktopAppRuntimeState::bootstrap_from_paths(paths, "ws://127.0.0.1:8080".to_owned()) + .expect("runtime bootstrap should succeed"), + ) + } + fn temp_shared_accounts_paths(label: &str) -> AppSharedAccountsPaths { let suffix = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -2608,6 +2721,22 @@ mod tests { } } + fn temp_desktop_runtime_paths(label: &str) -> AppDesktopRuntimePaths { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let home_dir = std::env::temp_dir().join(format!("radroots_runtime_home_{label}_{suffix}")); + AppDesktopRuntimePaths::for_desktop( + AppRuntimePlatform::Macos, + AppRuntimeHostEnvironment { + home_dir: Some(home_dir), + ..AppRuntimeHostEnvironment::default() + }, + ) + .expect("desktop runtime paths should resolve") + } + fn temp_remote_signer_paths(label: &str) -> DesktopRemoteSignerPaths { let suffix = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -2627,6 +2756,32 @@ mod tests { } } + fn cleanup_bootstrapped_runtime_paths(paths: &AppDesktopRuntimePaths) { + if let Some(home_dir) = paths.app.data.ancestors().nth(4) { + let _ = fs::remove_dir_all(home_dir); + } + } + + fn fixture_pending_session() -> RadrootsAppRemoteSignerPendingSession { + let signer_identity = RadrootsIdentity::from_secret_key_str( + "1111111111111111111111111111111111111111111111111111111111111111", + ) + .expect("signer identity"); + let client_identity = RadrootsIdentity::from_secret_key_str( + "3333333333333333333333333333333333333333333333333333333333333333", + ) + .expect("client identity"); + + RadrootsAppRemoteSignerPendingSession { + record: RadrootsAppRemoteSignerSessionRecord::pending( + client_identity.to_public(), + signer_identity.to_public(), + vec!["ws://127.0.0.1:8080".to_owned()], + ), + client_secret_key_hex: client_identity.secret_key_hex(), + } + } + fn save_surface_activation( runtime: &DesktopAppRuntime, account_id: &str, diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -5003,8 +5003,8 @@ mod tests { home_window_launch_size_px, home_window_minimum_size_px, parse_optional_product_editor_stock_input, parse_product_editor_price_input, product_display_title, startup_home_surface, startup_signer_preview_summary, - startup_signer_source_input_is_editable, startup_signer_status_spec, - startup_signer_transport_failure_requires_notice, + startup_signer_preview_summary_for_connect_state, startup_signer_source_input_is_editable, + startup_signer_status_spec, startup_signer_transport_failure_requires_notice, }; use crate::runtime::DesktopAppRuntimeSummary; use radroots_app_models::SettingsAccountProjection; @@ -5300,6 +5300,29 @@ mod tests { } #[test] + fn startup_signer_preview_summary_prefers_pending_session_details_once_connect_starts() { + let pending_session = fixture_pending_session(); + let preview = startup_signer_preview_summary_for_connect_state( + "bunker://466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27?relay=wss%3A%2F%2Frelay.changed.example", + &StartupSignerConnectState::PendingApproval { + pending_session: pending_session.clone(), + auth_challenge_url: None, + }, + ) + .expect("preview"); + + assert_eq!( + preview.signer_npub, + pending_session.record.signer_identity.public_key_npub + ); + assert_eq!(preview.relays_label, "wss://relay.radroots.example"); + assert_eq!( + preview.permissions_label, + "sign_event:kind:1, switch_relays" + ); + } + + #[test] fn startup_signer_transport_failure_notice_ignores_the_waiting_timeout_copy() { assert!(!startup_signer_transport_failure_requires_notice( "remote signer did not respond yet"