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:
@@ -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")
|
||||
Reference in New Issue
Block a user