screen-locker/screen_locker/tests/test_weekly_check_dedup_part2.py
Krzysztof kuhy Rudnicki 8ac7af158d Count one StrongLifts session once, not once per ingestion path
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
2026-08-29 22:23:17 +02:00

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