What happens
Four code paths read a file with a call whose exception type the surrounding handler does not catch. The result is a Python traceback on stderr and exit 1, where the documented contract is exit 2 with {"ok": false, ...} on stdout — "an unreadable trace" is the literal example given for EXIT_UNAVAILABLE in cli/output.py:26-27.
Repro (all verified)
$ mkdir adir
$ grapharc trace adir --json → exit=1 stdout=0B stderr=954B IsADirectoryError traceback
$ grapharc metrics adir x --json → exit=1 stdout=0B stderr=1090B IsADirectoryError traceback
$ grapharc viz adir x --json → exit=1 stdout=0B stderr=1385B IsADirectoryError traceback
$ grapharc replay adir x --json → exit=1 stdout=138B stderr=0B clean document <- correct
$ chmod 000 t.jsonl; grapharc trace t.jsonl --json → PermissionError traceback
$ printf '\xff\xfe\x00bin' > bin.json
$ grapharc run bin.json --json → exit=1 stdout=0B UnicodeDecodeError traceback
$ printf '\xff\xfe' > grapharc.toml
$ grapharc demo stage0 --json → exit=1 stdout=0B UnicodeDecodeError traceback
Root causes
_existing_trace (cli/main.py:239-248) only tests path.exists(), and the handlers at main.py:495 / :531 / :555 catch only TraceReadError, so OSError (directory, permission, I/O) escapes. replay/diff handle this correctly — the family is inconsistent with itself, which is the clearest sign it is an oversight.
cli/graphrun.py:73 — path.read_text(encoding="utf-8") sits outside the except (JSONDecodeError, TOMLDecodeError) on line 78, and UnicodeDecodeError is neither.
cli/config.py:164-166 — same shape: read_text inside the tomllib.loads(...) argument, but UnicodeDecodeError is not in except (OSError, TOMLDecodeError).
Why it matters
The exit-code contract is described in the README as part of the interface, and #37 fixed exactly this class for malformed trace lines one release ago — these are the remaining doors. The config case is the nastiest: grapharc.toml is picked up implicitly from the working directory, so a stray binary file there breaks demo, run and plan with a traceback rather than a report naming the file.
What to consider
- Widen
_existing_trace to attempt the read (or path.is_file() plus catching OSError) and route through fail(...) with EXIT_UNAVAILABLE, exactly as replay already does.
- Add
UnicodeDecodeError to the two except tuples, with a message naming the file and the fact that it is not UTF-8.
- One parametrized test per path (directory, unreadable, non-UTF-8) asserting exit 2, one document on stdout, empty stderr — the same shape
test_cli.py already uses for the malformed-line cases.
Related
Not the same bug, but adjacent and worth deciding together: argparse usage errors in --json mode (grapharc trace --json, demo stage99 --json) print text to stderr rather than a document. Exit code 2 is correct there; only the document half is missing. Fixing it needs a custom ArgumentParser.error, so it may deserve its own issue.
Acceptance criteria
Each of the seven commands above exits 2 with one {"ok": false, ...} document on stdout and empty stderr; text mode prints error: ... on stderr with empty stdout; no traceback reaches a user in any case.
What happens
Four code paths read a file with a call whose exception type the surrounding handler does not catch. The result is a Python traceback on stderr and exit 1, where the documented contract is exit 2 with
{"ok": false, ...}on stdout — "an unreadable trace" is the literal example given forEXIT_UNAVAILABLEincli/output.py:26-27.Repro (all verified)
Root causes
_existing_trace(cli/main.py:239-248) only testspath.exists(), and the handlers atmain.py:495/:531/:555catch onlyTraceReadError, soOSError(directory, permission, I/O) escapes.replay/diffhandle this correctly — the family is inconsistent with itself, which is the clearest sign it is an oversight.cli/graphrun.py:73—path.read_text(encoding="utf-8")sits outside theexcept (JSONDecodeError, TOMLDecodeError)on line 78, andUnicodeDecodeErroris neither.cli/config.py:164-166— same shape:read_textinside thetomllib.loads(...)argument, butUnicodeDecodeErroris not inexcept (OSError, TOMLDecodeError).Why it matters
The exit-code contract is described in the README as part of the interface, and #37 fixed exactly this class for malformed trace lines one release ago — these are the remaining doors. The config case is the nastiest:
grapharc.tomlis picked up implicitly from the working directory, so a stray binary file there breaksdemo,runandplanwith a traceback rather than a report naming the file.What to consider
_existing_traceto attempt the read (orpath.is_file()plus catchingOSError) and route throughfail(...)withEXIT_UNAVAILABLE, exactly asreplayalready does.UnicodeDecodeErrorto the twoexcepttuples, with a message naming the file and the fact that it is not UTF-8.test_cli.pyalready uses for the malformed-line cases.Related
Not the same bug, but adjacent and worth deciding together: argparse usage errors in
--jsonmode (grapharc trace --json,demo stage99 --json) print text to stderr rather than a document. Exit code 2 is correct there; only the document half is missing. Fixing it needs a customArgumentParser.error, so it may deserve its own issue.Acceptance criteria
Each of the seven commands above exits 2 with one
{"ok": false, ...}document on stdout and empty stderr; text mode printserror: ...on stderr with empty stdout; no traceback reaches a user in any case.