mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-07-04 13:43:45 +02:00
fix: HLTB matching uses game_alias for renamed games & hide library on reassign
- _pick_best_hltb_entry: check game_alias in exact-match fallback so renamed games (e.g. 'Needy Streamer Overload' -> 'NEEDY GIRL OVERDOSE') are not beaten by spinoffs with matching prefixes - _try_reassign_shorter_game: call hide_other_games after pick_next_game so library visibility is updated on reassignment, not only on completion - Added tests for alias matching and all hiding branches (100% coverage)
This commit is contained in:
parent
06b3674133
commit
6a9f137845
@ -108,6 +108,14 @@ def _try_reassign_shorter_game(
|
|||||||
f" (~{playable.completionist_hours:.1f}h vs ~{hours:.1f}h)"
|
f" (~{playable.completionist_hours:.1f}h vs ~{hours:.1f}h)"
|
||||||
)
|
)
|
||||||
pick_next_game(all_games, state, config)
|
pick_next_game(all_games, state, config)
|
||||||
|
|
||||||
|
if state.current_app_id is not None:
|
||||||
|
owned_ids = get_all_owned_app_ids(config)
|
||||||
|
if owned_ids:
|
||||||
|
hidden = hide_other_games(owned_ids, state.current_app_id)
|
||||||
|
if hidden > 0:
|
||||||
|
_echo(f"\n Library: hid {hidden} games")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -185,6 +185,7 @@ def _pick_best_hltb_entry(
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
if (e.get("game_name") or "").lower() == lower
|
if (e.get("game_name") or "").lower() == lower
|
||||||
|
or (e.get("game_alias") or "").lower() == lower
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class TestTryReassignShorterGame:
|
|||||||
_snap(1, "Long", 10, 5, 100.0),
|
_snap(1, "Long", 10, 5, 100.0),
|
||||||
_snap(2, "Short", 10, 5, 5.0),
|
_snap(2, "Short", 10, 5, 5.0),
|
||||||
]
|
]
|
||||||
state = State(current_app_id=1, current_game_name="Long")
|
state = State(current_app_id=2, current_game_name="Short")
|
||||||
short_game = GameInfo(
|
short_game = GameInfo(
|
||||||
app_id=2,
|
app_id=2,
|
||||||
name="Short",
|
name="Short",
|
||||||
@ -73,6 +73,11 @@ class TestTryReassignShorterGame:
|
|||||||
return_value=short_game,
|
return_value=short_game,
|
||||||
),
|
),
|
||||||
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
||||||
|
patch(
|
||||||
|
f"{CMD_DONE_PKG}.get_all_owned_app_ids",
|
||||||
|
return_value=[1, 2, 3],
|
||||||
|
),
|
||||||
|
patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=5) as mock_hide,
|
||||||
):
|
):
|
||||||
result = _try_reassign_shorter_game(
|
result = _try_reassign_shorter_game(
|
||||||
{1: 100.0, 2: 5.0},
|
{1: 100.0, 2: 5.0},
|
||||||
@ -82,6 +87,80 @@ class TestTryReassignShorterGame:
|
|||||||
Config(),
|
Config(),
|
||||||
)
|
)
|
||||||
assert result
|
assert result
|
||||||
|
mock_hide.assert_called_once_with([1, 2, 3], 2)
|
||||||
|
|
||||||
|
def test_reassigns_no_hide_when_no_owned_ids(self) -> None:
|
||||||
|
snap = [
|
||||||
|
_snap(1, "Long", 10, 5, 100.0),
|
||||||
|
_snap(2, "Short", 10, 5, 5.0),
|
||||||
|
]
|
||||||
|
state = State(current_app_id=2, current_game_name="Short")
|
||||||
|
short_game = GameInfo(
|
||||||
|
app_id=2,
|
||||||
|
name="Short",
|
||||||
|
total_achievements=10,
|
||||||
|
unlocked_achievements=5,
|
||||||
|
playtime_minutes=60,
|
||||||
|
completionist_hours=5.0,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
||||||
|
patch(f"{CMD_DONE_PKG}._echo") as mock_echo,
|
||||||
|
patch(
|
||||||
|
f"{CMD_DONE_PKG}._pick_playable_candidate",
|
||||||
|
return_value=short_game,
|
||||||
|
),
|
||||||
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
||||||
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[1, 2]),
|
||||||
|
patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=0),
|
||||||
|
):
|
||||||
|
result = _try_reassign_shorter_game(
|
||||||
|
{1: 100.0, 2: 5.0},
|
||||||
|
1,
|
||||||
|
100.0,
|
||||||
|
state,
|
||||||
|
Config(),
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
# hidden == 0, so "hid N games" should NOT be echoed
|
||||||
|
for call in mock_echo.call_args_list:
|
||||||
|
assert "hid" not in str(call)
|
||||||
|
|
||||||
|
def test_reassigns_skip_hide_when_no_app_assigned(self) -> None:
|
||||||
|
snap = [
|
||||||
|
_snap(1, "Long", 10, 5, 100.0),
|
||||||
|
_snap(2, "Short", 10, 5, 5.0),
|
||||||
|
]
|
||||||
|
state = State(current_app_id=None, current_game_name="")
|
||||||
|
short_game = GameInfo(
|
||||||
|
app_id=2,
|
||||||
|
name="Short",
|
||||||
|
total_achievements=10,
|
||||||
|
unlocked_achievements=5,
|
||||||
|
playtime_minutes=60,
|
||||||
|
completionist_hours=5.0,
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
||||||
|
patch(f"{CMD_DONE_PKG}._echo"),
|
||||||
|
patch(
|
||||||
|
f"{CMD_DONE_PKG}._pick_playable_candidate",
|
||||||
|
return_value=short_game,
|
||||||
|
),
|
||||||
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
||||||
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids") as mock_owned,
|
||||||
|
patch(f"{CMD_DONE_PKG}.hide_other_games") as mock_hide,
|
||||||
|
):
|
||||||
|
result = _try_reassign_shorter_game(
|
||||||
|
{1: 100.0, 2: 5.0},
|
||||||
|
1,
|
||||||
|
100.0,
|
||||||
|
state,
|
||||||
|
Config(),
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
mock_owned.assert_not_called()
|
||||||
|
mock_hide.assert_not_called()
|
||||||
|
|
||||||
def test_playable_none(self) -> None:
|
def test_playable_none(self) -> None:
|
||||||
snap = [
|
snap = [
|
||||||
@ -157,6 +236,8 @@ class TestTryReassignShorterGame:
|
|||||||
) as mock_pick_playable,
|
) as mock_pick_playable,
|
||||||
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
patch(f"{CMD_DONE_PKG}.pick_next_game"),
|
||||||
patch(f"{CMD_DONE_PKG}._echo"),
|
patch(f"{CMD_DONE_PKG}._echo"),
|
||||||
|
patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[]),
|
||||||
|
patch(f"{CMD_DONE_PKG}.hide_other_games"),
|
||||||
):
|
):
|
||||||
result = _try_reassign_shorter_game(
|
result = _try_reassign_shorter_game(
|
||||||
{1: 20.1},
|
{1: 20.1},
|
||||||
|
|||||||
@ -321,3 +321,27 @@ class TestPickBestHltbEntry:
|
|||||||
result = _pick_best_hltb_entry("Killing Floor", [(spinoff, 0.7), (base, 1.0)])
|
result = _pick_best_hltb_entry("Killing Floor", [(spinoff, 0.7), (base, 1.0)])
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result[0]["game_name"] == "Killing Floor"
|
assert result[0]["game_name"] == "Killing Floor"
|
||||||
|
|
||||||
|
def test_alias_exact_match_beats_spinoff(self) -> None:
|
||||||
|
"""Base game found via game_alias beats a spinoff with matching prefix.
|
||||||
|
|
||||||
|
When HLTB renames a game (e.g. 'Needy Streamer Overload' ->
|
||||||
|
'NEEDY GIRL OVERDOSE'), the old name lives in game_alias. A spinoff
|
||||||
|
like 'Needy Streamer Overload: Typing of The Net' must NOT be
|
||||||
|
preferred just because its game_name starts with the search term.
|
||||||
|
"""
|
||||||
|
base: dict[str, Any] = {
|
||||||
|
"game_name": "NEEDY GIRL OVERDOSE",
|
||||||
|
"game_alias": "Needy Streamer Overload",
|
||||||
|
"comp_100": 43200, # 12 h
|
||||||
|
}
|
||||||
|
spinoff: dict[str, Any] = {
|
||||||
|
"game_name": "Needy Streamer Overload: Typing of The Net",
|
||||||
|
"comp_100": 3600, # 1 h
|
||||||
|
}
|
||||||
|
result = _pick_best_hltb_entry(
|
||||||
|
"NEEDY STREAMER OVERLOAD",
|
||||||
|
[(spinoff, 0.7), (base, 0.9)],
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
assert result[0]["game_name"] == "NEEDY GIRL OVERDOSE"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user