Refuse to build a lock screen over a workout already logged today
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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-09-18 09:58:13 +02:00
parent bdc6cc4df1
commit c0099dff94
3 changed files with 180 additions and 0 deletions

View File

@ -0,0 +1,59 @@
"""The one thing the enforce path must never do: lock over a logged workout.
Every skip in ``_startup_checks._check_non_verify_exits`` is a rung of a
first-match-wins ladder, and the ladder's ORDER is what decides whether a
logged workout counts. That order has been wrong before: on 2026-09-18 an
expired early-bird marker sat one rung above ``workout_logged_today``, so a
walk logged on the lock screen at 09:32 closed the lock and the 09:35 tick
locked again -- and would have every five minutes for the rest of the day.
The DECISION line even printed ``also=workout_logged_today``; the evidence
was there, only the gate was missing.
This module is that gate. It runs right before the ``enforced`` decision is
recorded and refuses to build a lock screen when a counted workout for today
is already in the log, whatever the ladder above it concluded. Reaching it
with a workout logged is a bug in the ladder, so it is reported at ERROR --
the locker must fail safe (no lock) and say so, not fail closed on the user.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Callable
_logger = logging.getLogger(__name__)
GUARD_REASON = "workout_logged_today_guard"
GUARD_DETAIL = (
"A counted workout is already logged today but the exemption ladder fell "
"through to enforcement — refusing to lock (ladder bug, see journal ERROR)."
)
def refuse_to_lock_over_logged_workout(
has_logged_today: Callable[[], bool],
record_skip: Callable[[str, str], None],
) -> bool:
"""Skip enforcement if today already holds a counted workout.
Takes the locker's two relevant methods rather than the locker itself so
the guard stays a pure function of "is there a workout?" and "how do I
skip?". Returns True when the guard fired (the skip was recorded through
*record_skip*, which exits the process), False when the lock may go
ahead. The ERROR is deliberate: a fired guard means a rung above it is
misordered, and that has to be visible in the journal rather than quietly
absorbed as one more skip.
"""
if not has_logged_today():
return False
_logger.error(
"Enforcement reached with a workout already logged today — the "
"early-exit ladder in _startup_checks/_auto_upgrade is misordered. "
"Refusing to lock; fix the ladder so workout_logged_today is reached."
)
record_skip(GUARD_REASON, GUARD_DETAIL)
return True

View File

@ -29,6 +29,7 @@ from screen_locker._decision_log import (
)
from screen_locker._degraded_sources import degraded_sources
from screen_locker._extra_benefits import process_week_transition
from screen_locker._lock_invariant import refuse_to_lock_over_logged_workout
from screen_locker._shutdown_base import (
apply_leetcode_bonus_if_new,
reset_to_base_if_new_day,
@ -161,6 +162,9 @@ class StartupChecksMixin(SyncMixin):
# "skip a workout" credit — that mechanic works against the goal of
# maximizing weekly workouts, so it was removed in favor of a
# shutdown-time-only reward (see _apply_weekly_shutdown_bonus).
# A logged workout outranks every rung above, whatever their order.
if refuse_to_lock_over_logged_workout(self.has_logged_today, self._record_skip):
return
heat = self._check_heat_skip_exit()
# Nothing excused today: falling through here means the lock WILL be
# built. Recorded explicitly so the trail shows enforcement happening,

View File

@ -0,0 +1,117 @@
"""Tests for the enforce-path guard in ``_lock_invariant``.
The ladder above the guard is exercised rung-by-rung elsewhere
(``test_weekly_logic_part2``). These tests force the whole ladder to fall
through -- the shape of every ordering bug it could ever have -- and check
that a logged workout still cannot be locked over.
"""
from __future__ import annotations
from contextlib import ExitStack
import logging
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from screen_locker._lock_invariant import (
GUARD_DETAIL,
GUARD_REASON,
refuse_to_lock_over_logged_workout,
)
from screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
import pytest
class TestRefuseToLockOverLoggedWorkout:
"""The guard function on its own."""
def test_nothing_logged_lets_the_lock_proceed(self) -> None:
"""No workout today: the guard is inert and records nothing."""
record_skip = MagicMock()
assert refuse_to_lock_over_logged_workout(lambda: False, record_skip) is False
record_skip.assert_not_called()
def test_logged_workout_records_the_guard_skip_and_logs_error(
self, caplog: pytest.LogCaptureFixture
) -> None:
"""A logged workout fires the guard: skip recorded, ERROR in the journal."""
record_skip = MagicMock()
with caplog.at_level(logging.ERROR, logger="screen_locker._lock_invariant"):
assert refuse_to_lock_over_logged_workout(lambda: True, record_skip) is True
record_skip.assert_called_once_with(GUARD_REASON, GUARD_DETAIL)
assert any(
record.levelno == logging.ERROR and "misordered" in record.getMessage()
for record in caplog.records
)
class TestEnforcePathChokepoint:
"""``_check_non_verify_exits`` with every rung above the guard falling through.
``_check_today_state_exits`` is stubbed to return False -- that is what an
expired early-bird marker did on 2026-09-18, and what any future
misordering of that ladder would do again -- so the only thing standing
between a logged workout and a lock screen is the guard.
"""
@staticmethod
def _fall_through_the_ladder(locker: object) -> ExitStack:
stack = ExitStack()
for patcher in (
patch.object(locker, "_is_scheduled_skip_today", return_value=False),
patch.object(locker, "_ingest_synced_manual_workouts"),
patch.object(locker, "_auto_fill_week_runnerup_bonus"),
patch.object(locker, "_check_today_state_exits", return_value=False),
patch.object(locker, "_check_heat_skip_exit", return_value="skipped"),
patch(
"screen_locker._startup_checks.process_week_transition", return_value=[]
),
patch(
"screen_locker._startup_checks.reset_to_base_if_new_day",
return_value=False,
),
patch("screen_locker._startup_checks.apply_leetcode_bonus_if_new"),
):
stack.enter_context(patcher)
return stack
def test_logged_workout_is_never_locked_over(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Ladder fell through with a workout logged: guard skip, no ``enforced``."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "has_logged_today", return_value=True),
patch("screen_locker._startup_checks.record_decision") as record,
self._fall_through_the_ladder(locker),
):
locker._check_non_verify_exits()
reasons = [call.args[0].reason for call in record.call_args_list]
assert reasons == [GUARD_REASON]
assert record.call_args.args[0].locked is False
mock_sys_exit.assert_called_once_with(0)
def test_no_workout_still_enforces(
self,
mock_tk: MagicMock,
mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Same fall-through with nothing logged: the lock goes ahead as before."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "has_logged_today", return_value=False),
patch("screen_locker._startup_checks.record_decision") as record,
self._fall_through_the_ladder(locker),
):
locker._check_non_verify_exits()
assert [call.args[0].reason for call in record.call_args_list] == ["enforced"]
assert record.call_args.args[0].locked is True
mock_sys_exit.assert_not_called()