diff --git a/src/plyngent/cli/display.py b/src/plyngent/cli/display.py index 5c308f5..d838b73 100644 --- a/src/plyngent/cli/display.py +++ b/src/plyngent/cli/display.py @@ -43,6 +43,7 @@ _PRETTY_TOOLS = frozenset( "grep_files", "run_argv", "run_argv_batch", + "fetch", } ) @@ -331,6 +332,58 @@ def _run_argv_batch_pretty(_args_json: str, result: str) -> str: return _pretty_line("* Batch ", (f"({ran}/{steps} steps ran, {tail})", fg)) +def _human_bytes(value: str) -> str: + """Compact byte-count rendering: ``900B`` / ``1.2KB`` / ``3.4MB``.""" + kib = 1024 + try: + n = int(value) + except ValueError: + return "" + if n < kib: + return f"{n}B" + kb = n / kib + if kb < kib: + return f"{kb:.1f}KB" + mb = kb / kib + if mb < kib: + return f"{mb:.1f}MB" + return f"{mb / kib:.1f}GB" + + +def _http_status_fg(status: str) -> str | None: + """Foreground color by HTTP status class (2xx green, 3xx yellow, 4xx/5xx red).""" + if status and status[0] in {"2", "3", "4", "5"}: + return {"2": "green", "3": "yellow", "4": "red", "5": "red"}[status[0]] + return None + + +def _fetch_pretty(args_json: str, result: str) -> str: + """One-line summary for a ``fetch`` call: ``* GET 200 https://… (json, 1.2KB)``.""" + method = _json_str_arg(args_json, "method") or "GET" + if result.startswith("error:"): + return _pretty_line(f"* {method} ", (f"({result})", "red")) + fields: dict[str, str] = {} + for line in result.splitlines(): + if line.startswith("--- "): + break + if "=" in line: + key, _, value = line.partition("=") + fields[key] = value + status = fields.get("status", "?") + url = fields.get("final_url", "?") + detail_parts: list[str] = [] + kind = fields.get("body_kind", "") + if kind: + detail_parts.append(kind) + size = _human_bytes(fields.get("bytes", "")) + if size: + detail_parts.append(size) + if fields.get("truncated") == "true": + detail_parts.append("truncated") + detail = f" ({', '.join(detail_parts)})" if detail_parts else "" + return _pretty_line(f"* {method} {status} {url}", (detail, _http_status_fg(status))) + + _PRETTY_BUILDERS: dict[str, Callable[[str, str], str]] = { "read_file": _read_file_pretty, "tree": _tree_pretty, @@ -341,6 +394,7 @@ _PRETTY_BUILDERS: dict[str, Callable[[str, str], str]] = { "grep_files": _grep_pretty, "run_argv": _run_argv_pretty, "run_argv_batch": _run_argv_batch_pretty, + "fetch": _fetch_pretty, } diff --git a/tests/test_cli/test_display.py b/tests/test_cli/test_display.py index 609fa91..effeccd 100644 --- a/tests/test_cli/test_display.py +++ b/tests/test_cli/test_display.py @@ -410,3 +410,42 @@ async def test_pretty_run_argv_batch_complete(capsys: pytest.CaptureFixture[str] await render_events(_aiter([_pretty_call("run_argv_batch", '{"steps": []}'), _result(result)])) out = capsys.readouterr().out assert "* Batch (2/2 steps ran, last exit 0)" in out + + +async def test_pretty_fetch(capsys: pytest.CaptureFixture[str]) -> None: + result = ( + "status=200\nmethod=GET\nfinal_url=https://example.com\ncontent_type=application/json\n" + "body_kind=json\nbytes=1229\ntruncated=false\nredirects=0\nsecurity=public\n--- body ---\n{...}\n" + ) + await render_events(_aiter([_pretty_call("fetch", '{"url": "https://example.com"}'), _result(result)])) + out = capsys.readouterr().out + assert "* GET 200 https://example.com (json, 1.2KB)" in out + assert "[tool]" not in out + + +async def test_pretty_fetch_error_status(capsys: pytest.CaptureFixture[str]) -> None: + result = ( + "status=404\nmethod=GET\nfinal_url=https://example.com/nope\ncontent_type=text/html\n" + "body_kind=html\nbytes=45\ntruncated=false\nredirects=0\nsecurity=public\n--- body ---\nNot found\n" + ) + await render_events(_aiter([_pretty_call("fetch", '{"url": "https://example.com/nope"}'), _result(result)])) + out = capsys.readouterr().out + assert "* GET 404 https://example.com/nope (html, 45B)" in out + + +async def test_pretty_fetch_truncated(capsys: pytest.CaptureFixture[str]) -> None: + result = ( + "status=200\nmethod=GET\nfinal_url=https://example.com\ncontent_type=text/plain\n" + "body_kind=text\nbytes=50000\ntruncated=true\nredirects=0\nsecurity=public\n--- body ---\n..." + ) + await render_events(_aiter([_pretty_call("fetch", '{"url": "https://example.com"}'), _result(result)])) + out = capsys.readouterr().out + assert "* GET 200 https://example.com (text, 48.8KB, truncated)" in out + + +async def test_pretty_fetch_error(capsys: pytest.CaptureFixture[str]) -> None: + await render_events( + _aiter([_pretty_call("fetch", '{"url": "https://example.com"}'), _result("error: fetch failed: boom")]) + ) + out = capsys.readouterr().out + assert "* GET (error: fetch failed: boom)" in out