From bb483f01f60b05fd1daf16a0b3bd498f24602e33 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 16 May 2026 09:06:32 +0200 Subject: [PATCH] test(peer): cover update commit rollback Add a focused transaction test for the branch where update extraction succeeds but promoting `.local.installing` to `local` fails. The fake unpacker creates a non-empty `local/` conflict after extraction, so the commit rename fails without adding production hooks or brittle platform-specific permission tricks. The assertion verifies the old install is restored from `.local.backup`, the conflict and staging directories are removed, the backup is consumed, and the intent is cleared back to None. Test Plan: - git diff --check - just fmt - just clippy - just test Follow-up-Plan: FOLLOW_UP_2.md --- .../lanspread-peer/src/install/transaction.rs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/lanspread-peer/src/install/transaction.rs b/crates/lanspread-peer/src/install/transaction.rs index a6b8a27..38718e4 100644 --- a/crates/lanspread-peer/src/install/transaction.rs +++ b/crates/lanspread-peer/src/install/transaction.rs @@ -477,6 +477,7 @@ mod tests { #[derive(Default)] struct FakeUnpacker { fail: bool, + create_commit_conflict: bool, archives: Mutex>, } @@ -484,6 +485,15 @@ mod tests { fn failing() -> Self { Self { fail: true, + create_commit_conflict: false, + archives: Mutex::new(Vec::new()), + } + } + + fn commit_conflict() -> Self { + Self { + fail: false, + create_commit_conflict: true, archives: Mutex::new(Vec::new()), } } @@ -500,6 +510,14 @@ mod tests { eyre::bail!("forced unpack failure"); } tokio::fs::write(dest.join("payload.txt"), b"installed").await?; + if self.create_commit_conflict { + let game_root = dest + .parent() + .ok_or_else(|| eyre::eyre!("staging dir should have parent"))?; + let local_conflict = game_root.join(LOCAL_DIR); + tokio::fs::create_dir_all(&local_conflict).await?; + tokio::fs::write(local_conflict.join("conflict.txt"), b"conflict").await?; + } Ok(()) }) } @@ -608,6 +626,34 @@ mod tests { assert_eq!(intent.state, InstallIntentState::None); } + #[tokio::test] + async fn update_commit_rename_failure_restores_previous_local() { + let temp = TempDir::new(); + let root = temp.game_root(); + write_file(&root.join("game.eti"), b"archive"); + write_file(&root.join("version.ini"), b"20250101"); + write_file(&root.join("local").join("old.txt"), b"old"); + + let err = update(&root, "game", Arc::new(FakeUnpacker::commit_conflict())) + .await + .expect_err("update should fail at commit rename"); + + assert!( + err.to_string().contains("failed to promote update"), + "{err:?}" + ); + assert_eq!( + std::fs::read(root.join("local").join("old.txt")) + .expect("old install should be restored"), + b"old" + ); + assert!(!root.join("local").join("conflict.txt").exists()); + assert!(!root.join(".local.installing").exists()); + assert!(!root.join(".local.backup").exists()); + let intent = read_intent(&root, "game").await; + assert_eq!(intent.state, InstallIntentState::None); + } + #[tokio::test] async fn update_success_promotes_new_local_and_removes_backup() { let temp = TempDir::new();