From ed2b9e43c8164dc8684b93487e90c32cef3e75ce Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:45:40 +0530 Subject: [PATCH] fix(backup): stage SQLite snapshots beside output zip in pre-update path too The pre-update / pre-migration backup path (_write_full_zip_backup) had the same /tmp staging bug as run_backup: a small tmpfs at the default tempfile location silently drops large *.db files from the archive. Route its SQLite staging temp files to the output zip's directory as well, and add regression tests (mutation-verified) for both staging paths. Co-authored-by: liuhao1024 --- hermes_cli/backup.py | 8 ++++- tests/hermes_cli/test_backup.py | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/hermes_cli/backup.py b/hermes_cli/backup.py index d9bb12d62e1..62997528bd8 100644 --- a/hermes_cli/backup.py +++ b/hermes_cli/backup.py @@ -870,7 +870,13 @@ def _write_full_zip_backup(out_path: Path, hermes_root: Path) -> Optional[Path]: for abs_path, rel_path in files_to_add: try: if abs_path.suffix == ".db": - with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp: + # Stage the snapshot alongside the output zip so that the + # temp file lives on the same filesystem. The system + # default (/tmp) may be a small tmpfs that cannot hold + # large databases, causing silent backup incompleteness. + with tempfile.NamedTemporaryFile( + suffix=".db", delete=False, dir=str(out_path.parent) + ) as tmp: tmp_db = Path(tmp.name) try: if _safe_copy_db(abs_path, tmp_db): diff --git a/tests/hermes_cli/test_backup.py b/tests/hermes_cli/test_backup.py index 4052267b45e..15a2112ac26 100644 --- a/tests/hermes_cli/test_backup.py +++ b/tests/hermes_cli/test_backup.py @@ -192,6 +192,66 @@ class TestBackup: # Skins assert "skins/cyber.yaml" in names + def test_db_snapshots_staged_beside_output_zip(self, tmp_path, monkeypatch): + """SQLite staging temp files must be created on the output zip's + filesystem (dir=out_path.parent), NOT the system /tmp default — a + small tmpfs there silently drops large DBs from the backup (#35376).""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + _make_hermes_tree(hermes_home) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setattr(Path, "home", lambda: tmp_path) + + out_dir = tmp_path / "external-drive" + out_dir.mkdir() + out_zip = out_dir / "backup.zip" + args = Namespace(output=str(out_zip)) + + import hermes_cli.backup as backup_mod + staged_dirs = [] + real_ntf = backup_mod.tempfile.NamedTemporaryFile + + def _spy(*a, **kw): + staged_dirs.append(kw.get("dir")) + return real_ntf(*a, **kw) + + monkeypatch.setattr(backup_mod.tempfile, "NamedTemporaryFile", _spy) + backup_mod.run_backup(args) + + # At least one .db was staged, and every staging call targeted the + # output zip's directory rather than the system temp default. + assert staged_dirs, "no SQLite snapshot was staged" + assert all(d == str(out_dir) for d in staged_dirs), staged_dirs + + def test_pre_update_db_snapshots_staged_beside_output_zip(self, tmp_path, monkeypatch): + """The pre-update/pre-migration zip path (_write_full_zip_backup) must + also stage SQLite snapshots beside its output zip, not in /tmp.""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + _make_hermes_tree(hermes_home) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setattr(Path, "home", lambda: tmp_path) + + out_zip = hermes_home / "backups" / "pre-update-test.zip" + out_zip.parent.mkdir(parents=True, exist_ok=True) + + import hermes_cli.backup as backup_mod + staged_dirs = [] + real_ntf = backup_mod.tempfile.NamedTemporaryFile + + def _spy(*a, **kw): + staged_dirs.append(kw.get("dir")) + return real_ntf(*a, **kw) + + monkeypatch.setattr(backup_mod.tempfile, "NamedTemporaryFile", _spy) + result = backup_mod._write_full_zip_backup(out_zip, hermes_home) + + assert result is not None + assert staged_dirs, "no SQLite snapshot was staged" + assert all(d == str(out_zip.parent) for d in staged_dirs), staged_dirs + def test_excludes_hermes_agent(self, tmp_path, monkeypatch): """Backup does NOT include hermes-agent/ directory.""" hermes_home = tmp_path / ".hermes"