screen-locker/screen_locker/tests/test_manual_budget_merge_fixture.py
Krzysztof kuhy Rudnicki 4a36b6b264 Fix mechanical pylint findings in tests
Booleaness asserts, hoisted imports, fixture(name=...) instead of
shadowing arguments, explicit encodings, snake_case names, comparison
of bound methods, dead protected-access disables.

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

168 lines
6.4 KiB
Python

"""Cross-language fixture test for manual-workout budget counting.
The Dart app mirrors this accounting in ``lib/models/manual_workout_budget.dart``
and ``lib/services/workout_sync_service_read.dart``. Each language's own
round-trip test passed happily while the two sides disagreed -- on 2026-08-27
the phone read ``2/2w 4/10m (exhausted)`` for the very records this side read
as ``1/2w 2/10m`` -- so the only thing that catches the drift is a shared
literal both read: ``fixtures/manual_budget_merge.json``. The Dart twin of
this file is ``test/models/manual_budget_merge_fixture_test.dart``; they must
stay in step.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from screen_locker._log_mixin import write_signed_entry
from screen_locker._manual_future import budget_error_for_date, budget_usage_for_date
from screen_locker._manual_workout import (
MANUAL_WORKOUT_TYPE,
count_in_window,
is_budget_exhausted,
)
FIXTURE = Path(__file__).parent / "fixtures" / "manual_budget_merge.json"
def _fixture() -> dict[str, Any]:
"""Load the shared cross-language budget fixture."""
loaded: dict[str, Any] = json.loads(FIXTURE.read_text(encoding="utf-8"))
return loaded
def _live_payloads(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Drop tombstoned records, as the sync reader does across the union."""
return [r["payload"] for r in records if not r["deleted"]]
def _log_from(payloads: list[dict[str, Any]], log_file: Path) -> Path:
"""Write the ingestable payloads into a real signed log file.
Stubs without ``start_time`` are skipped exactly as ``_manual_sync``
refuses them ("no workout data to ingest"), so the fixture exercises the
same admission rule the live ingest applies.
"""
for payload in payloads:
if not isinstance(payload.get("start_time"), str):
continue
workout_data = {k: v for k, v in payload.items() if k != "kind"}
workout_data["type"] = MANUAL_WORKOUT_TYPE
workout_data["workout_id"] = f"manual:{payload['date']}T{payload['start_time']}"
write_signed_entry(log_file, str(payload["date"]), workout_data)
return log_file
def test_budget_matches_the_phone_for_the_2026_08_27_divergence(
tmp_path: Path,
) -> None:
"""The window counts must equal the numbers the Dart twin asserts."""
fixture = _fixture()
expected = fixture["expected"]
today = str(fixture["today"])
log_file = _log_from(_live_payloads(fixture["records"]), tmp_path / "log.json")
assert count_in_window(log_file, 7, today=today) == expected["week"]
assert count_in_window(log_file, 30, today=today) == expected["month"]
assert is_budget_exhausted(log_file, today=today) is expected["exhausted"]
def test_a_tombstoned_duplicate_frees_its_weekly_slot(tmp_path: Path) -> None:
"""Resurrecting the deleted duplicate must exhaust the week.
This is the pre-fix behaviour the phone exhibited; asserting it proves the
tombstone is load-bearing rather than incidentally outside the window.
"""
fixture = _fixture()
today = str(fixture["today"])
resurrected = [r["payload"] for r in fixture["records"]]
log_file = _log_from(resurrected, tmp_path / "log.json")
assert count_in_window(log_file, 7, today=today) == 2
assert is_budget_exhausted(log_file, today=today) is True
def test_an_evidence_less_stub_never_consumes_budget(tmp_path: Path) -> None:
"""The `{type, kind, date}` stub must cost nothing in either window."""
fixture = _fixture()
today = str(fixture["today"])
stub = next(
r["payload"] for r in fixture["records"] if "start_time" not in r["payload"]
)
log_file = _log_from([stub], tmp_path / "log.json")
assert not count_in_window(log_file, 7, today=today)
assert not count_in_window(log_file, 30, today=today)
def _planned_cases() -> list[tuple[str, dict[str, Any]]]:
"""Every ``planned.expected_by_date`` entry as ``(date, expectation)``."""
return sorted(_fixture()["planned"]["expected_by_date"].items())
def _planned_live_payloads() -> list[dict[str, Any]]:
"""The planned scenario: top-level records plus ``extra_records``, live only."""
fixture = _fixture()
records = [*fixture["records"], *fixture["planned"]["extra_records"]]
return _live_payloads(records)
@pytest.mark.parametrize(("day", "expected"), _planned_cases())
def test_planned_budget_by_workout_date_matches_the_phone(
tmp_path: Path, day: str, expected: dict[str, Any]
) -> None:
"""The per-date admission rule must equal the phone's, entry by entry.
The planned entries sit in the log here; the PC holds them out of the log
until their day, so the same scenario is also run with them as pending
dates (next test) and both must agree.
"""
log_file = _log_from(_planned_live_payloads(), tmp_path / "log.json")
usage = budget_usage_for_date(log_file, day)
assert usage is not None
assert usage.week == expected["week"]
assert usage.month == expected["month"]
assert usage.exhausted is expected["exhausted"]
assert (budget_error_for_date(log_file, day) is not None) is expected["exhausted"]
@pytest.mark.parametrize(("day", "expected"), _planned_cases())
def test_planned_entries_held_as_pending_count_the_same(
tmp_path: Path, day: str, expected: dict[str, Any]
) -> None:
"""Pending future entries (not yet in the log) hold the same slots."""
fixture = _fixture()
today = str(fixture["today"])
live = _planned_live_payloads()
logged = [p for p in live if str(p["date"]) <= today]
pending = [str(p["date"]) for p in live if str(p["date"]) > today]
log_file = _log_from(logged, tmp_path / "log.json")
usage = budget_usage_for_date(log_file, day, pending)
assert usage is not None
assert (usage.week, usage.month) == (expected["week"], expected["month"])
assert usage.exhausted is expected["exhausted"]
def test_a_cancelled_plan_frees_its_slot(tmp_path: Path) -> None:
"""Resurrecting the deleted 08-30 plan must exhaust the week around 08-28."""
fixture = _fixture()
everything = [
r["payload"]
for r in [*fixture["records"], *fixture["planned"]["extra_records"]]
]
log_file = _log_from(everything, tmp_path / "log.json")
usage = budget_usage_for_date(log_file, "2026-08-28")
assert usage is not None
assert usage.week == 2
assert usage.exhausted is True