mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 14:27:33 +01:00
Requiring 100% locked the user onto one game for weeks, which proved ineffective. The assignment and every manual pick now release as soon as one achievement is unlocked after the game was assigned (Steam unlock_time vs the pick's started_at / State.current_assigned_at). - finished_app_ids still means 100% complete; a game released below 100% gets released_at plus a 7-day cooldown and returns to the pool. - Next pick: least recently assigned first, then shortest HLTB time. - Releasing never evicts: released/expired picks stay installed and playable until the user explicitly chooses another game (new pick-manual, or accepting the next assignment); with no replacement the current game stays. - Tampering detection judges a released game by unlocks after its release. - The daemon reloads every State field each tick so its sweep save cannot overwrite cooldowns or assignment times written by the CLI. - State.load backfills assignment times once and persists them; load-time migrations moved to _state_migrations.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PhzaTAWtRfJk8gre6NLSka
200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
"""Tests for main CLI module — part 2 (missing coverage)."""
|
|
|
|
from typing import Any
|
|
from unittest.mock import (
|
|
patch,
|
|
)
|
|
|
|
from steam_backlog_enforcer._cmd_done_finalize import _finalize_completion
|
|
from steam_backlog_enforcer.config import Config, State
|
|
from steam_backlog_enforcer.tests._main_helpers import done_game
|
|
|
|
CMD_DONE_PKG = "steam_backlog_enforcer._cmd_done_finalize"
|
|
PKG = "steam_backlog_enforcer.main"
|
|
|
|
|
|
def _snap(
|
|
app_id: int = 1,
|
|
name: str = "G",
|
|
total: int = 10,
|
|
unlocked: int = 0,
|
|
hours: float = -1,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"app_id": app_id,
|
|
"name": name,
|
|
"total_achievements": total,
|
|
"unlocked_achievements": unlocked,
|
|
"playtime_minutes": 60,
|
|
"completionist_hours": hours,
|
|
}
|
|
|
|
|
|
class TestFinalizeCompletion:
|
|
def test_with_snapshot_and_hiding(self) -> None:
|
|
config = Config(steam_api_key="k", steam_id="i")
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
snap = [_snap(2, "NewGame", 10, 0, 5.0)]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game") as mock_pick,
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.get_all_owned_app_ids",
|
|
return_value=[1, 2, 3],
|
|
),
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.try_hide_other_games",
|
|
return_value=(2, None),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.send_notification"),
|
|
patch.object(State, "save"),
|
|
):
|
|
|
|
def set_next(
|
|
_games: object,
|
|
s: State,
|
|
_c: object,
|
|
**_kwargs: object,
|
|
) -> None:
|
|
s.current_app_id = 2
|
|
s.current_game_name = "NewGame"
|
|
|
|
mock_pick.side_effect = set_next
|
|
_finalize_completion(config, state, done_game(1, "G"))
|
|
assert 1 in state.finished_app_ids
|
|
|
|
def test_hide_skipped_when_steam_unreachable(self) -> None:
|
|
# Reconciliation is best-effort: an undrivable Steam must not abort
|
|
# the completion flow.
|
|
config = Config(steam_api_key="k", steam_id="i")
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
snap = [_snap(2, "NewGame", 10, 0, 5.0)]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}._echo") as mock_echo,
|
|
# The skip line is printed by the shared hide helper.
|
|
patch("steam_backlog_enforcer.library_hider._echo", mock_echo),
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game") as mock_pick,
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.get_all_owned_app_ids",
|
|
return_value=[1, 2, 3],
|
|
),
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.try_hide_other_games",
|
|
return_value=(0, "update in progress"),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.is_game_fully_installed", return_value=True),
|
|
patch(f"{CMD_DONE_PKG}.send_notification"),
|
|
patch.object(State, "save"),
|
|
):
|
|
|
|
def set_next(
|
|
_games: object,
|
|
s: State,
|
|
_c: object,
|
|
**_kwargs: object,
|
|
) -> None:
|
|
s.current_app_id = 2
|
|
s.current_game_name = "NewGame"
|
|
|
|
mock_pick.side_effect = set_next
|
|
_finalize_completion(config, state, done_game(1, "G"))
|
|
assert "skipped (update in progress)" in " ".join(
|
|
str(c) for c in mock_echo.call_args_list
|
|
)
|
|
|
|
def test_no_snapshot(self) -> None:
|
|
config = Config()
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=None),
|
|
patch.object(State, "save"),
|
|
):
|
|
_finalize_completion(config, state, done_game(1, "G"))
|
|
# No replacement was chosen, so the released game stays assigned.
|
|
assert state.current_app_id == 1
|
|
|
|
def test_no_next_game(self) -> None:
|
|
config = Config()
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
snap = [_snap(1, "G", 10, 10)]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game") as mock_pick,
|
|
patch.object(State, "save"),
|
|
):
|
|
|
|
def set_none(
|
|
_games: object,
|
|
s: State,
|
|
_c: object,
|
|
**_kwargs: object,
|
|
) -> None:
|
|
s.current_app_id = None
|
|
|
|
mock_pick.side_effect = set_none
|
|
_finalize_completion(config, state, done_game(1, "G"))
|
|
|
|
def test_no_owned_ids(self) -> None:
|
|
config = Config()
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
snap = [_snap(2, "Next", 10, 0)]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game") as mock_pick,
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.get_all_owned_app_ids",
|
|
return_value=[],
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.send_notification"),
|
|
patch.object(State, "save"),
|
|
):
|
|
|
|
def set_2(
|
|
_games: object,
|
|
s: State,
|
|
_c: object,
|
|
**_kwargs: object,
|
|
) -> None:
|
|
s.current_app_id = 2
|
|
s.current_game_name = "Next"
|
|
|
|
mock_pick.side_effect = set_2
|
|
_finalize_completion(config, state, done_game(1, "G"))
|
|
|
|
def test_hide_returns_zero(self) -> None:
|
|
config = Config()
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
snap = [_snap(2, "Next", 10, 0)]
|
|
with (
|
|
patch(f"{CMD_DONE_PKG}._echo"),
|
|
patch(f"{CMD_DONE_PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{CMD_DONE_PKG}.pick_next_game") as mock_pick,
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.get_all_owned_app_ids",
|
|
return_value=[1, 2],
|
|
),
|
|
patch(
|
|
"steam_backlog_enforcer.library_hider.try_hide_other_games",
|
|
return_value=(0, None),
|
|
),
|
|
patch(f"{CMD_DONE_PKG}.send_notification"),
|
|
patch.object(State, "save"),
|
|
):
|
|
|
|
def set_2(
|
|
_games: object,
|
|
s: State,
|
|
_c: object,
|
|
**_kwargs: object,
|
|
) -> None:
|
|
s.current_app_id = 2
|
|
s.current_game_name = "Next"
|
|
|
|
mock_pick.side_effect = set_2
|
|
_finalize_completion(config, state, done_game(1, "G"))
|