From 22404fbd6b9efdbb282d8664f8203b01c8dc3982 Mon Sep 17 00:00:00 2001 From: Mark Eaton Date: Sat, 11 Apr 2026 17:45:18 -0400 Subject: [PATCH] cli: add __main__ guard so `python -m cmos.cli` actually runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without `if __name__ == "__main__": sys.exit(main())` at the bottom of cli.py, `python -m cmos.cli format ` imports the module but never invokes main(), so the process silently exits 0 with empty stdout — indistinguishable from a successful run that produced no output. Discovered during real-draft testing on 2026-04-11. Adds a regression test that subprocesses the CLI with --help and asserts on stdout content. argparse --help exits 0 in both broken and fixed states; stdout content is the only discriminator. Both invocation paths now work: - uv run cmos format (pyproject script entry) - uv run python -m cmos.cli format (module invocation) --- src/cmos/cli.py | 4 ++++ tests/test_cli.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) 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