mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:07:16 +01:00
The counting fix made the numbers right; the history still read as though 2026-08-24 held two gym sessions. This annotates the pairs instead of deleting one of each, for two independent reasons. Deleting is self-undoing: every re-ingest path dedups against log.json's own contents rather than a remote ledger, so _session_sync re-pulls a removed pc_workout_verified from the CRDT store on the next 15-minute tick and re-applies its shutdown credit on the way in. And mutating signed history is against the position revoke_sick_day.py already states -- the correction belongs beside the record, not on top of it. Scanning the real log found six shared credits, not the three spotted by hand: 2026-07-17, 07-27, 08-10, 08-21, 08-24, 08-28. This has been happening since July. check_ledger_agrees refuses to write a ledger whose arithmetic disagrees with count_day_credits, so the ledger cannot come to describe a rule the locker does not use. The file is gitignored with the other state JSONs -- it names workout dates. The guard's test needs a fixture restoring sys.exit. conftest's autouse _block_real_tk_and_exit patches "screen_locker.screen_lock.sys.exit", and that attribute path resolves to the global sys module, so sys.exit is stubbed process-wide for every test in the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
164 lines
5.8 KiB
Python
164 lines
5.8 KiB
Python
"""Tests for the duplicate-credit ledger script.
|
|
|
|
The ledger is documentation, not behaviour -- nothing reads it to decide a
|
|
lock. Its one hard requirement is that it never contradicts
|
|
:func:`~screen_locker._weekly_check.count_day_credits`, because a ledger that
|
|
describes a different rule than the locker uses is worse than no ledger at all.
|
|
That invariant is enforced at runtime by ``check_ledger_agrees`` and pinned
|
|
here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from typing import Any, NoReturn
|
|
|
|
import pytest
|
|
|
|
from scripts.annotate_duplicate_credits import (
|
|
build_ledger,
|
|
check_ledger_agrees,
|
|
)
|
|
|
|
|
|
def _raising_exit(code: object = None) -> NoReturn:
|
|
"""Stand in for the real ``sys.exit``.
|
|
|
|
Args:
|
|
code: The exit status, as ``sys.exit`` takes.
|
|
|
|
Raises:
|
|
SystemExit: Always, exactly as ``sys.exit`` does.
|
|
"""
|
|
raise SystemExit(code)
|
|
|
|
|
|
@pytest.fixture
|
|
def restore_sys_exit(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Give this test a ``sys.exit`` that actually exits.
|
|
|
|
The autouse ``_block_real_tk_and_exit`` fixture in conftest patches
|
|
``screen_locker.screen_lock.sys.exit``. That attribute path resolves to the
|
|
*global* ``sys`` module object, not a per-module copy, so the patch replaces
|
|
``sys.exit`` for every module in the process for the duration of every test
|
|
-- including this script's. Without this fixture the guard under test is a
|
|
no-op and the test passes for the wrong reason.
|
|
|
|
Args:
|
|
monkeypatch: pytest's attribute patcher, which undoes this on teardown.
|
|
"""
|
|
monkeypatch.setattr(sys, "exit", _raising_exit)
|
|
|
|
|
|
def _entry(wtype: str, timestamp: str) -> dict[str, Any]:
|
|
"""Build a log entry with a workout_id, as the real writer does.
|
|
|
|
Args:
|
|
wtype: The ``workout_data.type`` value.
|
|
timestamp: ISO-8601 timestamp.
|
|
|
|
Returns:
|
|
A log entry dict.
|
|
"""
|
|
return {
|
|
"timestamp": timestamp,
|
|
"workout_data": {"type": wtype},
|
|
"workout_id": f"{wtype}:{timestamp[:10]}",
|
|
}
|
|
|
|
|
|
class TestBuildLedger:
|
|
"""The ledger names exactly the entries that share one credit."""
|
|
|
|
def test_stronglifts_pair_is_recorded(self) -> None:
|
|
"""A phone/PC pair on one day becomes one ledger record."""
|
|
logs = {
|
|
"2026-08-24": [
|
|
_entry("pc_workout_verified", "2026-08-24T10:55:29+00:00"),
|
|
_entry("phone_verified", "2026-08-24T10:55:38+00:00"),
|
|
],
|
|
}
|
|
ledger = build_ledger(logs)
|
|
assert list(ledger) == ["2026-08-24"]
|
|
record = ledger["2026-08-24"][0]
|
|
assert record["credit"] == "stronglifts"
|
|
# Earliest timestamp is the canonical entry; the later copy is absorbed.
|
|
assert record["counted"] == "pc_workout_verified:2026-08-24"
|
|
assert record["duplicate_of"] == ["phone_verified:2026-08-24"]
|
|
|
|
def test_days_without_a_shared_credit_are_absent(self) -> None:
|
|
"""Separate workouts produce no ledger entry at all."""
|
|
logs = {
|
|
"2026-08-24": [
|
|
_entry("phone_verified", "2026-08-24T10:00:00+00:00"),
|
|
_entry("runnerup_verified", "2026-08-24T19:00:00+00:00"),
|
|
],
|
|
"2026-08-25": [_entry("manual_workout", "2026-08-25T10:00:00+00:00")],
|
|
}
|
|
assert build_ledger(logs) == {}
|
|
|
|
def test_uncredited_entries_are_ignored(self) -> None:
|
|
"""A skip marker earns no credit, so it can never share one."""
|
|
logs = {
|
|
"2026-08-25": [
|
|
_entry("relaxed_day_skip", "2026-08-25T18:00:00+00:00"),
|
|
_entry("relaxed_day_skip", "2026-08-25T19:00:00+00:00"),
|
|
],
|
|
}
|
|
assert build_ledger(logs) == {}
|
|
|
|
def test_entry_without_workout_id_falls_back_to_position(self) -> None:
|
|
"""Legacy entries predating workout_id are still nameable."""
|
|
logs = {
|
|
"2026-08-24": [
|
|
{
|
|
"timestamp": "2026-08-24T10:00:00+00:00",
|
|
"workout_data": {"type": "phone_verified"},
|
|
},
|
|
{
|
|
"timestamp": "2026-08-24T11:00:00+00:00",
|
|
"workout_data": {"type": "pc_workout_verified"},
|
|
},
|
|
],
|
|
}
|
|
record = build_ledger(logs)["2026-08-24"][0]
|
|
assert record["counted"] == "2026-08-24#0"
|
|
assert record["duplicate_of"] == ["2026-08-24#1"]
|
|
|
|
|
|
class TestLedgerAgreesWithTheCreditRule:
|
|
"""The ledger's arithmetic must match count_day_credits exactly."""
|
|
|
|
def test_generated_ledger_agrees(self) -> None:
|
|
"""A ledger built from a log always agrees with that log."""
|
|
logs = {
|
|
"2026-08-24": [
|
|
_entry("pc_workout_verified", "2026-08-24T10:55:29+00:00"),
|
|
_entry("phone_verified", "2026-08-24T10:55:38+00:00"),
|
|
],
|
|
"2026-08-27": [_entry("manual_workout", "2026-08-27T18:45:32+00:00")],
|
|
}
|
|
check_ledger_agrees(logs, build_ledger(logs))
|
|
|
|
@pytest.mark.usefixtures("restore_sys_exit")
|
|
def test_over_claiming_ledger_is_refused(self) -> None:
|
|
"""A ledger claiming a duplicate that is not one aborts the script."""
|
|
logs = {
|
|
"2026-08-24": [
|
|
_entry("runnerup_verified", "2026-08-24T07:00:00+00:00"),
|
|
_entry("runnerup_verified", "2026-08-24T19:00:00+00:00"),
|
|
],
|
|
}
|
|
hand_written = {
|
|
"2026-08-24": [
|
|
{
|
|
"credit": "runnerup_verified",
|
|
"counted": "runnerup_verified:2026-08-24",
|
|
"duplicate_of": ["runnerup_verified:2026-08-24"],
|
|
"note": "wrong: two runs in a day are two workouts",
|
|
},
|
|
],
|
|
}
|
|
with pytest.raises(SystemExit, match="Ledger disagrees"):
|
|
check_ledger_agrees(logs, hand_written)
|