test(backup): cover failed zip member cleanup

(cherry picked from commit 857b969c57cd1c7f50b4d5c3d41800e2bc24e4d0)
This commit is contained in:
JoaoMarcos44
2026-09-26 21:14:49 -03:00
committed by kshitij
parent da1bc7d502
commit a71ff10071
2 changed files with 82 additions and 0 deletions

View File

@@ -468,6 +468,7 @@ def _write_zip_file(zf: zipfile.ZipFile, path: Path, arcname: str) -> None:
_discard_failed_zip_members(zf, filelist_len)
raise
def _zip_sqlite_snapshot(zf: zipfile.ZipFile, abs_path: Path, rel_path: Path, out_path: Path) -> Optional[int]:
"""Add a WAL-safe snapshot of *abs_path* to *zf*; return its byte size, or None on failure.

View File

@@ -1778,6 +1778,87 @@ class TestQuickSnapshotProjectsKanban:
assert rows == [("w1", "ship")]
def _fail_zip_write_after(monkeypatch, member: str, byte_count: int = 120_000) -> None:
"""Make ZipFile.write finalize a large partial member, then surface a source-read failure."""
real_write = zipfile.ZipFile.write
def flaky_write(self, filename, arcname=None, compress_type=None, compresslevel=None):
if str(arcname) == member:
with Path(filename).open("rb") as src, self.open(str(arcname), "w") as dst:
dst.write(src.read(byte_count))
raise OSError(5, "simulated source read failure")
return real_write(self, filename, arcname, compress_type, compresslevel)
monkeypatch.setattr(zipfile.ZipFile, "write", flaky_write)
class TestFailedZipMemberRecovery:
def test_automatic_backup_omits_crc_valid_partial_member(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text("model: test\n")
(hermes_home / "flaky.bin").write_bytes(os.urandom(300_000))
archive = tmp_path / "automatic.zip"
_fail_zip_write_after(monkeypatch, "flaky.bin")
from hermes_cli.backup import _write_full_zip_backup
assert _write_full_zip_backup(archive, hermes_home) is None
assert archive.exists()
# The failed payload is deliberately left orphaned before the central directory.
# Keeping >64 KiB here proves the archive remains readable without rewinding start_dir.
assert archive.stat().st_size > 65_535
with zipfile.ZipFile(archive) as zf:
assert "flaky.bin" not in zf.namelist()
assert zf.read("config.yaml") == b"model: test\n"
assert zf.testzip() is None
def test_failed_duplicate_restores_previous_central_directory_entry(self, tmp_path, monkeypatch):
source = tmp_path / "source.bin"
source.write_bytes(os.urandom(300_000))
archive = tmp_path / "duplicate.zip"
_fail_zip_write_after(monkeypatch, "dup.bin")
import hermes_cli.backup as backup_mod
with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("dup.bin", b"complete-before-failure")
with pytest.raises(OSError, match="simulated source read failure"):
backup_mod._write_zip_file(zf, source, "dup.bin")
zf.writestr("after.bin", b"after")
assert archive.stat().st_size > 65_535
with zipfile.ZipFile(archive) as zf:
assert zf.namelist().count("dup.bin") == 1
assert zf.read("dup.bin") == b"complete-before-failure"
assert zf.read("after.bin") == b"after"
assert zf.testzip() is None
def test_incomplete_pre_update_backup_does_not_rotate_last_complete(self, tmp_path, monkeypatch):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text("model: test\n")
from hermes_cli.backup import create_pre_update_backup
good = create_pre_update_backup(hermes_home=hermes_home, keep=1)
assert good is not None and good.exists()
_advance_backup_clock()
(hermes_home / "flaky.bin").write_bytes(os.urandom(300_000))
_fail_zip_write_after(monkeypatch, "flaky.bin")
incomplete = create_pre_update_backup(hermes_home=hermes_home, keep=1)
assert incomplete is None
assert good.exists(), "an incomplete generation rotated out the last complete backup"
backups = sorted((hermes_home / "backups").glob("pre-update-*.zip"))
assert len(backups) == 2
salvage = next(path for path in backups if path != good)
with zipfile.ZipFile(salvage) as zf:
assert "flaky.bin" not in zf.namelist()
assert zf.read("config.yaml") == b"model: test\n"
assert zf.testzip() is None
class TestPreUpdateBackup:
"""Tests for create_pre_update_backup — the auto-backup ``hermes update``
runs before touching anything."""