diff --git a/README.md b/README.md index 91baa27..54fa158 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ From a clone, for development: ```bash pip install -e ".[dev]" -pytest -q # 17 tests +pytest -q # 22 tests ``` No session of your own to look at yet? A synthetic one ships with the repo: @@ -132,4 +132,4 @@ as such rather than vanish from the report. ## Status -Working, 17 tests, validated against a real 34MB session with 152 subagent runs. +Working, 22 tests, validated against a real 34MB session with 152 subagent runs. diff --git a/agentrace/cli.py b/agentrace/cli.py index 7890bcb..da59a72 100644 --- a/agentrace/cli.py +++ b/agentrace/cli.py @@ -14,6 +14,7 @@ from pathlib import Path from rich.console import Console +from rich.markup import escape from rich.table import Table from .checks import analyse @@ -31,7 +32,11 @@ def _load(args) -> list[AgentRun]: sessions = parse_all(Path(args.dir) if args.dir else None) runs = [r for s in sessions for r in s.runs] if not runs: - console.print("[yellow]No subagent runs found.[/] Looked in ~/.claude/projects unless --dir was given.") + source = args.file or args.dir or "~/.claude/projects" + console.print( + f"[yellow]No subagent runs found.[/] Looked in {escape(source)}.", + soft_wrap=True, + ) return runs @@ -155,10 +160,17 @@ def cmd_stats(args) -> int: return 0 +def _existing_file(value: str) -> str: + if not Path(value).is_file(): + raise argparse.ArgumentTypeError(f"not a file: {value}") + return value + + def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(prog="agentrace", description=__doc__.split("\n")[0]) - p.add_argument("--dir", help="transcript root (default ~/.claude/projects)") - p.add_argument("--file", help="a single .jsonl transcript") + source = p.add_mutually_exclusive_group() + source.add_argument("--dir", help="transcript root (default ~/.claude/projects)") + source.add_argument("--file", type=_existing_file, help="a single .jsonl transcript") sub = p.add_subparsers(dest="cmd", required=True) sub.add_parser("list", help="list subagent runs").set_defaults(func=cmd_list) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..ecaf0fc --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import subprocess +import sys + + +def run_cli(*args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-m", "agentrace.cli", *args], + capture_output=True, + check=False, + text=True, + ) + + +def test_file_and_dir_are_mutually_exclusive(tmp_path): + transcript = tmp_path / "session.jsonl" + transcript.write_text("") + + result = run_cli("--dir", str(tmp_path), "--file", str(transcript), "list") + + assert result.returncode == 2 + assert "argument --file: not allowed with argument --dir" in result.stderr + assert result.stdout == "" + + +def test_missing_file_is_a_clean_parser_error(tmp_path): + missing = tmp_path / "missing.jsonl" + + result = run_cli("--file", str(missing), "list") + + assert result.returncode == 2 + assert f"not a file: {missing}" in result.stderr + assert "Traceback" not in result.stderr + assert result.stdout == "" + + +def test_empty_result_names_selected_file(tmp_path): + transcript = tmp_path / "empty.jsonl" + transcript.write_text("") + + result = run_cli("--file", str(transcript), "list") + + assert result.returncode == 0 + assert "No subagent runs found." in result.stdout + assert f"Looked in {transcript}." in result.stdout + + +def test_empty_result_names_selected_directory(tmp_path): + result = run_cli("--dir", str(tmp_path), "list") + + assert result.returncode == 0 + assert "No subagent runs found." in result.stdout + assert f"Looked in {tmp_path}." in result.stdout