diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 661b469..669f15d 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.6.1#subdirectory=earned_time + - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.7.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.6.1#subdirectory=earned_time + - earned-time @ git+https://github.com/kuhyx/utils@earned-time-v0.7.0#subdirectory=earned_time - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays - mcp>=2,<3 - pydantic diff --git a/screen_locker/_gate_bonus.py b/screen_locker/_gate_bonus.py index 4c1e8b5..1164716 100644 --- a/screen_locker/_gate_bonus.py +++ b/screen_locker/_gate_bonus.py @@ -5,7 +5,7 @@ A flat earner (LeetCode, reading) pays once a day, stamped signed ledger row per verified 15-minute block -- pays per unit, so its stamp is ``_bonus_units``: the day, and how many units the config already holds. The live pass pays only the difference, priced by -``Earner.shutdown_for`` so the uneven rung split (13/13/12/12) and the +``Earner.shutdown_for`` so the rung's per-unit split (15 per tutor block) and the ``max_units`` cap come from the registry, never from here. The workout is counted too, but screen-locker owns its log and diff --git a/screen_locker/tests/test_gate_bonus.py b/screen_locker/tests/test_gate_bonus.py index 5b06a5e..bb208fb 100644 --- a/screen_locker/tests/test_gate_bonus.py +++ b/screen_locker/tests/test_gate_bonus.py @@ -210,8 +210,8 @@ class TestThroughTheShutdownBase: @pytest.mark.usefixtures("key") -def test_the_real_tutor_rung_pays_13_13_12_12_then_nothing() -> None: - """The registry's uneven split, through this pass, on the cutover day.""" +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)") @@ -233,5 +233,5 @@ def test_the_real_tutor_rung_pays_13_13_12_12_then_nothing() -> None: ) write_ledger(tutor, rows) apply_counted_bonus(state, adjust, tutor, cutover) - assert [c.args[0] for c in adjust.call_args_list] == [13, 13, 12, 12] + 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_shutdown_reading.py b/screen_locker/tests/test_shutdown_reading.py index 35e0f56..6888450 100644 --- a/screen_locker/tests/test_shutdown_reading.py +++ b/screen_locker/tests/test_shutdown_reading.py @@ -120,9 +120,10 @@ class TestResetIncludesReading: apply_flat_bonuses_if_new(state, mixin) mixin._adjust_shutdown_time_by.assert_not_called() - def test_reset_is_still_capped_at_the_ceiling( + def test_reset_without_the_tutor_stops_an_hour_below_the_ceiling( self, tmp_path: Path, answers: tuple[dict[str, bool | None], MagicMock] ) -> None: + """Nine workouts still pay one rung; the confirmed tutor's 60 is unpaid.""" answers[0]["leetcode"] = True answers[0]["reading"] = True mixin = _mixin() @@ -131,7 +132,7 @@ class TestResetIncludesReading: tmp_path / "state.json", mixin, log_file=tmp_path / "log.json" ) mixin._write_shutdown_config.assert_called_once_with( - 1380, 1380, 300, restore=True + 1320, 1320, 300, restore=True ) def test_unknown_reading_earns_nothing_warns_and_is_not_stamped( diff --git a/screen_locker/tests/test_tutor_cutover.py b/screen_locker/tests/test_tutor_cutover.py index 52fc0ec..23d5765 100644 --- a/screen_locker/tests/test_tutor_cutover.py +++ b/screen_locker/tests/test_tutor_cutover.py @@ -76,42 +76,39 @@ def test_the_simulated_cutover_registers_the_tutor(tutor: Any) -> None: @pytest.mark.usefixtures("tutor") -class TestNeverCredited: - """A tutor that never paid out is not penalised once first_credits is wired.""" +class TestConfirmedButNeverCredited: + """earned_time 0.7.0: a confirmed tutor is penalised from its first day.""" def test_the_map_says_never(self) -> None: starts = first_credits(registry(_CUTOVER), _CUTOVER) assert starts is not None assert starts["automation"] is None - def test_no_tutor_penalty_on_the_gaming_base(self) -> None: + def test_the_tutor_penalty_is_on_the_gaming_base_unpaid(self) -> None: answers = {e.name: 0 for e in registry(_CUTOVER)} starts = first_credits(registry(_CUTOVER), _CUTOVER) wired = derive(DayInputs(_CUTOVER, answers, first_credits=starts)) unwired = derive(DayInputs(_CUTOVER, answers)) - cut = earned_time.AUTOMATION_TUTOR.max_gaming_minutes assert wired.resolution.base.gaming_minutes == ( - unwired.resolution.base.gaming_minutes + cut + unwired.resolution.base.gaming_minutes ) - def test_no_tutor_rung_off_the_shutdown_floor(self) -> None: - """earned_time 0.6.1: a gate whose penalty has not started spares the floor. + def test_the_whole_tutor_rung_comes_off_the_shutdown_floor(self) -> None: + """Confirmed on 2026-10-09, so a never-paid tutor still costs its rung. - Unwired, the tutor's penalty starts at ``penalty_from`` and its full - rung (13+13+12+12) comes off the ladder floor; wired, the never-paid - tutor costs no shutdown either, so the floor stays at 19:50. + Its full rung (4 blocks of 15) comes off the ladder floor whether or + not ``first_credits`` is wired, leaving 18:50. """ answers = {e.name: 0 for e in registry(_CUTOVER)} starts = first_credits(registry(_CUTOVER), _CUTOVER) wired = derive(DayInputs(_CUTOVER, answers, first_credits=starts)) unwired = derive(DayInputs(_CUTOVER, answers)) tutor = earned_time.AUTOMATION_TUTOR - rung = tutor.shutdown_for(tutor.max_units or 1, _CUTOVER) - assert rung == 50 + assert tutor.shutdown_for(tutor.max_units or 1, _CUTOVER) == 60 assert wired.resolution.base.shutdown_minutes == ( - unwired.resolution.base.shutdown_minutes + rung + unwired.resolution.base.shutdown_minutes ) - assert wired.resolution.base.shutdown_minutes == 19 * 60 + 50 + assert wired.resolution.base.shutdown_minutes == 18 * 60 + 50 @pytest.mark.usefixtures("tutor")