fix(backup): friendly error on unwritable output path instead of raw traceback
This commit is contained in:
@@ -645,22 +645,30 @@ def _run_backup_locked(args, hermes_root: Path) -> None:
|
||||
"""Write a full backup while the cross-process backup slot is held."""
|
||||
|
||||
# Determine output path
|
||||
if args.output:
|
||||
out_path = Path(args.output).expanduser().resolve()
|
||||
# If user gave a directory, put the zip inside it
|
||||
if out_path.is_dir():
|
||||
out_path = None
|
||||
try:
|
||||
if args.output:
|
||||
out_path = Path(args.output).expanduser().resolve()
|
||||
# If user gave a directory, put the zip inside it
|
||||
if out_path.is_dir():
|
||||
stamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
|
||||
out_path = out_path / f"hermes-backup-{stamp}.zip"
|
||||
else:
|
||||
stamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
|
||||
out_path = out_path / f"hermes-backup-{stamp}.zip"
|
||||
else:
|
||||
stamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
|
||||
out_path = Path.home() / f"hermes-backup-{stamp}.zip"
|
||||
out_path = Path.home() / f"hermes-backup-{stamp}.zip"
|
||||
|
||||
# Ensure the suffix is .zip
|
||||
if out_path.suffix.lower() != ".zip":
|
||||
out_path = out_path.with_suffix(out_path.suffix + ".zip")
|
||||
# Ensure the suffix is .zip
|
||||
if out_path.suffix.lower() != ".zip":
|
||||
out_path = out_path.with_suffix(out_path.suffix + ".zip")
|
||||
|
||||
# Ensure parent directory exists
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Ensure parent directory exists
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as exc:
|
||||
# A bad/unwritable output path (permission denied, unreadable parent,
|
||||
# etc.) should give a clean one-line error, not a raw traceback
|
||||
# (round-3 QA SUB-01). is_dir() and mkdir() both hit the filesystem.
|
||||
print(f"Error: cannot write backup to {args.output or out_path}: {exc}")
|
||||
raise SystemExit(1) from exc
|
||||
|
||||
# Collect files
|
||||
scan_started = time.monotonic()
|
||||
|
||||
38
tests/hermes_cli/test_backup_path_errors.py
Normal file
38
tests/hermes_cli/test_backup_path_errors.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Regression test: `hermes backup -o <bad path>` errors cleanly (round-3 SUB-01).
|
||||
|
||||
Before, an unwritable/nonexistent-parent output path raised a raw
|
||||
PermissionError traceback from the unguarded is_dir()/mkdir() calls. It must
|
||||
print a one-line error and exit 1 instead.
|
||||
"""
|
||||
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_home(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
(home / "config.yaml").write_text("model: {}\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
return home
|
||||
|
||||
|
||||
def test_backup_unwritable_parent_errors_cleanly(tmp_path, monkeypatch, capsys):
|
||||
_make_home(tmp_path, monkeypatch)
|
||||
import hermes_cli.backup as backup_mod
|
||||
|
||||
# A parent directory that cannot be created (a file stands where the dir
|
||||
# would go) reliably triggers an OSError on mkdir without needing root.
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("i am a file, not a dir")
|
||||
bad_out = blocker / "sub" / "backup.zip"
|
||||
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
backup_mod.run_backup(Namespace(output=str(bad_out)))
|
||||
|
||||
assert exc.value.code == 1
|
||||
out = capsys.readouterr().out
|
||||
assert "cannot write backup" in out.lower()
|
||||
assert "Traceback" not in out
|
||||
Reference in New Issue
Block a user