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

105 lines
3.9 KiB
Python

"""Tests for ``--backfill-workout-ledger``: idempotent, per source, range-aware."""
from __future__ import annotations
from datetime import date, datetime
import json
from typing import TYPE_CHECKING, Any
from unittest.mock import patch
import pytest
from screen_locker._rest_day import declare
from screen_locker._workout_ledger import ledger_path
from screen_locker._workout_ledger_cli import backfill_rows, run_backfill
from screen_locker.tests._earned_fixtures import signing_key
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
_DAY = date(2026, 10, 4)
_ISO = _DAY.isoformat()
@pytest.fixture
def key(tmp_path: Path) -> Iterator[Path]:
"""Provide a signing key in a temp dir for the duration of the test."""
with signing_key(tmp_path) as path:
yield path
def entry(data: dict[str, Any]) -> dict[str, Any]:
"""Build a log entry around the given workout data."""
return {"timestamp": "2026-10-04T20:00:00+00:00", "workout_data": data}
def ledger() -> list[dict[str, Any]]:
"""Read back the entries stored in the signed ledger file."""
return json.loads(ledger_path().read_text())["entries"]
@pytest.mark.usefixtures("key")
class TestBackfill:
"""The backfill CLI rebuilds missing ledger rows from the workout log and rest days."""
def _log(self, tmp_path: Path) -> Path:
log = tmp_path / "log.json"
log.write_text(
json.dumps(
{
_ISO: [
entry({"type": "phone_verified"}),
entry({"type": "relaxed_day_skip"}),
],
"2026-10-05": [
entry({"type": "runnerup_verified", "completed_at": 9.0})
],
}
)
)
return log
def test_rows_for_a_range_and_its_rest_days(self, tmp_path: Path) -> None:
"""Backfill yields rows for the log entries in range plus its declared rest days."""
with patch("screen_locker._rest_day.datetime") as clock:
clock.now.return_value = datetime(2026, 10, 1).astimezone()
assert declare(date(2026, 10, 6)).ok
ledger_path().unlink() # the declaration mirrored it; backfill must too
rows = backfill_rows(self._log(tmp_path), _DAY, date(2026, 10, 6))
assert [r["detail"]["source"] for r in rows] == [
"stronglifts",
"runnerup_tcx",
"rest_day",
]
def test_dry_run_prints_and_writes_nothing(
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A dry run reports missing rows per source and writes no ledger."""
argv = ["--dry-run", "--from", _ISO, "--to", "2026-10-05"]
assert not run_backfill(self._log(tmp_path), argv)
out = capsys.readouterr().out
assert "(missing)" in out
assert "per source: {'runnerup_tcx': 1, 'stronglifts': 1}" in out
assert not ledger_path().exists()
def test_a_real_run_is_idempotent(
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Running the backfill twice adds rows only the first time."""
log = self._log(tmp_path)
assert not run_backfill(log, ["--from", _ISO, "--to", "2026-10-05"])
assert not run_backfill(log, ["--from", _ISO, "--to", "2026-10-05"])
out = capsys.readouterr().out.splitlines()
assert out[0].startswith("2 new row(s)")
assert out[1].startswith("0 new row(s)")
assert not run_backfill(log, ["--dry-run"])
assert "(present)" in capsys.readouterr().out
def test_default_is_today(self, tmp_path: Path) -> None:
"""With no range given, the backfill covers today only."""
with patch("screen_locker._workout_ledger_cli.today_str", return_value=_ISO):
assert not run_backfill(self._log(tmp_path), [])
assert len(ledger()) == 1