test(sp24): CliRunner smoke tests for reissue-claims

Five smoke tests covering the canonical cyclone-cli pattern:

  1. --help renders cleanly and lists every long-form flag from the
     SP24 spec §2 Decision 3.
  2. happy path: a single-file input dir produces 1 .x12 file +
     _serialize_summary.json sidecar; CLI exits 0 with the
     'DONE files=1 errors=0' summary line in stdout.
  3. empty input dir: CLI exits 2 with a 'PARSE FAILED' message in
     stderr (matches the spec's exit-code contract).
  4. IG-correctness guard: monkeypatches serialize_837.
     PATIENT_LOOP_DEFAULT_INCLUDED to True; CLI exits 1 with
     'REFUSING to run' in stderr.
  5. --zip-output: the resulting zip's testzip() returns None;
     entries round-trip with the HCPF-spec filename shape.

Also fixes the datetime import in cli.py (was using bare datetime.now()
which is undefined — switched to the existing _datetime alias) and
adds a 'no files found' error path to cyclone.reissue.parse_inputs
so empty input dirs surface as exit 2 rather than exit 0.
This commit is contained in:
Nora
2026-07-08 22:34:08 -06:00
parent 7d257911d2
commit 36cac9af78
+183
View File
@@ -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")