From 80c0783194e01f205b6c224f388efe6115147131 Mon Sep 17 00:00:00 2001 From: eunwoo song Date: Sun, 6 Sep 2026 21:45:11 +0900 Subject: [PATCH] fix: handle invalid receipt files in CLI --- CHANGELOG.md | 5 +++++ src/answerproof/cli.py | 18 +++++++++++++++--- tests/test_cli.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d366cb..4dc0ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- `verify` and `inspect` now report unreadable or invalid receipt files as + one-line CLI errors instead of raising tracebacks. + ## [0.1.0] - 2026-07-27 ### Added diff --git a/src/answerproof/cli.py b/src/answerproof/cli.py index 6059e65..5505d08 100644 --- a/src/answerproof/cli.py +++ b/src/answerproof/cli.py @@ -23,9 +23,17 @@ from .verifier import verify_receipt +class ReceiptLoadError(Exception): + """Raised when a receipt file cannot be read or validated.""" + + def _load_receipt(path: str) -> Receipt: - text = Path(path).read_text(encoding="utf-8") - return Receipt.from_json(text) + try: + text = Path(path).read_text(encoding="utf-8") + return Receipt.from_json(text) + except (OSError, UnicodeError, ValueError) as exc: + detail = str(exc).splitlines()[0] + raise ReceiptLoadError(f"cannot load receipt {path!r}: {detail}") from exc def cmd_keygen(args: argparse.Namespace) -> int: @@ -130,7 +138,11 @@ def build_parser() -> argparse.ArgumentParser: def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) - return args.func(args) + try: + return args.func(args) + except ReceiptLoadError as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 if __name__ == "__main__": diff --git a/tests/test_cli.py b/tests/test_cli.py index bf0305f..1378cf9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -62,6 +62,35 @@ def test_inspect(tmp_path, capsys, receipt): assert "merkle_root" in out +@pytest.mark.parametrize("command", ["verify", "inspect"]) +def test_receipt_command_reports_missing_file(command, tmp_path, capsys): + missing = tmp_path / "missing.json" + + rc = main([command, str(missing)]) + + captured = capsys.readouterr() + assert rc == 2 + assert captured.out == "" + assert captured.err.startswith(f"error: cannot load receipt '{missing}':") + assert "Traceback" not in captured.err + assert len(captured.err.splitlines()) == 1 + + +@pytest.mark.parametrize("command", ["verify", "inspect"]) +def test_receipt_command_reports_invalid_json(command, tmp_path, capsys): + invalid = tmp_path / "invalid.json" + invalid.write_text("not json", encoding="utf-8") + + rc = main([command, str(invalid)]) + + captured = capsys.readouterr() + assert rc == 2 + assert captured.out == "" + assert captured.err.startswith(f"error: cannot load receipt '{invalid}':") + assert "Traceback" not in captured.err + assert len(captured.err.splitlines()) == 1 + + def test_missing_command_errors(): with pytest.raises(SystemExit): main([])