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))
|
sys.stdout.write(reformat_draft(text, concurrency=args.concurrency))
|
||||||
return 0
|
return 0
|
||||||
return 2
|
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.
|
LLM behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -68,6 +70,37 @@ def test_reformat_draft_bibliography_heading_preserved():
|
|||||||
assert "## Bibliography" in output
|
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():
|
def test_reformat_draft_preserves_order_under_concurrency():
|
||||||
# With concurrent execution the formatter is called on all entries in
|
# With concurrent execution the formatter is called on all entries in
|
||||||
# parallel; the CLI must reassemble them in input order regardless of
|
# parallel; the CLI must reassemble them in input order regardless of
|
||||||
|
|||||||
Reference in New Issue
Block a user