screen-locker/screen_locker/tests/test_late_credit.py
Krzysztof kuhy Rudnicki aa3e290391 Document every test, split files at the cap
Real one-line docstrings stating what each test pins down. Files that
would pass 250 lines moved whole classes into _part2 siblings
(sick_panel, earned, poke_server, shutdown_reading,
manual_workout_dialog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
2026-11-01 21:11:34 +01:00

191 lines
7.6 KiB
Python

"""A credit that lands after tonight's shutdown restarts the clock from it.
kuhy, 2026-10-17: shutting down must never make a finished task impossible to
submit. A credit timed at or after tonight's shutdown moves it to the
credit's own time plus its reward (capped at the ceiling), so LeetCode done
at 19:00 after an 18:00 shutdown means 20:00. Anchored on the credit's time,
never the sync's: a workout done at 17:00 and uploaded at 19:00 is not late.
"""
from __future__ import annotations
from datetime import date, datetime
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
import earned_time
import pytest
from screen_locker import _grace_floor, _late_credit, _shutdown, _sick_floor
from screen_locker._earned import first_minutes
from screen_locker._grace_floor import plan_absorb, save_lift
from screen_locker._shutdown import ShutdownMixin
from screen_locker.tests._sick_helpers import mark_sick
if TYPE_CHECKING:
from collections.abc import Iterator
# Captured before conftest pins it to noon for every test.
_real_now = _late_credit._now
_DAY = date(2026, 10, 17) # a Saturday: the Thu-Sun band is tonight's
_ISO = _DAY.isoformat()
def at(hhmm: str, day: date = _DAY) -> float:
"""Unix time of ``hhmm`` local on ``day``."""
hour, minute = (int(p) for p in hhmm.split(":"))
return datetime(day.year, day.month, day.day, hour, minute).astimezone().timestamp()
def host_workout_minutes() -> int:
"""The reward in minutes that the workout earner pays on the test day."""
return first_minutes(earned_time.WORKOUT, _DAY)
def m(hhmm: str) -> int:
"""Convert an HH:MM string into minutes after midnight."""
hour, minute = (int(p) for p in hhmm.split(":"))
return hour * 60 + minute
class Host(ShutdownMixin):
"""The real add paths over an in-memory config."""
def __init__(self, tonight: str, *, ok: bool = True) -> None:
self.config: tuple[int, int, int] = (m(tonight), m(tonight), 300)
self.ok = ok
def _read_shutdown_config(self) -> tuple[int, int, int] | None:
return self.config
def _write_shutdown_config(
self, mon_wed: int, thu_sun: int, morning_end: int, *, restore: bool = False
) -> bool:
assert restore
if self.ok:
self.config = (mon_wed, thu_sun, morning_end)
return self.ok
@property
def tonight(self) -> int:
"""Tonight's shutdown time in minutes (the Thu-Sun band)."""
return self.config[1]
@pytest.fixture(autouse=True)
def _on_the_day() -> Iterator[Any]:
with (
patch.object(_sick_floor, "today_str", return_value=_ISO),
patch.object(_grace_floor, "today_str", return_value=_ISO),
patch.object(_shutdown, "today_str", return_value=_ISO),
patch.object(_late_credit, "today_str", return_value=_ISO),
):
yield
class TestLateCredit:
"""Credits timed after tonight's shutdown restart the clock from their own time."""
def test_a_credit_before_the_shutdown_adds_as_before(self) -> None:
"""A credit before the shutdown simply adds its reward to tonight."""
host = Host("20:00")
assert host._adjust_shutdown_time_by(60, at=at("18:30")) is True
assert host.tonight == m("21:00")
def test_a_credit_after_the_shutdown_counts_from_its_own_time(self) -> None:
"""A late credit sets tonight to its own time plus the reward, in both bands."""
host = Host("18:00")
assert host._adjust_shutdown_time_by(60, at=at("19:00")) is True
assert host.tonight == m("20:00")
assert host.config[0] == m("20:00") # both bands, like the floors
def test_a_credit_exactly_at_the_shutdown_is_late(self) -> None:
"""The check script locks at ``now >= shutdown``."""
host = Host("19:00")
host._adjust_shutdown_time_by(60, at=at("19:00"))
assert host.tonight == m("20:00")
def test_capped_at_the_ceiling(self) -> None:
"""A late credit never pushes tonight past the 23:00 ceiling."""
host = Host("22:30")
host._adjust_shutdown_time_by(120, at=at("22:45"))
assert host.tonight == m("23:00")
def test_after_the_ceiling_nothing_moves_past_it(self) -> None:
"""A credit after the ceiling leaves tonight at 23:00."""
host = Host("23:00")
host._adjust_shutdown_time_by(60, at=at("23:10"))
assert host.tonight == m("23:00")
def test_a_late_upload_is_not_a_late_credit(self) -> None:
"""Done at 17:00, synced at 19:00: 18:00 + 2 h, not 19:00 + 2 h."""
host = Host("18:00")
assert host._adjust_shutdown_time_later(at=at("17:00")) is True
assert host.tonight == m("18:00") + host_workout_minutes()
def test_a_late_workout_counts_from_its_end(self) -> None:
"""A workout finishing after the shutdown counts from its end time."""
host = Host("18:00")
host._adjust_shutdown_time_later(at=at("19:00"))
assert host.tonight == min(m("23:00"), m("19:00") + host_workout_minutes())
def test_a_credit_timed_on_another_day_is_never_late(self) -> None:
"""A credit stamped on a different day is treated as not late."""
host = Host("18:00")
host._adjust_shutdown_time_by(60, at=at("19:00", date(2026, 10, 16)))
assert host.tonight == m("19:00")
def test_no_time_means_now(self) -> None:
"""Without an explicit time the credit is anchored on the current time."""
host = Host("18:00")
with patch.object(_late_credit, "_now", return_value=at("19:30")):
host._adjust_shutdown_time_by(60)
assert host.tonight == m("20:30")
def test_now_is_the_wall_clock(self) -> None:
"""The default clock reads the real wall-clock time."""
with patch.object(_late_credit.time, "time", return_value=123.0):
assert _real_now() == 123.0
def test_a_late_credit_clears_the_lift(self) -> None:
"""Past the shutdown the floor is spent: nothing is left to absorb."""
save_lift(30)
host = Host("18:00")
host._adjust_shutdown_time_by(60, at=at("19:00"))
assert host.tonight == m("20:00")
assert plan_absorb(60) == (60, 0)
def test_a_failed_write_keeps_the_lift(self) -> None:
"""If writing the config fails the saved lift is kept."""
save_lift(30)
host = Host("18:00", ok=False)
assert host._adjust_shutdown_time_by(60, at=at("19:00")) is False
assert plan_absorb(0) == (0, 30)
def test_unreadable_config_fails(self) -> None:
"""An unreadable shutdown config makes the late adjustment fail."""
host = Host("18:00")
with patch.object(Host, "_read_shutdown_config", return_value=None):
assert host._adjust_shutdown_time_by(60, at=at("19:00")) is False
class TestLateOnASickDay:
"""Late-credit behaviour on a sick day, where the sick target is the floor."""
def test_late_after_the_sick_target(self) -> None:
"""A credit after the sick target counts from its own time."""
mark_sick(_DAY)
host = Host("20:00")
with patch.object(_sick_floor, "sick_target", return_value=m("20:00")):
assert host._adjust_shutdown_time_by(60, at=at("21:00")) is True
assert host.tonight == m("22:00")
def test_not_late_raises_to_the_target_only(self) -> None:
"""A not-late credit on a sick day only raises tonight to the sick target."""
mark_sick(_DAY)
host = Host("21:00")
with patch.object(_sick_floor, "sick_target", return_value=m("22:00")):
host._adjust_shutdown_time_by(60, at=at("15:00"))
assert host.tonight == m("22:00")