Files
cmos/tests/test_cli.py
T
Mark Eaton 22404fbd6b 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)
2026-04-11 17:45:18 -04:00

137 lines
4.3 KiB
Python

"""Tests for src/cmos/cli.py — end-to-end draft reformatting.
The CLI orchestrates parser → formatter → reassemble. Tests inject a fake
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
from cmos.cli import reformat_draft
from cmos.parser import NoBibliographyError
def _fake_formatter(messy: str) -> str:
# Deterministic fake: return a cleaned-up marker string so the output is
# recognizable.
return f"FORMATTED({messy.strip()})"
def test_reformat_draft_rewrites_only_bibliography_section():
draft = """\
# My Paper
Some prose with citations.
## Bibliography
yu, charles. interior chinatown. 2020.
kwon, hyeyoung. inclusion work. 2022.
"""
output = reformat_draft(draft, formatter=_fake_formatter)
assert "# My Paper" in output
assert "Some prose with citations." in output
assert "FORMATTED(yu, charles. interior chinatown. 2020.)" in output
assert "FORMATTED(kwon, hyeyoung. inclusion work. 2022.)" in output
# Original messy lines must not also survive.
assert "yu, charles. interior chinatown. 2020." not in output.replace(
"FORMATTED(yu, charles. interior chinatown. 2020.)", ""
)
def test_reformat_draft_preserves_sections_after_bibliography():
draft = """\
## Bibliography
one entry.
## Appendix
appendix text.
"""
output = reformat_draft(draft, formatter=_fake_formatter)
assert "FORMATTED(one entry.)" in output
assert "## Appendix" in output
assert "appendix text." in output
def test_reformat_draft_raises_when_no_bibliography():
with pytest.raises(NoBibliographyError):
reformat_draft("## Intro\n\nno bib here.\n", formatter=_fake_formatter)
def test_reformat_draft_bibliography_heading_preserved():
draft = "## Bibliography\n\nentry.\n"
output = reformat_draft(draft, formatter=_fake_formatter)
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
# which call finishes first. This test uses a fake formatter that
# sleeps based on the entry content so earlier entries finish AFTER
# later ones if order were naively tied to completion.
import time
def slow_fake(messy: str) -> str:
# Entries with lower index sleep longer so they finish last.
n = int(messy.split()[-1])
time.sleep(0.05 * (5 - n))
return f"FORMATTED({messy})"
draft = """\
## Bibliography
entry 0
entry 1
entry 2
entry 3
entry 4
"""
output = reformat_draft(draft, formatter=slow_fake, concurrency=4)
# Entries must appear in input order.
lines = [l for l in output.splitlines() if l.startswith("FORMATTED(")]
assert lines == [
"FORMATTED(entry 0)",
"FORMATTED(entry 1)",
"FORMATTED(entry 2)",
"FORMATTED(entry 3)",
"FORMATTED(entry 4)",
]