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
This commit is contained in:
2026-05-16 09:06:32 +02:00
parent 47733713ca
commit bb483f01f6
@@ -477,6 +477,7 @@ mod tests {
#[derive(Default)] #[derive(Default)]
struct FakeUnpacker { struct FakeUnpacker {
fail: bool, fail: bool,
create_commit_conflict: bool,
archives: Mutex<Vec<PathBuf>>, archives: Mutex<Vec<PathBuf>>,
} }
@@ -484,6 +485,15 @@ mod tests {
fn failing() -> Self { fn failing() -> Self {
Self { Self {
fail: true, 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()), archives: Mutex::new(Vec::new()),
} }
} }
@@ -500,6 +510,14 @@ mod tests {
eyre::bail!("forced unpack failure"); eyre::bail!("forced unpack failure");
} }
tokio::fs::write(dest.join("payload.txt"), b"installed").await?; 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(()) Ok(())
}) })
} }
@@ -608,6 +626,34 @@ mod tests {
assert_eq!(intent.state, InstallIntentState::None); 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] #[tokio::test]
async fn update_success_promotes_new_local_and_removes_backup() { async fn update_success_promotes_new_local_and_removes_backup() {
let temp = TempDir::new(); let temp = TempDir::new();