mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 22:47:13 +01:00
- A day's RunnerUp walks (_Walking.tcx) count once their moving time sums to 40 min - Rest days: declared ahead, 2 per ISO week, HMAC-signed; pay the first workout unit - Grace floor: shutdown >= first task + 60 min (capped); skipped on a sick day - Workout ledger: one signed credit row per log credit slot and rest day - Day-aware ladder amounts from earned_time 0.4.0; preview_shutdown dry-run script Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QrjUTo1yhgAsUUasdw4bpG Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
3.3 KiB
Python
96 lines
3.3 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]:
|
|
with signing_key(tmp_path) as path:
|
|
yield path
|
|
|
|
|
|
def entry(data: dict[str, Any]) -> dict[str, Any]:
|
|
return {"timestamp": "2026-10-04T20:00:00+00:00", "workout_data": data}
|
|
|
|
|
|
def ledger() -> list[dict[str, Any]]:
|
|
return json.loads(ledger_path().read_text())["entries"]
|
|
|
|
|
|
@pytest.mark.usefixtures("key")
|
|
class TestBackfill:
|
|
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:
|
|
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:
|
|
argv = ["--dry-run", "--from", _ISO, "--to", "2026-10-05"]
|
|
assert run_backfill(self._log(tmp_path), argv) == 0
|
|
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:
|
|
log = self._log(tmp_path)
|
|
assert run_backfill(log, ["--from", _ISO, "--to", "2026-10-05"]) == 0
|
|
assert run_backfill(log, ["--from", _ISO, "--to", "2026-10-05"]) == 0
|
|
out = capsys.readouterr().out.splitlines()
|
|
assert out[0].startswith("2 new row(s)")
|
|
assert out[1].startswith("0 new row(s)")
|
|
assert run_backfill(log, ["--dry-run"]) == 0
|
|
assert "(present)" in capsys.readouterr().out
|
|
|
|
def test_default_is_today(self, tmp_path: Path) -> None:
|
|
with patch("screen_locker._workout_ledger_cli.today_str", return_value=_ISO):
|
|
assert run_backfill(self._log(tmp_path), []) == 0
|
|
assert len(ledger()) == 1
|