diff --git a/src/cmos/cli.py b/src/cmos/cli.py index 3559304..beebc77 100644 --- a/src/cmos/cli.py +++ b/src/cmos/cli.py @@ -84,3 +84,7 @@ def main(argv: list[str] | None = None) -> int: sys.stdout.write(reformat_draft(text, concurrency=args.concurrency)) return 0 return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_cli.py b/tests/test_cli.py index 725ae7a..55baf5d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,6 +5,8 @@ formatter so no API calls happen; the goal is to pin the glue logic, not the LLM behavior. """ +import subprocess +import sys from pathlib import Path import pytest @@ -68,6 +70,37 @@ def test_reformat_draft_bibliography_heading_preserved(): assert "## Bibliography" in output +def test_python_dash_m_invocation_actually_runs_main(): + """Regression test: `python -m cmos.cli` must actually invoke main(). + + Without an `if __name__ == "__main__"` guard at the bottom of cli.py, + `python -m cmos.cli` imports the module body but never calls main(), + so the process silently exits 0 with empty stdout. That looks + indistinguishable from a successful run that produced no output — + the worst kind of bug, since callers assume the pipeline ran. This + test forces the guard to exist by invoking the CLI as a subprocess + with --help and asserting argparse actually fired. + """ + result = subprocess.run( + [sys.executable, "-m", "cmos.cli", "--help"], + capture_output=True, + text=True, + timeout=10, + ) + # argparse --help exits 0 whether or not main() ran, so the + # discriminator is the stdout content. Without main(), stdout is empty. + assert result.returncode == 0, ( + f"expected exit 0, got {result.returncode}; stderr={result.stderr!r}" + ) + assert result.stdout, ( + "stdout was empty — `python -m cmos.cli` likely silently exited " + "without invoking main(). Check that cli.py has an " + '`if __name__ == "__main__": sys.exit(main())` guard at the bottom.' + ) + assert "format" in result.stdout + assert "usage" in result.stdout.lower() + + def test_reformat_draft_preserves_order_under_concurrency(): # With concurrent execution the formatter is called on all entries in # parallel; the CLI must reassemble them in input order regardless of