From 12c8cefbce3cb2e5b12b72b501cda7b481b61dfc Mon Sep 17 00:00:00 2001 From: salt-555 Date: Mon, 13 Apr 2026 19:24:48 -0600 Subject: [PATCH] fix(backup): handle files with pre-1980 timestamps ZipFile.write() raises ValueError for files with mtime before 1980-01-01 (the ZIP format uses MS-DOS timestamps which can't represent earlier dates). This crashes the entire backup. Add ValueError to the existing except clause so these files are skipped and reported in the warnings summary, matching the existing behavior for PermissionError and OSError. --- hermes_cli/backup.py | 2 +- tests/hermes_cli/test_backup.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/hermes_cli/backup.py b/hermes_cli/backup.py index 667b8915..8b5b90ef 100644 --- a/hermes_cli/backup.py +++ b/hermes_cli/backup.py @@ -201,7 +201,7 @@ def run_backup(args) -> None: else: zf.write(abs_path, arcname=str(rel_path)) total_bytes += abs_path.stat().st_size - except (PermissionError, OSError) as exc: + except (PermissionError, OSError, ValueError) as exc: errors.append(f" {rel_path}: {exc}") continue diff --git a/tests/hermes_cli/test_backup.py b/tests/hermes_cli/test_backup.py index b4589dc9..35089ecd 100644 --- a/tests/hermes_cli/test_backup.py +++ b/tests/hermes_cli/test_backup.py @@ -702,6 +702,34 @@ class TestBackupEdgeCases: # Zip should still be created with the readable files assert out_zip.exists() + def test_pre1980_timestamp_skipped(self, tmp_path, monkeypatch): + """Backup skips files with pre-1980 timestamps (ZIP limitation).""" + hermes_home = tmp_path / ".hermes" + hermes_home.mkdir() + (hermes_home / "config.yaml").write_text("model: test\n") + + # Create a file with epoch timestamp (1970-01-01) + old_file = hermes_home / "ancient.txt" + old_file.write_text("old data") + os.utime(old_file, (0, 0)) + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + monkeypatch.setattr(Path, "home", lambda: tmp_path) + + out_zip = tmp_path / "out.zip" + args = Namespace(output=str(out_zip)) + + from hermes_cli.backup import run_backup + run_backup(args) + + # Zip should still be created with the valid files + assert out_zip.exists() + with zipfile.ZipFile(out_zip, "r") as zf: + names = zf.namelist() + assert "config.yaml" in names + # The pre-1980 file should be skipped, not crash the backup + assert "ancient.txt" not in names + def test_skips_output_zip_inside_hermes(self, tmp_path, monkeypatch): """Backup skips its own output zip if it's inside hermes root.""" hermes_home = tmp_path / ".hermes"