diff --git a/docs/superpowers/evidence/steam-backlog-done-install-retry-2026-05-08.json b/docs/superpowers/evidence/steam-backlog-done-install-retry-2026-05-08.json new file mode 100644 index 0000000..2d76366 --- /dev/null +++ b/docs/superpowers/evidence/steam-backlog-done-install-retry-2026-05-08.json @@ -0,0 +1,53 @@ +{ + "intent": "Ensure the next assigned Steam game remains install-triggered even if library reconciliation restarts Steam.", + "scope": [ + "python_pkg/steam_backlog_enforcer/_cmd_done.py", + "python_pkg/steam_backlog_enforcer/tests/test_main_part2.py", + "Non-goal: modify runtime behavior outside done/finalize assignment flow" + ], + "changes": [ + "In _finalize_completion, after hide_other_games, re-check is_game_installed for the assigned game and re-trigger install_game(..., use_steam_protocol=True) when still missing.", + "Add user-facing echo and logger.info diagnostics for the post-hide install retry path.", + "Add regression test test_retriggers_install_after_library_hide_if_still_missing to lock behavior." + ], + "verification": [ + { + "command": "runTests: test_main_part2.py::test_retriggers_install_after_library_hide_if_still_missing", + "result": "pass", + "evidence": "1 passed, 0 failed" + }, + { + "command": "runTests: test_main_part2.py::test_skips_install_retry_when_assigned_game_already_installed", + "result": "pass", + "evidence": "1 passed, 0 failed" + }, + { + "command": "runTests: test_main_part2.py targeted tests", + "result": "pass", + "evidence": "2 passed, 0 failed" + }, + { + "command": "runTests: python_pkg/steam_backlog_enforcer/tests", + "result": "pass", + "evidence": "516 passed, 0 failed" + }, + { + "command": "pre-commit run --files python_pkg/steam_backlog_enforcer/_cmd_done.py python_pkg/steam_backlog_enforcer/tests/test_main_part2.py", + "result": "pass", + "evidence": "all hooks passed" + }, + { + "command": "pre-commit run --files python_pkg/steam_backlog_enforcer/_cmd_done.py", + "result": "pass", + "evidence": "all hooks passed" + } + ], + "risks": [ + "Repeated install trigger logs may appear when Steam metadata lags briefly.", + "No functional impact expected if game is already installed because branch is guarded by is_game_installed check." + ], + "rollback": [ + "git revert ", + "Re-run targeted test and done-flow tests to confirm prior behavior restored" + ] +} diff --git a/python_pkg/steam_backlog_enforcer/_cmd_done.py b/python_pkg/steam_backlog_enforcer/_cmd_done.py index 4631865..75e55e8 100644 --- a/python_pkg/steam_backlog_enforcer/_cmd_done.py +++ b/python_pkg/steam_backlog_enforcer/_cmd_done.py @@ -2,6 +2,8 @@ from __future__ import annotations +import logging + from python_pkg.steam_backlog_enforcer._enforce_loop import get_all_owned_app_ids from python_pkg.steam_backlog_enforcer.config import Config, State, load_snapshot from python_pkg.steam_backlog_enforcer.enforcer import ( @@ -32,6 +34,7 @@ from python_pkg.steam_backlog_enforcer.scanning import ( from python_pkg.steam_backlog_enforcer.steam_api import GameInfo, SteamAPIClient _REASSIGN_REFRESH_LIMIT = 50 +logger = logging.getLogger(__name__) def _backfill_polls_for_finished( @@ -308,6 +311,23 @@ def _finalize_completion( if hidden > 0: _echo(f"\n Library: hid {hidden} games") + if not is_game_installed(state.current_app_id): + logger.info( + "Assigned game still missing after library reconciliation; " + "re-triggering install" + ) + _echo( + "\n Assigned game still missing after library reconciliation; " + "re-triggering install..." + ) + _echo(f"\n Auto-installing {state.current_game_name}...") + install_game( + state.current_app_id, + state.current_game_name, + config.steam_id, + use_steam_protocol=True, + ) + send_notification( "Game Complete!", f"Finished {game_name}! Now playing: {state.current_game_name}", diff --git a/python_pkg/steam_backlog_enforcer/tests/test_main_part2.py b/python_pkg/steam_backlog_enforcer/tests/test_main_part2.py index a91c03c..f6b4b67 100644 --- a/python_pkg/steam_backlog_enforcer/tests/test_main_part2.py +++ b/python_pkg/steam_backlog_enforcer/tests/test_main_part2.py @@ -185,6 +185,64 @@ class TestFinalizeCompletion: assert seen[3] == 18.81 mock_fetch_hltb.assert_called_once_with([(3, "Lacuna")]) + def test_retriggers_install_after_library_hide_if_still_missing(self) -> None: + """Re-trigger install after hide step in case Steam restart drops it.""" + config = Config(steam_id="sid") + state = State(current_app_id=1, current_game_name="DoneGame") + snap = [_snap(2, "Next", 10, 0, 5.0)] + + def set_next( + _games: object, + s: State, + _c: object, + ) -> None: + s.current_app_id = 2 + s.current_game_name = "Next" + + 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", side_effect=set_next), + patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[1, 2]), + patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=1), + patch(f"{CMD_DONE_PKG}.is_game_installed", return_value=False), + patch(f"{CMD_DONE_PKG}.install_game") as mock_install, + patch(f"{CMD_DONE_PKG}.send_notification"), + patch.object(State, "save"), + ): + _finalize_completion(config, state, "DoneGame", 1) + + mock_install.assert_called_once_with(2, "Next", "sid", use_steam_protocol=True) + + def test_skips_install_retry_when_assigned_game_already_installed(self) -> None: + """Do not re-trigger install when assigned game is already present.""" + config = Config(steam_id="sid") + state = State(current_app_id=1, current_game_name="DoneGame") + snap = [_snap(2, "Next", 10, 0, 5.0)] + + def set_next( + _games: object, + s: State, + _c: object, + ) -> None: + s.current_app_id = 2 + s.current_game_name = "Next" + + 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", side_effect=set_next), + patch(f"{CMD_DONE_PKG}.get_all_owned_app_ids", return_value=[1, 2]), + patch(f"{CMD_DONE_PKG}.hide_other_games", return_value=1), + patch(f"{CMD_DONE_PKG}.is_game_installed", return_value=True), + patch(f"{CMD_DONE_PKG}.install_game") as mock_install, + patch(f"{CMD_DONE_PKG}.send_notification"), + patch.object(State, "save"), + ): + _finalize_completion(config, state, "DoneGame", 1) + + mock_install.assert_not_called() + class TestEnforceOnDone: """Tests for _enforce_on_done."""