cli: add __main__ guard so python -m cmos.cli actually runs
Without `if __name__ == "__main__": sys.exit(main())` at the bottom of cli.py, `python -m cmos.cli format <path>` 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 <path> (pyproject script entry) - uv run python -m cmos.cli format <path> (module invocation)
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user