test(peer): cover uninstall rollback restore

Add coverage for the uninstall branch where `local` has already been moved to
`.local.backup`, but deleting that backup fails. The Unix-gated test makes a
child directory non-writable before uninstall starts, so recursive deletion of
the renamed backup fails without adding production hooks.

The test verifies rollback restores the previous local install, removes the
backup path, and clears the intent. It is gated to Unix because deletion
permission behavior is platform-specific; Windows coverage would need a
different failure mechanism rather than pretending this setup is portable.

Test Plan:
- git diff --check
- just fmt
- just clippy
- just test

Follow-up-Plan: FOLLOW_UP_2.md
This commit is contained in:
2026-05-16 09:08:28 +02:00
parent bb483f01f6
commit 3abb2e051b
@@ -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,