screen-locker/screen_locker/tests/test_manual_schedule.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

161 lines
6.5 KiB
Python

"""Tests for logging a manual workout for a future day from this PC."""
from __future__ import annotations
from datetime import date, timedelta
import json
from types import SimpleNamespace
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
from screen_locker import _manual_schedule
from screen_locker._day import today_str
from screen_locker._manual_push import PushResult
from screen_locker._manual_schedule import budget_error, schedule_future_workout
from screen_locker._manual_workout import (
SPORT_OTHER,
ManualWorkoutDraft,
build_sync_payload,
)
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
def _plus(days: int) -> str:
"""Return the local ISO date ``days`` from today."""
return (date.fromisoformat(today_str()) + timedelta(days=days)).isoformat()
def _draft(start: str = "18:00") -> ManualWorkoutDraft:
"""A valid 'other sport' draft."""
return ManualWorkoutDraft(
sport=SPORT_OTHER,
start_time=start,
end_time="19:30",
location_name="Padel Club",
transport_method="bike",
cost="40 PLN",
rpe=7,
went_well="Good rallies and fast footwork",
to_improve="Second serve needs more spin work",
overall_feeling="Tired but happy, legs felt strong",
activity_type_other="padel",
activity_details="Ninety minutes of padel doubles, baseline and net play",
)
@pytest.fixture(name="stubs")
def stubs_fixture(tmp_path: Path) -> Iterator[SimpleNamespace]:
"""Redirect the ledger; stub Firebase pull, the publisher and the identity."""
ledger = tmp_path / "pending.json"
pulled: list[tuple[str, dict]] = []
push = MagicMock(return_value=PushResult(pushed=True, record_count=1, reason="ok"))
with (
patch("screen_locker._manual_pending.MANUAL_PENDING_FILE", ledger),
patch.object(_manual_schedule, "pull_all_manual_records", lambda: list(pulled)),
patch.object(_manual_schedule, "push_pc_workouts", push),
patch.object(
_manual_schedule,
"device_identity",
lambda: SimpleNamespace(device_id="pc-test"),
),
):
yield SimpleNamespace(
ledger=ledger, pulled=pulled, push=push, log_file=tmp_path / "log.json"
)
class TestBudgetError:
"""Budget sees the Firebase pull AND this PC's ledger."""
def test_open_when_nothing_is_pending(self, stubs: SimpleNamespace) -> None:
"""With nothing pending the weekly budget has no error."""
assert budget_error(stubs.log_file, _plus(1)) is None
def test_phone_pending_entries_hold_a_slot(self, stubs: SimpleNamespace) -> None:
"""Entries pulled from the phone count against the budget."""
for offset in (1, 2):
payload = build_sync_payload(_draft(f"1{offset}:00"), _plus(offset))
stubs.pulled.append((f"manual:{_plus(offset)}T1{offset}:00", payload))
assert budget_error(stubs.log_file, _plus(3)) is not None
def test_excluding_an_id_frees_its_slot(self, stubs: SimpleNamespace) -> None:
"""Excluding a record id frees the slot that record held."""
ids = []
for offset in (1, 2):
rid = f"manual:{_plus(offset)}T1{offset}:00"
ids.append(rid)
stubs.pulled.append((rid, build_sync_payload(_draft(), _plus(offset))))
assert budget_error(stubs.log_file, _plus(3)) is not None
assert budget_error(stubs.log_file, _plus(3), exclude_id=ids[0]) is None
class TestScheduleFutureWorkout:
"""Hold a validated draft for a later day and publish it."""
def test_schedules_stores_and_publishes(self, stubs: SimpleNamespace) -> None:
"""A valid future draft is stored in the ledger and pushed once without touching the log."""
day = _plus(1)
result = schedule_future_workout(stubs.log_file, _draft(), day)
assert result.scheduled is True
assert result.record_id == f"manual:{day}T18:00"
assert "counts automatically" in result.reason
assert list(json.loads(stubs.ledger.read_text())) == [result.record_id]
stubs.push.assert_called_once_with(stubs.log_file)
assert not stubs.log_file.exists()
def test_an_unpublished_entry_is_still_scheduled_and_says_so(
self, stubs: SimpleNamespace
) -> None:
"""A failed publish still schedules the entry and reports it is unpublished."""
stubs.push.return_value = PushResult(
pushed=False, record_count=1, reason="Firebase down"
)
result = schedule_future_workout(stubs.log_file, _draft(), _plus(2))
assert result.scheduled is True
assert "not yet published: Firebase down" in result.reason
def test_today_is_not_a_future_date(self, stubs: SimpleNamespace) -> None:
"""Today is refused as a schedule date and nothing is pushed."""
result = schedule_future_workout(stubs.log_file, _draft(), _plus(0))
assert result.scheduled is False
assert "is today" in result.reason
stubs.push.assert_not_called()
@pytest.mark.parametrize("ahead", [-1, 8])
def test_a_date_outside_the_window_is_refused(
self, stubs: SimpleNamespace, ahead: int
) -> None:
"""Dates in the past or beyond the window are refused with nothing stored."""
result = schedule_future_workout(stubs.log_file, _draft(), _plus(ahead))
assert result.scheduled is False
assert not stubs.ledger.exists()
def test_an_exhausted_budget_is_refused(self, stubs: SimpleNamespace) -> None:
"""Once two workouts are scheduled a third is refused for budget."""
assert schedule_future_workout(
stubs.log_file, _draft("10:00"), _plus(1)
).scheduled
assert schedule_future_workout(
stubs.log_file, _draft("11:00"), _plus(2)
).scheduled
refused = schedule_future_workout(stubs.log_file, _draft("12:00"), _plus(3))
assert refused.scheduled is False
assert "budget" in refused.reason.lower()
assert len(json.loads(stubs.ledger.read_text())) == 2
def test_the_same_entry_twice_is_refused(self, stubs: SimpleNamespace) -> None:
"""Scheduling the same entry twice is refused and pushed only once."""
day = _plus(1)
assert schedule_future_workout(stubs.log_file, _draft(), day).scheduled
again = schedule_future_workout(stubs.log_file, _draft(), day)
assert again.scheduled is False
assert "already scheduled" in again.reason
stubs.push.assert_called_once()