diff --git a/pyproject.toml b/pyproject.toml index 6f7948e..7dbb59b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,10 @@ dependencies = [ # transitively via howlongtobeatpy and websockets was undeclared — both # are declared here so a clean install imports the package. "aiohttp==3.14.5", - "websockets==17.2", + "websockets==17.3", # The earner registry (gaming budget terms); stdlib-only, so the root # daemon imports it from the desktop user's site-packages. - "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.16.0#subdirectory=earned_time", + "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.17.0#subdirectory=earned_time", # The shared /proc game scan and the signed leisure facts (shadow budget, # contract C4); stdlib-only, imported by root from the user site like # earned-time. diff --git a/requirements.txt b/requirements.txt index 1206f42..409393b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,8 +4,8 @@ aiohttp==3.14.5 bandit==1.9.4 codespell==2.4.3 -coverage==7.16.2 -earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.16.0#subdirectory=earned_time +coverage==7.17.0 +earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.17.0#subdirectory=earned_time howlongtobeatpy==1.0.24 leisure-watch @ git+https://github.com/kuhyx/leisure-watch@leisure-watch-v0.2.1 mcp==2.3.0 @@ -19,6 +19,6 @@ pytest-sugar==1.1.1 pytest-timeout==2.4.0 pytest-xdist==3.8.0 requests==2.34.2 -ruff==0.17.1 +ruff==0.17.2 types-requests==2.33.0.20261006 -websockets==17.2 +websockets==17.3 diff --git a/steam_backlog_enforcer/_leetcode_bonus.py b/steam_backlog_enforcer/_leetcode_bonus.py index e65d1b3..af5c8e6 100644 --- a/steam_backlog_enforcer/_leetcode_bonus.py +++ b/steam_backlog_enforcer/_leetcode_bonus.py @@ -20,10 +20,9 @@ rather than a permission problem -- this daemon runs as root, so it already has strictly more access than the endpoint does. Both carry the same staleness (see below); the fallback is not a fresher source. -**Known staleness, accepted.** leetcode-guard only writes credits from its lock -window's poll loop, so a *voluntary extra* solve made after the day is settled -never reaches the ledger and earns nothing. The bonus tracks what the day's -gate run recorded. +**Staleness.** leetcode-guard's one-minute harvester credits a solve within +about a minute, even with no lock window up. A day paid ahead from banked +credits counts like a solve (``earned_time`` >= 0.17.0, kuhy 2026-10-23). Reading and verifying the ledger itself lives in :mod:`steam_backlog_enforcer._leetcode_ledger`; this module owns the two @@ -60,7 +59,8 @@ def _fetch_leetcode_today(url: str) -> bool: url: The status endpoint, e.g. ``http://127.0.0.1:8771/api/status``. Returns: - Whether an accepted submission is recorded for today. + Whether an accepted submission is recorded for today, or today was + paid from banked credits. Raises: OSError: The endpoint could not be reached. @@ -73,7 +73,9 @@ def _fetch_leetcode_today(url: str) -> bool: # The server answered, but said it could not look. That is not a "no". msg = f"leetcode-guard could not check: {block.get('reason', 'no reason')}" raise ValueError(msg) - return bool(block["solved_today"]) + # A day paid ahead from banked credits earns the hour like a solve + # (leetcode-guard >= 2026-10-23); an older server has no such field. + return bool(block["solved_today"]) or bool(block.get("settled_by_credit")) def leetcode_solved_today(config: Config) -> bool | None: diff --git a/steam_backlog_enforcer/tests/test_leetcode_bonus.py b/steam_backlog_enforcer/tests/test_leetcode_bonus.py index 8a33bdf..c618dac 100644 --- a/steam_backlog_enforcer/tests/test_leetcode_bonus.py +++ b/steam_backlog_enforcer/tests/test_leetcode_bonus.py @@ -140,3 +140,24 @@ class TestFetchLeetcodeToday: ) is True ) + + @pytest.mark.parametrize( + ("block", "expected"), + [ + ({"solved_today": False, "settled_by_credit": True}, True), + ({"solved_today": False, "settled_by_credit": False}, False), + # A leetcode-guard older than the field: the solve alone decides. + ({"solved_today": False}, False), + ], + ) + def test_a_day_paid_from_banked_credit_counts( + self, block: dict[str, bool], *, expected: bool + ) -> None: + """Working ahead earns the hour (kuhy, 2026-10-23).""" + payload = json.dumps({"leetcode": {"checked": True, **block}}).encode() + with patch(f"{_API}.http.client.HTTPConnection") as conn: + resp = conn.return_value.getresponse.return_value + resp.status = 200 + resp.read.return_value = payload + url = "http://127.0.0.1:8771/api/status" + assert _leetcode_bonus._fetch_leetcode_today(url) is expected