"""Tests for ingesting synced manual workouts into log.json.""" # pylint: disable=protected-access from __future__ import annotations import json from typing import TYPE_CHECKING from unittest.mock import patch from screen_locker import _manual_sync from screen_locker._log_mixin import RecordResult from screen_locker._manual_sync import ( ingest_manual_records, ) from screen_locker._manual_workout import ( SPORT_OTHER, SPORT_TABLE_TENNIS, ManualWorkoutDraft, ) from screen_locker.tests.test_manual_sync import _manual_record if TYPE_CHECKING: from pathlib import Path import pytest _DATE = "2026-07-13" _TT_DRAFT = ManualWorkoutDraft( sport=SPORT_TABLE_TENNIS, start_time="18:00", end_time="19:30", location_name="Solec", transport_method="bike", cost="40 PLN", rpe=6, went_well="Served consistently and moved feet well", to_improve="Backhand topspin needs more consistency", overall_feeling="Felt strong focused and in good rhythm", matches_won=3, matches_lost=1, sets_won=7, sets_lost=4, racket="Butterfly", balls="Nittaku 3-star", ) _OTHER_DRAFT = ManualWorkoutDraft( sport=SPORT_OTHER, start_time="09:00", end_time="10:00", location_name="Squash club", transport_method="car", cost="30 PLN", rpe=5, went_well="Moved well and kept long rallies going", to_improve="Need to volley earlier off the back wall", overall_feeling="Tired but satisfied with the effort", activity_type_other="squash", activity_details="Full-court squash drills and three practice games total", equipment="racket, goggles", ) class TestIngestManualRecordsCallback: """Tests for the ``on_ingested`` callback that drives shutdown credit.""" def test_on_ingested_called_with_entry_and_prior_entries( self, tmp_path: Path ) -> None: """On ingested called with entry and prior entries.""" log_file = tmp_path / "log.json" log_file.write_text( json.dumps({_DATE: [{"workout_data": {"type": "phone_verified"}}]}) ) record = _manual_record(_TT_DRAFT) calls: list[tuple[dict, list[dict]]] = [] ingest_manual_records( log_file, [record], today=_DATE, on_ingested=lambda entry, prior: calls.append((entry, prior)), ) assert len(calls) == 1 entry, prior = calls[0] assert entry["type"] == "manual_workout" assert entry["sync_record_id"] == record[0] assert prior == [{"workout_data": {"type": "phone_verified"}}] def test_on_ingested_not_called_when_budget_exhausted(self, tmp_path: Path) -> None: """On ingested not called when budget exhausted.""" log_file = tmp_path / "log.json" record = _manual_record(_TT_DRAFT) calls: list[tuple[dict, list[dict]]] = [] with patch.object(_manual_sync, "is_budget_exhausted", return_value=True): ingest_manual_records( log_file, [record], today=_DATE, on_ingested=lambda entry, prior: calls.append((entry, prior)), ) assert calls == [] def test_write_chokepoint_collision_is_not_credited_or_returned( self, tmp_path: Path ) -> None: """If the write chokepoint's own workout_id dedup no-ops the write (``appended=False``), the record must not be reported as ingested or handed to ``on_ingested`` — crediting a write that never happened would be a real correctness bug, not just a missed optimization.""" log_file = tmp_path / "log.json" record = _manual_record(_TT_DRAFT) calls: list[tuple[dict, list[dict]]] = [] with patch( "screen_locker._manual_sync.write_signed_entry", return_value=RecordResult(appended=False, prior_entries=[]), ): ingested = ingest_manual_records( log_file, [record], today=_DATE, on_ingested=lambda entry, prior: calls.append((entry, prior)), ) assert ingested == [] assert calls == [] class TestEmptyStubRecords: """A payload with no workout content is skipped quietly, not as a fault. Observed in the wild as ``{"type", "kind", "date"}`` -- the phone created the record but never attached the workout. It replayed as a WARNING on every 15-minute sync cycle forever, which is how genuinely actionable lines from this logger got tuned out. """ def test_is_empty_stub_detects_a_metadata_only_payload(self) -> None: """No substantive field present → stub.""" assert _manual_sync.is_empty_stub( {"type": "manual_workout", "kind": "manual_workout", "date": _DATE} ) def test_is_empty_stub_is_false_when_any_workout_field_exists(self) -> None: """One real field is enough to make it a (possibly broken) workout.""" assert not _manual_sync.is_empty_stub( {"kind": "manual_workout", "sport": "run"} ) def test_stub_is_skipped_without_a_warning( self, tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: """The stub is reported at info, so it stops polluting the warnings.""" log_file = tmp_path / "log.json" log_file.write_text(json.dumps({})) with caplog.at_level("INFO"): ingested = ingest_manual_records( log_file, [ ( "manual_workout:2026-08-15", { "kind": "manual_workout", "type": "manual_workout", "date": _DATE, }, ) ], ) assert ingested == [] stub_records = [r for r in caplog.records if "empty stub" in r.message] assert stub_records, "the stub must still be reported" assert all(r.levelname == "INFO" for r in stub_records) assert not [r for r in caplog.records if r.levelname == "WARNING"]