screen-locker/screen_locker/tests/test_morning_session.py
Krzysztof kuhy Rudnicki 53bbdda419
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 (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Read the morning-session carrot from wake-alarm's signed file
The wake_alarm_skip rung has been dead since the repos split: it read
../wake_alarm/wake_state.json (no longer a path) for a skip_workout field
nothing ever wrote, and answered "no skip" for a year without an error.
It now reads ~/.local/state/wake_alarm/morning_session.json, written
HMAC-signed by wake-alarm every two minutes through the morning, and the
whole decision is: signature ok, dated today, now < exempt_until. A
completed-in-time morning defers the lock to 11:00; a live one until
min(start + 90 min, 09:30); nothing else changes the ladder. The enforce
path waits up to 30 s for a freshly booted PC's refresher; status paths
never wait. Takes Flutter 3.47.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
2026-09-19 16:41:17 +02:00

140 lines
4.8 KiB
Python

"""screen_locker._morning_session — the carrot file, read and never re-derived."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
import json
from typing import TYPE_CHECKING
from unittest.mock import patch
from gatelock.log_integrity import compute_entry_hmac
import pytest
from screen_locker import _morning_session
from screen_locker._morning_session import (
MorningSkip,
has_workout_skip_today,
morning_skip_today,
)
if TYPE_CHECKING:
from pathlib import Path
NOW = datetime(2026, 9, 19, 9, 0).astimezone()
def _write(entry: dict[str, object], *, sign: bool = True) -> Path:
"""Write an entry to the redirected file; signed unless told otherwise."""
if sign:
entry["hmac"] = compute_entry_hmac(entry)
path = _morning_session.MORNING_SESSION_FILE
path.write_text(json.dumps(entry))
return path
def _entry(
outcome: str = "completed",
exempt_until: str | None = "11:00",
date: str = "2026-09-19",
) -> dict[str, object]:
entry: dict[str, object] = {"date": date, "outcome": outcome}
if exempt_until is not None:
entry["exempt_until"] = (
datetime.fromisoformat(f"{date}T{exempt_until}").astimezone().isoformat()
)
return entry
@pytest.fixture(autouse=True)
def _signing_key(tmp_path: Path) -> None:
key = tmp_path / "hmac.key"
key.write_bytes(b"1" * 32)
with patch("gatelock.log_integrity.DEFAULT_HMAC_KEY_FILE", key):
yield
class TestVerdict:
"""One rule: signed, today, not yet expired."""
def test_completed_grants_until_the_signed_instant(self) -> None:
_write(_entry())
skip = morning_skip_today(NOW, wait=False)
assert skip == MorningSkip("completed", NOW.replace(hour=11))
assert "no lock until 11:00" in str(skip)
def test_expired_and_absent_exemptions_grant_nothing(self) -> None:
_write(_entry())
assert morning_skip_today(NOW + timedelta(hours=3), wait=False) is None
_write(_entry("failed", exempt_until=None))
assert morning_skip_today(NOW, wait=False) is None
_write({**_entry(), "exempt_until": "later"})
assert morning_skip_today(NOW, wait=False) is None
def test_yesterdays_file_is_not_today(self) -> None:
_write(_entry(date="2026-09-18"))
assert morning_skip_today(NOW, wait=False) is None
def test_missing_unreadable_tampered_are_all_nothing(self, tmp_path: Path) -> None:
assert morning_skip_today(NOW, wait=False) is None
path = _write(_entry(), sign=False)
assert morning_skip_today(NOW, wait=False) is None
path.write_text("[1]")
assert morning_skip_today(NOW, wait=False) is None
path.write_text("{")
assert morning_skip_today(NOW, wait=False) is None
path.unlink()
path.mkdir()
assert morning_skip_today(NOW, wait=False) is None
assert tmp_path in path.parents
def test_default_now_is_the_wall_clock(self) -> None:
_write(
_entry(
date=datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d"),
exempt_until="23:59",
)
)
assert has_workout_skip_today() is True
class TestBootRetry:
"""The enforce path waits for the refresher, bounded; status never does."""
def test_waits_inside_the_window_then_warns(
self, caplog: pytest.LogCaptureFixture
) -> None:
naps: list[float] = []
# The window is re-checked against the real clock after each nap.
with (
patch.object(_morning_session, "MORNING_RETRY_SECONDS", 10.0),
patch.object(_morning_session, "MORNING_WINDOW", ((0, 0), (23, 59))),
):
assert morning_skip_today(NOW, wait=True, sleep=naps.append) is None
assert naps == [5.0, 5.0]
assert "is wake-alarm-session.timer running" in caplog.text
def test_a_file_landing_mid_wait_is_honoured(self) -> None:
def land(_seconds: float) -> None:
_write(
_entry(
date=datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d"),
exempt_until="23:59",
)
)
with (
patch.object(_morning_session, "MORNING_RETRY_SECONDS", 10.0),
patch.object(_morning_session, "MORNING_WINDOW", ((0, 0), (23, 59))),
):
assert morning_skip_today(NOW, wait=True, sleep=land) is not None
def test_never_waits_outside_the_window_or_without_wait(self) -> None:
naps: list[float] = []
with patch.object(_morning_session, "MORNING_RETRY_SECONDS", 10.0):
assert (
morning_skip_today(NOW.replace(hour=14), wait=True, sleep=naps.append)
is None
)
assert morning_skip_today(NOW, wait=False, sleep=naps.append) is None
assert naps == []