Prerequisite for `banger update`'s swap step. The updater renames a
staged binary into place and needs (a) atomicity per file (no
half-written bytes for a process that's about to systemctl restart
into the new binary) and (b) a backup it can restore from when
post-restart doctor reports FAIL.
* AtomicReplace(newSrc, dst, suffixPrevious): if dst exists,
move it to dst+suffixPrevious. Then os.Rename newSrc → dst.
Atomic on a single fs (the only case relevant to the updater —
everything is staged under /var/cache/banger and then renamed
into /usr/local/bin, but those should be on the same fs in a
typical install). On rename failure, restore the backup so we
don't leave the caller without their binary.
* AtomicReplaceRollback(dst, suffixPrevious): symmetric inverse.
Removes dst, renames dst+suffixPrevious back to dst. Tolerant
of a missing backup (fresh-install case) so the updater can
call it unconditionally on failure paths without tracking
backup state itself.
* Refuses an empty suffix at compile-time-style guard: an empty
suffix would silently no-op the backup AND break rollback.
Six tests cover: happy path, fresh install (no prior dst), stale
.previous from a half-finished prior run, empty-suffix rejection,
rollback restores, rollback tolerant of no-backup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>