diff --git a/backend/tests/test_cli_reissue_claims.py b/backend/tests/test_cli_reissue_claims.py new file mode 100644 index 0000000..2886aea --- /dev/null +++ b/backend/tests/test_cli_reissue_claims.py @@ -0,0 +1,183 @@ +"""SP24 — tests for the `cyclone reissue-claims` CLI subcommand. + +Five smoke tests covering the canonical cyclone-cli pattern: + + 1. --help renders cleanly and lists the long-form flags. + 2. happy path: a single-file input dir produces the expected + number of output files. + 3. empty input dir: exits 2 with a "PARSE FAILED" message. + 4. IG-correctness guard fires when the constant is monkeypatched + to True; CLI exits 1 with the REFUSING log line. + 5. --zip-output produces a valid zip with round-trip integrity. +""" +from __future__ import annotations + +import zipfile +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from cyclone.cli import main + +# Canonical minimal-claim fixture — flat, module-level Path constant +# per the cyclone-tests convention. NEVER reach into docs/prodfiles/. +MINIMAL_837P = Path(__file__).parent / "fixtures" / "minimal_837p.txt" + + +@pytest.fixture +def _input_dir(tmp_path: Path) -> Path: + """Drop the minimal_837p fixture into tmp_path/in/.""" + in_dir = tmp_path / "in" + in_dir.mkdir() + (in_dir / "real.x12").write_text(MINIMAL_837P.read_text()) + return in_dir + + +def test_reissue_claims_help_renders(): + """--help exits 0 and lists every long-form flag in the SP24 spec.""" + runner = CliRunner() + result = runner.invoke(main, ["reissue-claims", "--help"]) + + assert result.exit_code == 0, result.output + # Required flag. + assert "--input-dir" in result.output + # Optional flags documented in the spec §2 Decision 3. + for flag in [ + "--output-root", + "--date", + "--pipeline", + "--payer", + "--sender-id", + "--receiver-id", + "--submitter-name", + "--submitter-contact-name", + "--submitter-contact-email", + "--receiver-name", + "--zip-output", + "--no-clean", + "--log-level", + ]: + assert flag in result.output, f"missing {flag} in help output" + + +def test_reissue_claims_happy_path(_input_dir: Path, tmp_path: Path): + """A single-file input dir produces 1 .x12 output + summary sidecar.""" + runner = CliRunner() + out_root = tmp_path / "out" + + result = runner.invoke( + main, + [ + "reissue-claims", + "--input-dir", str(_input_dir), + "--output-root", str(out_root), + "--date", "2026-07-08", + "--pipeline", "initial", + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0, ( + f"CLI exited {result.exit_code}, output={result.output!r}, " + f"exception={result.exception!r}" + ) + # 1 .x12 + 1 _serialize_summary.json = 2 entries. + emitted_dir = out_root / "2026-07-08" / "initial" + assert emitted_dir.is_dir() + files = sorted(emitted_dir.iterdir()) + x12s = [f for f in files if f.suffix == ".x12"] + assert len(x12s) == 1 + assert (emitted_dir / "_serialize_summary.json").is_file() + # HCPF-spec filename. + assert x12s[0].name.startswith("tp11525703-837P-") + assert x12s[0].name.endswith("-1of1.x12") + # DONE summary line is in stdout. + assert "DONE files=1 errors=0" in result.output + + +def test_reissue_claims_empty_input_exits_2(tmp_path: Path): + """An empty input dir exits 2 with a clear PARSE FAILED message.""" + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + out_root = tmp_path / "out" + + runner = CliRunner() + result = runner.invoke( + main, + [ + "reissue-claims", + "--input-dir", str(empty_dir), + "--output-root", str(out_root), + ], + catch_exceptions=False, + ) + + # No claims → exit 2. + assert result.exit_code == 2, ( + f"CLI exited {result.exit_code}, output={result.output!r}" + ) + # The error message should explain the failure. + combined = result.output + (result.stderr or "") + assert "PARSE FAILED" in combined or "no claims" in combined.lower() + + +def test_reissue_claims_ig_correctness_guard_fires(monkeypatch, _input_dir: Path, tmp_path: Path): + """When PATIENT_LOOP_DEFAULT_INCLUDED is True, the CLI refuses to run.""" + # Monkeypatch the serializer constant to the broken value. + # `raising=False` lets us set an attribute that didn't exist + # pre-import (defensive against pytest collection order). + from cyclone.parsers import serialize_837 as ser_mod + monkeypatch.setattr(ser_mod, "PATIENT_LOOP_DEFAULT_INCLUDED", True) + + runner = CliRunner() + out_root = tmp_path / "out" + result = runner.invoke( + main, + [ + "reissue-claims", + "--input-dir", str(_input_dir), + "--output-root", str(out_root), + ], + catch_exceptions=False, + ) + + # Guard fires → exit 1. + assert result.exit_code == 1, ( + f"CLI exited {result.exit_code} (expected 1); output={result.output!r}" + ) + # The refusal message is in stderr (click.echo(..., err=True)). + combined = result.output + (result.stderr or "") + assert "REFUSING to run" in combined + assert "PATIENT_LOOP_DEFAULT_INCLUDED" in combined + + +def test_reissue_claims_zip_output_round_trip(_input_dir: Path, tmp_path: Path): + """--zip-output writes a zip whose testzip() returns None.""" + runner = CliRunner() + out_root = tmp_path / "out" + zip_path = tmp_path / "out.zip" + + result = runner.invoke( + main, + [ + "reissue-claims", + "--input-dir", str(_input_dir), + "--output-root", str(out_root), + "--date", "2026-07-08", + "--zip-output", str(zip_path), + ], + catch_exceptions=False, + ) + + assert result.exit_code == 0, result.output + assert zip_path.is_file() + + with zipfile.ZipFile(zip_path) as zf: + # None = no corrupt entries. + assert zf.testzip() is None + names = zf.namelist() + # 1 X12 file in the zip (the summary sidecar is not zipped). + assert len(names) == 1 + assert names[0].startswith("tp11525703-837P-") + assert names[0].endswith("-1of1.x12") \ No newline at end of file