mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
The locker did not fire on 2026-08-29, or for the two weeks before it. Every 30 minutes it logged: DECISION lock=no reason=weekly_minimum_met weekly=5/5 The 5 was wrong. A StrongLifts session is ingested twice -- the phone sync writes phone_verified, the PC session sync writes pc_workout_verified, both from the same WorkoutSession payload -- and workout_id is "<type>:<date>", so the two copies get different IDs and neither dedups the other away. count_weekly_workouts then counted each entry individually. Real totals against what was counted: week of 2026-08-17 5 counted 4 real week of 2026-08-24 5 counted 3 real Both weeks were waived. Nothing reported a problem, because 5/5 is exactly what a compliant week looks like. Add credit_key/count_day_credits to _weekly_check as the single rule for "does this entry earn a credit, and is it the same credit as another": the two StrongLifts ingestion paths share one slot per day, everything else stays per-entry so two runs or two manual workouts in a day still count twice. Exported, so counters share the rule instead of re-deriving it -- re-derivation is what caused this. Route _status_data._day_status through it too. It had re-derived the rule as "verified + (1 if has_manual else 0)", which disagreed with count_weekly_workouts on any day with two manual workouts; since that value feeds the status view's weekly_minimum_met explanation, the window could explain a decision the locker never made. Two existing fixtures reached their counts using two same-day phone_verified entries -- the double-count itself. They now use a gym session plus a run, which is two real workouts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
"""Tests that the StrongLifts dedup does not collapse unrelated workouts.
|
|
|
|
Split out of test_weekly_check_dedup.py for the 250-line cap; the shared
|
|
fixture helpers stay in that module and are imported here.
|
|
|
|
These are the guard rail on the other side of the 2026-08-29 fix: collapsing
|
|
the two StrongLifts ingestion paths must not turn into "one workout per day",
|
|
which would quietly undercount a genuine double session and lock on a day that
|
|
was actually earned.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._weekly_check import count_weekly_workouts
|
|
from screen_locker.tests.test_weekly_check_dedup import (
|
|
_SATURDAY_2026_08_29,
|
|
_entry,
|
|
_write_log,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
class TestDedupDoesNotOverCollapse:
|
|
"""Only the two StrongLifts ingestion paths share a slot -- nothing else."""
|
|
|
|
def test_two_runs_on_one_day_both_count(self, tmp_path: Path) -> None:
|
|
"""Verified runs are machine-checked, so multiple per day still count."""
|
|
log = _write_log(
|
|
{
|
|
"2026-08-24": [
|
|
_entry(
|
|
"runnerup_verified",
|
|
timestamp="2026-08-24T07:00:00+00:00",
|
|
),
|
|
_entry(
|
|
"runnerup_verified",
|
|
timestamp="2026-08-24T19:00:00+00:00",
|
|
),
|
|
],
|
|
},
|
|
tmp_path / "log.json",
|
|
)
|
|
assert count_weekly_workouts(log, today=_SATURDAY_2026_08_29) == 2
|
|
|
|
def test_manual_and_phone_on_one_day_both_count(self, tmp_path: Path) -> None:
|
|
"""A manual workout is a different session from a StrongLifts one."""
|
|
log = _write_log(
|
|
{
|
|
"2026-08-24": [
|
|
_entry("manual_workout", timestamp="2026-08-24T08:00:00+00:00"),
|
|
_entry("phone_verified", timestamp="2026-08-24T18:00:00+00:00"),
|
|
],
|
|
},
|
|
tmp_path / "log.json",
|
|
)
|
|
assert count_weekly_workouts(log, today=_SATURDAY_2026_08_29) == 2
|
|
|
|
def test_two_manual_workouts_on_one_day_both_count(
|
|
self,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""The manual-workout budget is the limiter, not a per-day collapse."""
|
|
log = _write_log(
|
|
{
|
|
"2026-08-24": [
|
|
_entry("manual_workout", timestamp="2026-08-24T08:00:00+00:00"),
|
|
_entry("manual_workout", timestamp="2026-08-24T18:00:00+00:00"),
|
|
],
|
|
},
|
|
tmp_path / "log.json",
|
|
)
|
|
assert count_weekly_workouts(log, today=_SATURDAY_2026_08_29) == 2
|
|
|
|
def test_lone_phone_verified_still_counts(self, tmp_path: Path) -> None:
|
|
"""A phone session with no PC twin is one workout, not zero."""
|
|
log = _write_log(
|
|
{
|
|
"2026-08-24": [
|
|
_entry("phone_verified", timestamp="2026-08-24T10:00:00+00:00"),
|
|
],
|
|
},
|
|
tmp_path / "log.json",
|
|
)
|
|
assert count_weekly_workouts(log, today=_SATURDAY_2026_08_29) == 1
|
|
|
|
def test_lone_pc_workout_still_counts(self, tmp_path: Path) -> None:
|
|
"""A PC session with no phone twin is one workout, not zero."""
|
|
log = _write_log(
|
|
{
|
|
"2026-08-24": [
|
|
_entry(
|
|
"pc_workout_verified",
|
|
timestamp="2026-08-24T10:00:00+00:00",
|
|
),
|
|
],
|
|
},
|
|
tmp_path / "log.json",
|
|
)
|
|
assert count_weekly_workouts(log, today=_SATURDAY_2026_08_29) == 1
|
|
|
|
def test_relaxed_day_skip_never_counts(self, tmp_path: Path) -> None:
|
|
"""A skip marker is a UI dismissal, not a workout, twin or not."""
|
|
log = _write_log(
|
|
{
|
|
"2026-08-25": [
|
|
_entry("relaxed_day_skip", timestamp="2026-08-25T18:00:00+00:00"),
|
|
],
|
|
},
|
|
tmp_path / "log.json",
|
|
)
|
|
assert count_weekly_workouts(log, today=_SATURDAY_2026_08_29) == 0
|