screen-locker/screen_locker/tests/test_tutor_cutover.py
Krzysztof kuhy Rudnicki 96ad163ee6
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
earned_time 0.9.0: tutor earner renamed automation -> tutor
Pins bumped at all four sites; tests follow TUTOR / tutor_bonus_units.
pylint 4.1.3 (freshness gate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YADHKXzZhhePzQ2rNoXFcK
2026-10-12 16:38:25 +02:00

125 lines
4.6 KiB
Python

"""The Automation tutor on a simulated cutover: penalty start and grace floor.
``earned_time.TUTOR_FROM`` is 2026-10-10 since earned_time 0.6.1; the
fixture still pins the registry's cutover (bound by value in ``_registry`` and
``_ladder``) and the tutor's own ``penalty_from`` to :data:`_CUTOVER`, so the
suite does not move if a later release moves the cutover. Nothing is written
outside the suite's redirected home.
"""
from __future__ import annotations
from dataclasses import replace
from datetime import date, datetime, time
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
import earned_time
import pytest
from screen_locker import _earned
from screen_locker._earned import first_credits, registry
from screen_locker._grace_floor import first_done_at
from screen_locker._shutdown_target import DayInputs, derive
from screen_locker.tests._earned_fixtures import signed, signing_key, write_ledger
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
_CUTOVER = date(2026, 10, 10)
@pytest.fixture
def tutor(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[Any]:
"""The tutor registry from ``_CUTOVER``, its ledger signed, today pinned.
Args:
monkeypatch: Moves the cutover.
tmp_path: Where the key is written.
Yields:
The tutor earner, penalised from ``_CUTOVER``.
"""
item = replace(earned_time.TUTOR, penalty_from=_CUTOVER)
swapped = tuple(
item if e is earned_time.TUTOR else e for e in earned_time.TUTOR_EARNERS
)
monkeypatch.setattr(earned_time, "TUTOR_EARNERS", swapped)
monkeypatch.setattr("earned_time._registry.TUTOR_FROM", _CUTOVER)
monkeypatch.setattr("earned_time._ladder.TUTOR_FROM", _CUTOVER)
with (
signing_key(tmp_path),
patch.object(_earned, "today_str", return_value=_CUTOVER.isoformat()),
):
yield item
def _block(hhmm: str) -> dict[str, Any]:
hours, minutes = map(int, hhmm.split(":"))
ended = datetime.combine(_CUTOVER, time(hours, minutes)).astimezone()
return signed(
{
"kind": "credit",
"entry_id": f"s-b{hhmm}",
"day": _CUTOVER.isoformat(),
"detail": {"block": 1, "ended_at": ended.timestamp()},
}
)
def test_the_simulated_cutover_registers_the_tutor(tutor: Any) -> None:
assert tutor in registry(_CUTOVER)
assert tutor in _earned.gate_earners(_CUTOVER)
assert tutor not in _earned.flat_earners(_CUTOVER)
@pytest.mark.usefixtures("tutor")
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["tutor"] is 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))
assert wired.resolution.base.gaming_minutes == (
unwired.resolution.base.gaming_minutes
)
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.
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.TUTOR
assert tutor.shutdown_for(tutor.max_units or 1, _CUTOVER) == 60
assert wired.resolution.base.shutdown_minutes == (
unwired.resolution.base.shutdown_minutes
)
assert wired.resolution.base.shutdown_minutes == 18 * 60 + 50
@pytest.mark.usefixtures("tutor")
class TestTheGraceFloor:
"""A tutor block is a first task like any other."""
def test_a_tutor_block_starts_the_floor(self, tutor: Any) -> None:
write_ledger(tutor, [_block("18:55"), _block("19:10")])
expected = datetime.combine(_CUTOVER, time(18, 55)).astimezone()
assert first_done_at(_CUTOVER, None) == expected.timestamp()
def test_no_block_no_floor(self, tutor: Any) -> None:
write_ledger(tutor, [])
assert first_done_at(_CUTOVER, None) is None