diff --git a/postflight/__main__.py b/postflight/__main__.py index aff80ae..1241077 100644 --- a/postflight/__main__.py +++ b/postflight/__main__.py @@ -42,7 +42,18 @@ def _load(args: argparse.Namespace) -> list[Turn]: if args.otel: from .adapters.otel import turns_from_jsonl - return turns_from_jsonl(args.otel) + # A typo'd path and a file that is not JSONL are both ordinary mistakes, and a + # traceback answers neither of them. SystemExit prints the message and sets a + # non-zero status without pretending the tool crashed. + try: + return turns_from_jsonl(args.otel) + except OSError as exc: + raise SystemExit(f"cannot read {args.otel}: {exc.strerror}") from exc + except json.JSONDecodeError as exc: + raise SystemExit( + f"{args.otel} is not newline-delimited JSON: {exc} " + "(expected one exported span object per line)" + ) from exc from .adapters.langfuse import LangfuseAdapter, LangfuseClient diff --git a/tests/test_cli.py b/tests/test_cli.py index 68f4e8a..99f5190 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -59,3 +59,17 @@ def test_langfuse_without_credentials_fails_loudly(monkeypatch): def test_a_source_is_required(): with pytest.raises(SystemExit): main([]) + + +def test_a_missing_file_is_a_message_not_a_traceback(): + """A typo'd path is an ordinary mistake. Answering it with a stack trace tells the + user the tool crashed, which is both unhelpful and untrue.""" + with pytest.raises(SystemExit, match="cannot read"): + main(["--otel", "/nonexistent/spans.jsonl"]) + + +def test_a_file_that_is_not_jsonl_says_so(tmp_path): + bad = tmp_path / "notes.txt" + bad.write_text("these are my notes, not spans\n") + with pytest.raises(SystemExit, match="not newline-delimited JSON"): + main(["--otel", str(bad)])