"""Tests for gather_status's lock-suppression paths. Split out of test_status_data_part2.py for the 250-line cap: these are the cases where something (sick day, wake alarm, scheduled skip, relaxed day, weekly minimum) means no lock is needed. """ from __future__ import annotations from datetime import UTC, date, datetime import json from typing import TYPE_CHECKING from unittest.mock import patch import freedays import sickdays from screen_locker._status_data import format_summary_line, gather_status from screen_locker.tests._sick_helpers import mark_sick from screen_locker.tests.test_status_data import _files if TYPE_CHECKING: from pathlib import Path # Fixed reference instant: Friday 2024-01-05, 12:00 UTC == 13:00 Europe/Warsaw. # Outside the 05:00-09:00 early-bird window and not a Tue/Wed/Thu relaxed day, # so lock-decision branches are fully deterministic regardless of wall clock. _FRIDAY_NOON_UTC = datetime(2024, 1, 5, 12, 0, tzinfo=UTC) # Monday of that same ISO week, for Mon-Wed shutdown-band assertions. _MONDAY_NOON_UTC = datetime(2024, 1, 1, 12, 0, tzinfo=UTC) class TestGatherStatusLockSuppression: """Conditions under which gather_status reports no lock is needed.""" def test_sick_day_marks_today_and_skips_lock(self, tmp_path: Path) -> None: """Sick day marks today and skips lock.""" files = _files(tmp_path) mark_sick("2024-01-05") with patch( "screen_locker._status_data.has_workout_skip_today", return_value=False ): snap = gather_status(**files, now=_FRIDAY_NOON_UTC) assert snap.today.is_sick_day is True assert format_summary_line(snap).endswith("· sick day") assert snap.lock_explanation.fired is False assert snap.lock_explanation.stage == "sick_day" def test_sick_day_keeping_the_workout_lock_still_locks( self, tmp_path: Path ) -> None: """A kept workout lock is a normal day: not marked sick, not skipped.""" files = _files(tmp_path) mark_sick("2024-01-05", keep=[sickdays.LOCKER_WORKOUT]) with patch( "screen_locker._status_data.has_workout_skip_today", return_value=False ): snap = gather_status(**files, now=_FRIDAY_NOON_UTC) assert snap.today.is_sick_day is False assert not format_summary_line(snap).endswith("· sick day") assert snap.lock_explanation.stage != "sick_day" def test_wake_alarm_skip_stops_lock(self, tmp_path: Path) -> None: """Wake alarm skip stops lock.""" files = _files(tmp_path) with patch( "screen_locker._status_data.has_workout_skip_today", return_value=True ): snap = gather_status(**files, now=_FRIDAY_NOON_UTC) assert snap.lock_explanation.fired is False assert snap.lock_explanation.stage == "wake_alarm_skip" def test_scheduled_skip_stops_lock(self, tmp_path: Path) -> None: """Scheduled skip stops lock.""" files = _files(tmp_path) freedays.mark( date(2024, 1, 5), paths=freedays.Paths.under(tmp_path / "freedays"), now=date(2024, 1, 5), ) with patch( "screen_locker._status_data.has_workout_skip_today", return_value=False ): snap = gather_status(**files, now=_FRIDAY_NOON_UTC) assert snap.lock_explanation.stage == "scheduled_skip" def test_relaxed_day_stops_lock(self, tmp_path: Path) -> None: """Tuesday is a relaxed day — is_relaxed_day derives it from `now`.""" files = _files(tmp_path) tuesday_noon = datetime(2024, 1, 2, 12, 0, tzinfo=UTC) with patch( "screen_locker._status_data.has_workout_skip_today", return_value=False ): snap = gather_status(**files, now=tuesday_noon) assert snap.lock_explanation.stage == "relaxed_day" def test_weekly_minimum_met_stops_lock(self, tmp_path: Path) -> None: # Avoid dating anything "today" (2024-01-05): an unsigned today entry # trips already_logged before weekly_minimum_met, and does so only # when no HMAC key is configured (env-dependent) -- Jan 4 holds two. """Weekly minimum met stops lock.""" files = _files(tmp_path) files["log_file"].write_text( json.dumps( { "2024-01-01": {"workout_data": {"type": "phone_verified"}}, "2024-01-02": {"workout_data": {"type": "phone_verified"}}, "2024-01-03": {"workout_data": {"type": "phone_verified"}}, # A gym session and a run on the same day: two genuinely # separate workouts, so two credits. Deliberately NOT two # phone_verified entries -- since the 2026-08-29 fix those # share one StrongLifts credit slot per day, which is the # whole point of the dedup. "2024-01-04": [ {"workout_data": {"type": "phone_verified"}}, {"workout_data": {"type": "runnerup_verified"}}, ], } ) ) with patch( "screen_locker._status_data.has_workout_skip_today", return_value=False ): snap = gather_status(**files, now=_FRIDAY_NOON_UTC) assert snap.week.counted_count == 5 assert not snap.week.remaining assert not snap.week.extra assert snap.lock_explanation.stage == "weekly_minimum_met" def test_extra_workouts_beyond_minimum(self, tmp_path: Path) -> None: """Extra workouts beyond minimum.""" files = _files(tmp_path) # Jan 1-4 one each, Jan 5 (today, Friday — the last day in-week without # going into the future) holds two, for 6 counted total: 1 above the # new minimum of 5. Also exercises multiple same-day entries each # counting individually — which is why the pair is a gym session plus a # run rather than two phone_verified entries: those are the two # ingestion paths for ONE StrongLifts session and share a credit slot. files["log_file"].write_text( json.dumps( { "2024-01-01": {"workout_data": {"type": "phone_verified"}}, "2024-01-02": {"workout_data": {"type": "phone_verified"}}, "2024-01-03": {"workout_data": {"type": "phone_verified"}}, "2024-01-04": {"workout_data": {"type": "phone_verified"}}, "2024-01-05": [ {"workout_data": {"type": "phone_verified"}}, {"workout_data": {"type": "runnerup_verified"}}, ], } ) ) with patch( "screen_locker._status_data.has_workout_skip_today", return_value=False ): snap = gather_status(**files, now=_FRIDAY_NOON_UTC) assert snap.week.extra == 1 assert not snap.week.remaining