buy-dlc: fixed-length phrase the daemon accepts (was an unfillable {minutes} slot)
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-10-10 16:11:32 +02:00
parent 341bc784ac
commit e4c61442c2
2 changed files with 40 additions and 3 deletions

View File

@ -14,6 +14,8 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Final
from steam_backlog_enforcer._store_window import DEFAULT_WINDOW_MINUTES
@dataclass(frozen=True)
class Friction:
@ -30,13 +32,19 @@ class Friction:
GAMING_RESET_COUNTDOWN_SECONDS: Final = 300
_STORE_WINDOW_PHRASE: Final = "unblock the store for {minutes} minutes"
FRICTION: Final[dict[str, Friction]] = {
"gaming-reset": Friction(
"reset today's gaming budget", GAMING_RESET_COUNTDOWN_SECONDS
),
"block-gaming": Friction("block all gaming for {days} days"),
"unblock": Friction("unblock the store for {minutes} minutes"),
"buy-dlc": Friction("unblock the store for {minutes} minutes"),
"unblock": Friction(_STORE_WINDOW_PHRASE),
# buy-dlc takes no params: it is always the default-length window, and the
# daemon checks it as an ``unblock`` of that length. So its phrase is that
# exact sentence with the number already in it, not a ``{minutes}`` slot
# that nothing could fill.
"buy-dlc": Friction(_STORE_WINDOW_PHRASE.format(minutes=DEFAULT_WINDOW_MINUTES)),
"gaming-unblock": Friction("force release playtime mounts"),
"abandon-pick": Friction("abandon {game_name}"),
"pick-manual": Friction("lock in {game_name}"),

View File

@ -2,10 +2,14 @@
from __future__ import annotations
from string import Formatter
from unittest.mock import patch
from steam_backlog_enforcer import _command_catalog, _command_locks
from steam_backlog_enforcer._friction import Friction
from steam_backlog_enforcer._command_params import PARAMS
from steam_backlog_enforcer._ctl_context import require_phrase
from steam_backlog_enforcer._friction import FRICTION, Friction, expected_phrase
from steam_backlog_enforcer._web_privileged import BUY_DLC_MINUTES
from steam_backlog_enforcer.config import Config, State
from steam_backlog_enforcer.jobs._specs import JobFlags
from steam_backlog_enforcer.main import _shared
@ -115,3 +119,28 @@ class TestCommandLocks:
_command_locks._MANUAL_LOCK_EXEMPT_COMMANDS
)
assert "gaming-reset" not in _command_locks._TOTAL_BLOCK_EXEMPT_COMMANDS
class TestPhrasesAreFillable:
"""Every phrase the UI shows can be completed and is the one checked."""
def test_every_template_field_has_a_source(self) -> None:
# Mirrors fillPhrase in web/src/commands/catalog.ts: a field is a
# param, ``game_name`` from an ``app_id`` param, or ``count`` from
# the installed list. Anything else leaves Run disabled forever.
# The same two tables ``_spec`` builds each CommandSpec from.
for name in _command_catalog.CATALOG:
friction = FRICTION.get(name)
if friction is None:
continue
params = {p.name for p in PARAMS.get(name, ())}
derived = {"count"} | ({"game_name"} if "app_id" in params else set())
template = friction.phrase_template
fields = {f for _, f, _, _ in Formatter().parse(template) if f}
assert fields <= params | derived, name
def test_buy_dlc_phrase_is_the_unblock_the_daemon_checks(self) -> None:
shown = expected_phrase("buy-dlc")
assert shown == expected_phrase("unblock", minutes=BUY_DLC_MINUTES)
assert shown is not None
require_phrase("unblock", shown, minutes=BUY_DLC_MINUTES)