mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 18:13:49 +01:00
Each of these was one file doing several jobs, so they are split along what they actually do rather than at arbitrary line offsets: - _total_block: the lock file and the status derived from it stay in _total_block; making Steam *not installed* moves to _total_block_purge, making it *unreachable* to _total_block_hosts / _total_block_iptables. The generic pacman verbs (owner, installed, remove) become _pacman, since nothing about them is total-block-specific. Domain and process name tables become data modules. - _playtime: budget state and persistence (_playtime_state), pure transitions (_playtime_budget), cmdline process detection (_playtime_procs), the warn/cutoff/kill ladder (_playtime_cutoff), desktop notifications (_playtime_notify), process-tree walking and killing (_playtime_kill), mountinfo parsing (_playtime_mountinfo) and the shared privileged-command helper (_playtime_run). - _mcp: read-only tools (_mcp_query) split from mutating ones (_mcp_actions, _mcp_gaming_time), so "what can this server change?" is answerable from one file. _mcp keeps the server object and entry point. No re-export shims: callers import from the module that defines the name. That keeps a stale patch target loud (AttributeError) instead of letting it pass while intercepting nothing. `ruff check --select F821,F401` is run after every extraction -- an earlier mechanical attempt moved functions without the module-level names they referenced, which imported fine and only failed when called (349 errors), so it was reverted and redone by hand. The autouse fixtures in conftest are now imported by name and held by __all__; ruff --unsafe-fixes had deleted them as unused, since pytest resolves fixtures by name. 1324 tests pass at 100.00% coverage in the clean venv; `main status` is byte-identical to a worktree at HEAD. 49 -> 39 files over the cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPpmMpTSkp8UziRc5u8B7P
141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
"""Tests for the read-only MCP tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
from steam_backlog_enforcer import _mcp_actions as mcp_actions
|
|
from steam_backlog_enforcer import _mcp_query as mcp_query
|
|
from steam_backlog_enforcer import _mcp_server as mcp_server
|
|
from steam_backlog_enforcer.steam_api import GameInfo
|
|
|
|
|
|
def _game(app_id: int, hours: float) -> GameInfo:
|
|
return GameInfo(
|
|
app_id=app_id,
|
|
name=f"g{app_id}",
|
|
total_achievements=10,
|
|
unlocked_achievements=1,
|
|
playtime_minutes=0,
|
|
completionist_hours=hours,
|
|
)
|
|
|
|
|
|
class TestReadTools:
|
|
def test_get_dataset(self) -> None:
|
|
with (
|
|
patch.object(mcp_query, "State") as state,
|
|
patch.object(mcp_query, "build_web_dataset", return_value="DS") as bwd,
|
|
patch.object(mcp_query, "dataset_to_payload", return_value={"x": 1}) as dtp,
|
|
):
|
|
assert mcp_query.get_dataset() == {"x": 1}
|
|
bwd.assert_called_once_with(state.load.return_value)
|
|
dtp.assert_called_once_with("DS")
|
|
|
|
def test_get_status(self) -> None:
|
|
with (
|
|
patch.object(mcp_query, "State") as state,
|
|
patch.object(mcp_query, "status_payload", return_value={"ok": 1}) as sp,
|
|
):
|
|
assert mcp_query.get_status() == {"ok": 1}
|
|
sp.assert_called_once_with(state.load.return_value)
|
|
|
|
def test_get_stats_subsets_dataset(self) -> None:
|
|
payload = {
|
|
"default_summary": {"qualifying": 3},
|
|
"pace_vs_hltb": None,
|
|
"games": ["ignored"],
|
|
}
|
|
with (
|
|
patch.object(mcp_query, "State"),
|
|
patch.object(mcp_query, "build_web_dataset"),
|
|
patch.object(mcp_query, "dataset_to_payload", return_value=payload),
|
|
):
|
|
out = mcp_query.get_stats()
|
|
assert out == {"default_summary": {"qualifying": 3}, "pace_vs_hltb": None}
|
|
|
|
|
|
class TestListBacklog:
|
|
def test_no_snapshot_returns_note(self) -> None:
|
|
with patch.object(mcp_query, "load_snapshot", return_value=None):
|
|
out = mcp_query.list_backlog()
|
|
assert out["total"] == 0
|
|
assert out["games"] == []
|
|
assert "note" in out
|
|
|
|
def test_sorts_shortest_first_excludes_complete_and_caps(self) -> None:
|
|
snap = [
|
|
{
|
|
"app_id": 440,
|
|
"name": "TF2",
|
|
"total_achievements": 100,
|
|
"unlocked_achievements": 10,
|
|
"completionist_hours": 50.0,
|
|
},
|
|
# complete → excluded
|
|
{
|
|
"app_id": 620,
|
|
"name": "Portal 2",
|
|
"total_achievements": 50,
|
|
"unlocked_achievements": 50,
|
|
"completionist_hours": 20.0,
|
|
},
|
|
{
|
|
"app_id": 70,
|
|
"name": "HL",
|
|
"total_achievements": 10,
|
|
"unlocked_achievements": 1,
|
|
"completionist_hours": 12.0,
|
|
},
|
|
# unknown hours → sorted last
|
|
{
|
|
"app_id": 30,
|
|
"name": "NoHrs",
|
|
"total_achievements": 10,
|
|
"unlocked_achievements": 0,
|
|
"completionist_hours": -1,
|
|
},
|
|
]
|
|
with patch.object(mcp_query, "load_snapshot", return_value=snap):
|
|
out = mcp_query.list_backlog(limit=2)
|
|
assert out["total"] == 3
|
|
assert out["returned"] == 2
|
|
assert [g["app_id"] for g in out["games"]] == [70, 440]
|
|
assert out["games"][0]["completion_pct"] == 10.0
|
|
|
|
def test_negative_limit_returns_none(self) -> None:
|
|
snap = [
|
|
{
|
|
"app_id": 70,
|
|
"name": "HL",
|
|
"total_achievements": 10,
|
|
"unlocked_achievements": 1,
|
|
}
|
|
]
|
|
with patch.object(mcp_query, "load_snapshot", return_value=snap):
|
|
out = mcp_query.list_backlog(limit=-5)
|
|
assert out["returned"] == 0
|
|
assert out["games"] == []
|
|
|
|
|
|
class TestResolveAndSort:
|
|
def test_resolve_found(self) -> None:
|
|
with patch.object(
|
|
mcp_server, "load_snapshot", return_value=[{"app_id": 440, "name": "TF2"}]
|
|
):
|
|
assert mcp_actions._resolve_game_name(440) == "TF2"
|
|
|
|
def test_resolve_missing(self) -> None:
|
|
with patch.object(
|
|
mcp_server, "load_snapshot", return_value=[{"app_id": 1, "name": "X"}]
|
|
):
|
|
assert mcp_actions._resolve_game_name(440) is None
|
|
|
|
def test_resolve_no_snapshot(self) -> None:
|
|
with patch.object(mcp_query, "load_snapshot", return_value=None):
|
|
assert mcp_actions._resolve_game_name(440) is None
|
|
|
|
def test_sort_key_branches(self) -> None:
|
|
assert mcp_query._backlog_sort_key(_game(1, 5.0)) == (0, 5.0)
|
|
assert mcp_query._backlog_sort_key(_game(2, -1)) == (1, 0.0)
|