commit fd4af3103b24cf6d0b4b394a5791d158cf74cb4d
parent 199a904bf779b192c73121fcbfbafc80207c7e5c
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Apr 2026 23:01:14 +0000
test: cover startup hardening invariants
Diffstat:
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"