93 lines
3.8 KiB
Python
93 lines
3.8 KiB
Python
|
|
"""Integration: manual_activities flow through into the Banister PMC.
|
||
|
|
|
||
|
|
The contract is that `daily_training_load_series(..., include_manual=True)`
|
||
|
|
unions manual rows into the same Series the PMC consumes, so adding a
|
||
|
|
strength session lifts CTL on that day and the days following — same
|
||
|
|
recursion, no special case.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
from openrun.model import banister, daily_training_load_series
|
||
|
|
|
||
|
|
|
||
|
|
def _add_activity(conn, aid: int, when: str, atype: str, training_load: float) -> None:
|
||
|
|
conn.execute(
|
||
|
|
"""INSERT INTO activities
|
||
|
|
(activity_id, start_time_local, activity_type, training_load, raw, fetched_at)
|
||
|
|
VALUES (?, ?, ?, ?, '{}', 'now')""",
|
||
|
|
(aid, when, atype, training_load),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _add_manual(conn, when: str, atype: str, training_load: float) -> None:
|
||
|
|
conn.execute(
|
||
|
|
"""INSERT INTO manual_activities
|
||
|
|
(activity_date, activity_type, training_load, source, imported_at)
|
||
|
|
VALUES (?, ?, ?, 'test', datetime('now'))""",
|
||
|
|
(when, atype, training_load),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_include_manual_increases_daily_sum(tmp_conn) -> None:
|
||
|
|
_add_activity(tmp_conn, aid=1, when="2026-05-01 06:00:00", atype="running", training_load=50.0)
|
||
|
|
_add_manual(tmp_conn, when="2026-05-01", atype="running", training_load=30.0)
|
||
|
|
tmp_conn.commit()
|
||
|
|
|
||
|
|
without = daily_training_load_series(tmp_conn)
|
||
|
|
with_manual = daily_training_load_series(tmp_conn, include_manual=True)
|
||
|
|
|
||
|
|
assert without.iloc[0] == 50.0
|
||
|
|
assert with_manual.iloc[0] == 80.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_manual_only_day_appears_when_included(tmp_conn) -> None:
|
||
|
|
"""A day with only a manual row is invisible without the flag and appears with it."""
|
||
|
|
_add_manual(tmp_conn, when="2026-05-02", atype="running", training_load=25.0)
|
||
|
|
tmp_conn.commit()
|
||
|
|
|
||
|
|
without = daily_training_load_series(tmp_conn)
|
||
|
|
with_manual = daily_training_load_series(tmp_conn, include_manual=True)
|
||
|
|
assert without.empty
|
||
|
|
assert with_manual.loc[pd.Timestamp("2026-05-02")] == 25.0
|
||
|
|
|
||
|
|
|
||
|
|
def test_manual_load_lifts_banister_ctl_monotonically(tmp_conn) -> None:
|
||
|
|
"""For two otherwise-identical worlds, the one with extra manual TL must
|
||
|
|
show strictly higher CTL on the day the manual session was logged and on
|
||
|
|
every following day (the EWMA carries forward)."""
|
||
|
|
_add_activity(tmp_conn, aid=1, when="2026-05-01 06:00:00", atype="running", training_load=50.0)
|
||
|
|
_add_activity(tmp_conn, aid=2, when="2026-05-03 06:00:00", atype="running", training_load=60.0)
|
||
|
|
tmp_conn.commit()
|
||
|
|
|
||
|
|
base = banister(daily_training_load_series(tmp_conn))
|
||
|
|
|
||
|
|
_add_manual(tmp_conn, when="2026-05-02", atype="running", training_load=40.0)
|
||
|
|
tmp_conn.commit()
|
||
|
|
with_manual = banister(daily_training_load_series(tmp_conn, include_manual=True))
|
||
|
|
|
||
|
|
# 2026-05-01 is unchanged (manual is in the future at that point).
|
||
|
|
assert with_manual.loc["2026-05-01", "CTL"] == base.loc["2026-05-01", "CTL"]
|
||
|
|
# 2026-05-02 onward is strictly larger.
|
||
|
|
for d in ("2026-05-02", "2026-05-03"):
|
||
|
|
assert with_manual.loc[d, "CTL"] > base.loc[d, "CTL"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_activity_type_filter_excludes_other_manual_rows(tmp_conn) -> None:
|
||
|
|
"""Manual rows whose type is not in activity_types stay out of the series."""
|
||
|
|
_add_manual(tmp_conn, when="2026-05-02", atype="strength", training_load=100.0)
|
||
|
|
tmp_conn.commit()
|
||
|
|
out = daily_training_load_series(tmp_conn, include_manual=True)
|
||
|
|
assert out.empty
|
||
|
|
|
||
|
|
|
||
|
|
def test_manual_same_day_as_garmin_row_sums(tmp_conn) -> None:
|
||
|
|
"""A manual row and a Garmin row on the same day must combine, not overwrite."""
|
||
|
|
_add_activity(tmp_conn, aid=1, when="2026-05-01 18:00:00", atype="running", training_load=70.0)
|
||
|
|
_add_manual(tmp_conn, when="2026-05-01", atype="running", training_load=30.0)
|
||
|
|
tmp_conn.commit()
|
||
|
|
s = daily_training_load_series(tmp_conn, include_manual=True)
|
||
|
|
assert s.loc[pd.Timestamp("2026-05-01")] == 100.0
|