diff --git a/openfoia/cli.py b/openfoia/cli.py index 49e79b9..84921e1 100644 --- a/openfoia/cli.py +++ b/openfoia/cli.py @@ -3916,24 +3916,29 @@ def records_search( if filing_type: kwargs["filing_type"] = filing_type - with Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - console=console, - ) as progress: - progress.add_task(f"Searching {source} for '{query}'...", total=None) - - try: + try: + if raw: + # Raw output is intended for pipes and scripts, so it must contain + # only JSON -- no spinner or Rich markup before the document. result = asyncio.run(adapter.search(query, **kwargs)) - except Exception as e: - rprint(f"[red]Search failed: {e}[/red]") - raise typer.Exit(1) from None + else: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console, + ) as progress: + progress.add_task(f"Searching {source} for '{query}'...", total=None) + result = asyncio.run(adapter.search(query, **kwargs)) + except Exception as e: + rprint(f"[red]Search failed: {e}[/red]") + raise typer.Exit(1) from None if raw: - rprint( + typer.echo( json.dumps( [e.to_dict() for e in result.entities[:limit]], indent=2, + ensure_ascii=False, default=str, ) ) diff --git a/tests/test_records_cli.py b/tests/test_records_cli.py new file mode 100644 index 0000000..cd40563 --- /dev/null +++ b/tests/test_records_cli.py @@ -0,0 +1,53 @@ +"""Regression tests for public-records CLI output.""" + +from __future__ import annotations + +import json + +from typer.testing import CliRunner + +from openfoia.cli import app +from openfoia.records.base import RecordEntity, SearchResult + + +def test_records_search_raw_is_parseable_json_without_terminal_output(monkeypatch): + """Raw mode must be pipe-safe even when upstream data has control characters.""" + + class Adapter: + async def search(self, query, **kwargs): + return SearchResult( + source="sec", + query=query, + total_results=1, + entities=[ + RecordEntity( + entity_type="ORGANIZATION", + name="Uranium\x1fEnergy", + source="sec", + extra_data={"snippet": "filing\x00text"}, + ) + ], + ) + + monkeypatch.setattr("openfoia.records.get_adapter", lambda source: Adapter()) + + result = CliRunner().invoke( + app, + ["records", "search", "Uranium Energy", "--source", "sec", "--raw"], + ) + + assert result.exit_code == 0 + assert "\x1f" not in result.stdout + assert "\x00" not in result.stdout + assert json.loads(result.stdout) == [ + { + "entity_type": "ORGANIZATION", + "name": "Uranium\x1fEnergy", + "source": "sec", + "source_url": None, + "jurisdiction": None, + "status": None, + "identifiers": {}, + "extra_data": {"snippet": "filing\x00text"}, + } + ]