diff --git a/crates/lanspread-peer/src/install/transaction.rs b/crates/lanspread-peer/src/install/transaction.rs index 38718e4..6685160 100644 --- a/crates/lanspread-peer/src/install/transaction.rs +++ b/crates/lanspread-peer/src/install/transaction.rs @@ -691,6 +691,52 @@ mod tests { assert!(root.join("version.ini").is_file()); } + #[cfg(unix)] + #[tokio::test] + async fn uninstall_delete_failure_restores_backup() { + use std::os::unix::fs::PermissionsExt; + + let temp = TempDir::new(); + let root = temp.game_root(); + let locked_dir = root.join("local").join("locked"); + write_file(&root.join("version.ini"), b"20250101"); + write_file(&root.join("local").join("old.txt"), b"old"); + write_file(&locked_dir.join("payload.txt"), b"locked"); + std::fs::set_permissions(&locked_dir, std::fs::Permissions::from_mode(0o500)) + .expect("locked dir permissions should be set"); + + let _err = uninstall(&root, "game") + .await + .expect_err("uninstall should fail while deleting backup"); + + for restored_locked_dir in [ + root.join("local").join("locked"), + root.join(".local.backup").join("locked"), + ] { + if restored_locked_dir.exists() { + std::fs::set_permissions( + &restored_locked_dir, + std::fs::Permissions::from_mode(0o700), + ) + .expect("locked dir permissions should be restored for cleanup"); + } + } + + assert_eq!( + std::fs::read(root.join("local").join("old.txt")) + .expect("old install should be restored"), + b"old" + ); + assert_eq!( + std::fs::read(root.join("local").join("locked").join("payload.txt")) + .expect("locked payload should be restored"), + b"locked" + ); + assert!(!root.join(".local.backup").exists()); + let intent = read_intent(&root, "game").await; + assert_eq!(intent.state, InstallIntentState::None); + } + #[derive(Clone)] struct RecoveryCase { name: &'static str,