diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 669f15d..d1b76b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -134,7 +134,7 @@ repos: # than merely an old one. - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync - - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.7.0#subdirectory=earned_time + - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.8.0#subdirectory=earned_time - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays - mcp>=2,<3 - pydantic @@ -155,7 +155,7 @@ repos: # than merely an old one. - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync - - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.7.0#subdirectory=earned_time + - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.8.0#subdirectory=earned_time - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays - mcp>=2,<3 - pydantic diff --git a/pyproject.toml b/pyproject.toml index a831f87..9eb1b79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync", "freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays", - "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.7.0#subdirectory=earned_time", + "earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.8.0#subdirectory=earned_time", ] [project.scripts] diff --git a/requirements.txt b/requirements.txt index c4fc762..5d1b272 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ bandit==1.9.4 codespell==2.4.3 coverage==7.16.2 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync -earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.7.0#subdirectory=earned_time +earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.8.0#subdirectory=earned_time freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock mcp==2.3.0 diff --git a/screen_locker/tests/test_gate_bonus.py b/screen_locker/tests/test_gate_bonus.py index bb208fb..6668674 100644 --- a/screen_locker/tests/test_gate_bonus.py +++ b/screen_locker/tests/test_gate_bonus.py @@ -7,7 +7,7 @@ the tutor-cutover one (``earners_for``) read, so these hold on either. from __future__ import annotations -from datetime import date, datetime, time +from datetime import date import json import logging from typing import TYPE_CHECKING, Any @@ -207,31 +207,3 @@ class TestThroughTheShutdownBase: apply_flat_bonuses_if_new(state, mixin) mixin._adjust_shutdown_time_by.assert_not_called() assert not state.exists() - - -@pytest.mark.usefixtures("key") -def test_the_real_tutor_rung_pays_15_per_block_then_nothing() -> None: - """The registry's per-block rung (earned_time 0.7.0), through this pass.""" - tutor = getattr(earned_time, "AUTOMATION_TUTOR", None) - if tutor is None: - pytest.skip("installed earned_time predates the tutor (< 0.5)") - cutover = earned_time.TUTOR_FROM.isoformat() - state: dict[str, Any] = {} - adjust = MagicMock(return_value=True) - rows: list[dict[str, Any]] = [] - for number in range(1, 6): - ended = datetime.combine(earned_time.TUTOR_FROM, time(18, number)).astimezone() - rows.append( - signed( - { - "kind": "credit", - "entry_id": f"s-b{number}", - "day": cutover, - "detail": {"block": number, "ended_at": ended.timestamp()}, - } - ) - ) - write_ledger(tutor, rows) - apply_counted_bonus(state, adjust, tutor, cutover) - assert [c.args[0] for c in adjust.call_args_list] == [15, 15, 15, 15] - assert state["automation_bonus_units"] == {"date": cutover, "units": 5} diff --git a/screen_locker/tests/test_gate_bonus_tutor.py b/screen_locker/tests/test_gate_bonus_tutor.py new file mode 100644 index 0000000..c45a5f5 --- /dev/null +++ b/screen_locker/tests/test_gate_bonus_tutor.py @@ -0,0 +1,74 @@ +"""The real registry's tutor rung through the live pass (earned_time 0.8.0). + +Units are credited minutes: a ``detail.minutes`` row pays its minutes, a legacy +block row (no ``detail.minutes``) pays 15, and the rung stops paying at 60. +""" + +from __future__ import annotations + +from datetime import datetime, time +from typing import TYPE_CHECKING, Any +from unittest.mock import MagicMock + +import earned_time +import pytest + +from screen_locker._gate_bonus import apply_counted_bonus +from screen_locker.tests._earned_fixtures import signed, signing_key, write_ledger + +if TYPE_CHECKING: + from collections.abc import Iterator + from pathlib import Path + + +@pytest.fixture +def key(tmp_path: Path) -> Iterator[Path]: + with signing_key(tmp_path) as path: + yield path + + +@pytest.mark.usefixtures("key") +def test_the_real_tutor_rung_pays_legacy_blocks_as_15_minutes_up_to_60() -> None: + """Legacy block rows (no ``detail.minutes``) pay 15 minutes each (0.8.0).""" + tutor = getattr(earned_time, "AUTOMATION_TUTOR", None) + if tutor is None: + pytest.skip("installed earned_time predates the tutor (< 0.5)") + cutover = earned_time.TUTOR_FROM.isoformat() + state: dict[str, Any] = {} + adjust = MagicMock(return_value=True) + rows: list[dict[str, Any]] = [] + for number in range(1, 6): + ended = datetime.combine(earned_time.TUTOR_FROM, time(18, number)).astimezone() + rows.append( + signed( + { + "kind": "credit", + "entry_id": f"s-b{number}", + "day": cutover, + "detail": {"block": number, "ended_at": ended.timestamp()}, + } + ) + ) + write_ledger(tutor, rows) + apply_counted_bonus(state, adjust, tutor, cutover) + assert [c.args[0] for c in adjust.call_args_list] == [15, 15, 15, 15] + # Units are minutes now: five block rows count 75; the cap stops the pay. + assert state["automation_bonus_units"] == {"date": cutover, "units": 75} + + +@pytest.mark.usefixtures("key") +def test_the_real_tutor_rung_pays_each_credited_minute() -> None: + """A per-minute row (``detail.minutes``) pays exactly its minutes.""" + tutor = getattr(earned_time, "AUTOMATION_TUTOR", None) + if tutor is None: + pytest.skip("installed earned_time predates the tutor (< 0.5)") + cutover = earned_time.TUTOR_FROM.isoformat() + state: dict[str, Any] = {} + adjust = MagicMock(return_value=True) + ended = datetime.combine(earned_time.TUTOR_FROM, time(18, 7)).astimezone() + detail = {"block": 7, "minutes": 7, "ended_at": ended.timestamp()} + row = {"kind": "credit", "entry_id": "s-m7", "day": cutover, "detail": detail} + write_ledger(tutor, [signed(row)]) + apply_counted_bonus(state, adjust, tutor, cutover) + adjust.assert_called_once_with(7) + assert state["automation_bonus_units"] == {"date": cutover, "units": 7}