Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions src/answerproof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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__":
Expand Down
29 changes: 29 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
Loading