mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 17:53:48 +01:00
main.py became main/ with __main__.py, so the invocation string baked into
run.sh and the systemd unit (`python -m steam_backlog_enforcer.main`) stays
byte-identical and nothing needs redeploying. Commands are grouped into
sibling modules (status, install, picks, abandon, misc), with _shared.py as
a leaf holding the lock helpers and constants both need, and _registry.py
holding the command tables. main/__init__.py keeps every public and private
name importable, so `from steam_backlog_enforcer.main import X` is unchanged.
The subtle part is patch targets. The test suite builds them as
f"{PKG}.{name}", so a grep for the literal module path found nothing -- but
once a command moves to a submodule it resolves its dependencies from that
module's globals, and patching the main package's re-export silently stops
biting. Measured which of the 36 patched symbols moved: 8 could have passed
vacuously. Rather than repair PKG per class, the five test_main*.py files
(all cap violators anyway) were regrouped to mirror the production modules,
so each file has exactly one correct PKG. Verified by mutation: breaking a
line in status/misc/picks/_shared each makes the corresponding tests fail.
test_web_server.py patched main_mod.serve, which no longer intercepts
cmd_serve; repointed at main.misc and confirmed it fails when the call is
removed. Added tests for the __main__ shim, which coverage would otherwise
report at 0% under fail_under = 100.
Shared test builders live in tests/_main_helpers.py, reachable because the
Phase 0 name-tests-test exclude allows tests/_*.py.
1290 tests pass; every main/ module reports 100% coverage; `main`, `status`,
`bogus` and `--status` produce byte-identical output and exit codes to
pre-refactor. 55 -> 49 files over the cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPpmMpTSkp8UziRc5u8B7P
137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
"""Tests for the main CLI: argv resolution and command dispatch."""
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from steam_backlog_enforcer.config import Config, State
|
|
from steam_backlog_enforcer.main import (
|
|
_print_usage,
|
|
_resolve_command,
|
|
main,
|
|
)
|
|
|
|
PKG = "steam_backlog_enforcer.main"
|
|
CMD_DONE_PKG = "steam_backlog_enforcer._cmd_done"
|
|
|
|
|
|
class TestMain:
|
|
"""Tests for main CLI entry point."""
|
|
|
|
def test_no_args_exits(self) -> None:
|
|
with (
|
|
patch.object(sys, "argv", ["prog"]),
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
main()
|
|
|
|
def test_unknown_command_exits(self) -> None:
|
|
with (
|
|
patch.object(sys, "argv", ["prog", "bogus"]),
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
main()
|
|
|
|
def test_valid_command_runs(self) -> None:
|
|
mock_cmd = MagicMock()
|
|
with (
|
|
patch.object(sys, "argv", ["prog", "status"]),
|
|
patch(f"{PKG}.Config.load", return_value=Config(steam_api_key="k")),
|
|
patch(f"{PKG}.State.load", return_value=State()),
|
|
patch.dict(f"{PKG}.COMMANDS", {"status": ("s", mock_cmd)}),
|
|
):
|
|
main()
|
|
mock_cmd.assert_called_once()
|
|
|
|
def test_setup_no_key_required(self) -> None:
|
|
mock_cmd = MagicMock()
|
|
with (
|
|
patch.object(sys, "argv", ["prog", "setup"]),
|
|
patch(f"{PKG}.Config.load", return_value=Config()),
|
|
patch(f"{PKG}.State.load", return_value=State()),
|
|
patch.dict(f"{PKG}.COMMANDS", {"setup": ("s", mock_cmd)}),
|
|
):
|
|
main()
|
|
mock_cmd.assert_called_once()
|
|
|
|
def test_no_api_key_exits(self) -> None:
|
|
with (
|
|
patch.object(sys, "argv", ["prog", "status"]),
|
|
patch(f"{PKG}.Config.load", return_value=Config()),
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
main()
|
|
|
|
def test_dashed_command_dispatches_with_note(self) -> None:
|
|
"""'--status' runs 'status' and says so."""
|
|
mock_cmd = MagicMock()
|
|
with (
|
|
patch.object(sys, "argv", ["prog", "--status"]),
|
|
patch(f"{PKG}.Config.load", return_value=Config(steam_api_key="k")),
|
|
patch(f"{PKG}.State.load", return_value=State()),
|
|
patch.dict(f"{PKG}.COMMANDS", {"status": ("s", mock_cmd)}),
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
):
|
|
main()
|
|
mock_cmd.assert_called_once()
|
|
assert any(
|
|
"treating '--status' as 'status'" in str(call.args[0])
|
|
for call in mock_echo.call_args_list
|
|
if call.args
|
|
)
|
|
|
|
@pytest.mark.parametrize("raw", ["bogus", "--bogus"])
|
|
def test_unknown_command_names_the_input(self, raw: str) -> None:
|
|
with (
|
|
patch.object(sys, "argv", ["prog", raw]),
|
|
patch("steam_backlog_enforcer.main._registry._echo") as mock_echo,
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
main()
|
|
assert any(
|
|
str(call.args[0]).startswith(f"Unknown command: {raw}")
|
|
for call in mock_echo.call_args_list
|
|
if call.args
|
|
)
|
|
|
|
|
|
class TestResolveCommand:
|
|
"""Tests for argv[1] normalisation and the usage printer."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw", "expected"),
|
|
[
|
|
("status", "status"),
|
|
("--status", "status"),
|
|
("--abandon-pick", "abandon-pick"),
|
|
("-status", "status"),
|
|
("bogus", None),
|
|
("--bogus", None),
|
|
("--", None),
|
|
],
|
|
)
|
|
def test_resolve(self, raw: str, expected: str | None) -> None:
|
|
assert _resolve_command(raw) == expected
|
|
|
|
def test_usage_suggests_close_match(self) -> None:
|
|
with patch("steam_backlog_enforcer.main._registry._echo") as mock_echo:
|
|
_print_usage("statu")
|
|
lines = [str(c.args[0]) for c in mock_echo.call_args_list if c.args]
|
|
assert "Did you mean 'status'?" in lines
|
|
|
|
def test_usage_omits_suggestion_when_far_off(self) -> None:
|
|
with patch("steam_backlog_enforcer.main._registry._echo") as mock_echo:
|
|
_print_usage("--zzzzzz")
|
|
lines = [str(c.args[0]) for c in mock_echo.call_args_list if c.args]
|
|
assert not any(line.startswith("Did you mean") for line in lines)
|
|
|
|
def test_usage_without_unknown_has_no_error_line(self) -> None:
|
|
with patch("steam_backlog_enforcer.main._registry._echo") as mock_echo:
|
|
_print_usage()
|
|
lines = [str(c.args[0]) for c in mock_echo.call_args_list if c.args]
|
|
assert not any(line.startswith("Unknown command") for line in lines)
|