tests: earned_time 0.7.0 tutor policy (confirmed, 15 min per block); hook envs on 0.7.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 }}) (flutter, flutter-analyze, ) (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,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
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 15:15:35 +02:00
parent 41ce020e10
commit 384ffcda7d
5 changed files with 20 additions and 22 deletions

View File

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

View File

@ -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 ``<name>_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

View File

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

View File

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

View File

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