mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 21:33:49 +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
164 lines
4.5 KiB
Python
164 lines
4.5 KiB
Python
"""Shared fixtures and builders for the ``main`` CLI test modules.
|
|
|
|
Lives outside a ``test_*.py`` name on purpose: these are constructors, not
|
|
tests. ``name-tests-test`` exempts ``tests/_*.py`` for exactly this.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from typing import Any
|
|
|
|
from steam_backlog_enforcer._allowed_games import MANUAL_LOCK_DAYS
|
|
from steam_backlog_enforcer._total_block import TotalBlockStatus
|
|
from steam_backlog_enforcer.config import State
|
|
from steam_backlog_enforcer.steam_api import GameInfo
|
|
|
|
STARTED_AT = (datetime.now(UTC) - timedelta(days=1)).isoformat()
|
|
|
|
# A start timestamp that is always past the 14-day deadline.
|
|
EXPIRED_AT = (datetime.now(UTC) - timedelta(days=MANUAL_LOCK_DAYS + 1)).isoformat()
|
|
|
|
ACTIVE_STATUS = TotalBlockStatus(
|
|
active=True,
|
|
started_at=datetime.now(UTC) - timedelta(hours=1),
|
|
until=datetime.now(UTC) + timedelta(hours=23),
|
|
days=1,
|
|
days_remaining=0.96,
|
|
)
|
|
INACTIVE_STATUS = TotalBlockStatus(
|
|
active=False, started_at=None, until=None, days=0, days_remaining=0.0
|
|
)
|
|
|
|
|
|
def snap(
|
|
app_id: int = 1,
|
|
name: str = "G",
|
|
total: int = 10,
|
|
unlocked: int = 0,
|
|
hours: float = -1,
|
|
) -> dict[str, Any]:
|
|
"""Build one snapshot entry.
|
|
|
|
Args:
|
|
app_id: Steam application id.
|
|
name: Game name.
|
|
total: Total achievement count.
|
|
unlocked: Unlocked achievement count.
|
|
hours: Completionist hours (-1 when unknown).
|
|
|
|
Returns:
|
|
A snapshot dict shaped like the on-disk format.
|
|
"""
|
|
return {
|
|
"app_id": app_id,
|
|
"name": name,
|
|
"total_achievements": total,
|
|
"unlocked_achievements": unlocked,
|
|
"playtime_minutes": 60,
|
|
"completionist_hours": hours,
|
|
}
|
|
|
|
|
|
def snap_with_achievements(
|
|
app_id: int = 1,
|
|
name: str = "G",
|
|
total: int = 10,
|
|
unlocked: int = 0,
|
|
hours: float = -1,
|
|
) -> dict[str, Any]:
|
|
"""Build a snapshot entry carrying an empty ``achievements`` list.
|
|
|
|
Args:
|
|
app_id: Steam application id.
|
|
name: Game name.
|
|
total: Total achievement count.
|
|
unlocked: Unlocked achievement count.
|
|
hours: Completionist hours (-1 when unknown).
|
|
|
|
Returns:
|
|
A snapshot dict with ``achievements`` and zero playtime.
|
|
"""
|
|
entry = snap(app_id, name, total, unlocked, hours)
|
|
entry["playtime_minutes"] = 0
|
|
entry["achievements"] = []
|
|
return entry
|
|
|
|
|
|
def locked_state(
|
|
app_id: int = 100,
|
|
name: str = "TestGame",
|
|
started_at: str = STARTED_AT,
|
|
) -> State:
|
|
"""Build a State carrying one active manual pick.
|
|
|
|
Args:
|
|
app_id: Picked application id.
|
|
name: Picked game name.
|
|
started_at: ISO timestamp the pick started at.
|
|
|
|
Returns:
|
|
A State with a single manual pick.
|
|
"""
|
|
return State(
|
|
manual_picks=[{"app_id": app_id, "game_name": name, "started_at": started_at}],
|
|
)
|
|
|
|
|
|
RECENT_PICK = (datetime.now(UTC) - timedelta(days=1)).isoformat()
|
|
OLD_PICK = (datetime.now(UTC) - timedelta(days=8)).isoformat()
|
|
|
|
VALID_REASON = "I need this game installed for a work presentation this week."
|
|
|
|
|
|
def abandonable_state(app_id: int = 100, started_at: str = RECENT_PICK) -> State:
|
|
"""Build a State whose single manual pick is still abandonable.
|
|
|
|
Args:
|
|
app_id: Picked application id.
|
|
started_at: ISO timestamp the pick started at.
|
|
|
|
Returns:
|
|
A State with the pick also set as the current assignment.
|
|
"""
|
|
state = locked_state(app_id=app_id, started_at=started_at)
|
|
state.current_app_id = app_id
|
|
state.current_game_name = state.manual_pick_game_name
|
|
return state
|
|
|
|
|
|
def two_pick_state() -> State:
|
|
"""Build a State carrying two active manual picks.
|
|
|
|
Returns:
|
|
A State whose current assignment is the second pick.
|
|
"""
|
|
state = State(
|
|
manual_picks=[
|
|
{"app_id": 100, "game_name": "TestGame", "started_at": RECENT_PICK},
|
|
{"app_id": 200, "game_name": "SecondGame", "started_at": RECENT_PICK},
|
|
],
|
|
)
|
|
state.current_app_id = 200
|
|
state.current_game_name = "SecondGame"
|
|
return state
|
|
|
|
|
|
def done_game(app_id: int = 1, name: str = "G") -> GameInfo:
|
|
"""Build a 100%-complete GameInfo, as ``done`` sees a finished game.
|
|
|
|
Args:
|
|
app_id: Steam application id.
|
|
name: Game name.
|
|
|
|
Returns:
|
|
A GameInfo with every achievement unlocked.
|
|
"""
|
|
return GameInfo(
|
|
app_id=app_id,
|
|
name=name,
|
|
total_achievements=10,
|
|
unlocked_achievements=10,
|
|
playtime_minutes=60,
|
|
)
|