LeetCode bonus: a day paid from banked credits counts (earned_time 0.17.0)
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-23 11:33:39 +02:00
parent d2c4ea7ace
commit 55cf19fae4
4 changed files with 35 additions and 12 deletions

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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