steam-backlog-enforcer/steam_backlog_enforcer/tests/test_gaming_days.py
Krzysztof kuhy Rudnicki a09f08701a playtime: drop the Fri-Mon carry-over; log budget on change only
A Sunday ran past the 6.0h the i3 bar showed: 2h47m of Saturday's
leftover was carried into the budget the cutoff used, and the 3-second
"Gaming budget" journal line omitted it. Unspent time is now dropped at
06:00 every day; load_state takes a legacy ledger's share back out of
today's high-water mark. The budget line logs only on change (was 25k
lines/day), fired warnings are logged, and budget.jsonl records carry
budget_seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015PrwCnQouKTF4EUMENMxnG
2026-10-18 21:37:29 +02:00

113 lines
3.9 KiB
Python

"""Tests for _gaming_days: the day boundary, the held budget, no carry-over."""
from __future__ import annotations
from dataclasses import fields
from datetime import datetime, timedelta, timezone
import json
from unittest.mock import patch
import pytest
from steam_backlog_enforcer._gaming_days import held_today
from steam_backlog_enforcer._playtime_budget import roll_over
from steam_backlog_enforcer._playtime_state import (
PlaytimeRules,
PlaytimeState,
load_state,
rules_for,
state_path,
)
from steam_backlog_enforcer.config import Config
HOUR = 3600.0
LOCAL = timezone(timedelta(hours=2))
# 2026-10-01 is a Thursday.
THU, FRI, SAT, SUN, MON, TUE = (f"2026-10-{d:02d}" for d in range(1, 7))
class TestNoCarryOver:
"""Unspent time never moves to a later day (carry-over dropped 2026-10-18)."""
def test_a_saturday_leftover_is_not_passed_on(self) -> None:
sun = roll_over(
PlaytimeState(day_key=SAT, seconds=HOUR, budget_seconds=8 * HOUR),
day_key=SUN,
)
assert sun == PlaytimeState(day_key=SUN)
assert "carry" not in vars(sun)
def test_rules_have_no_carry(self) -> None:
assert "carry_seconds" not in {f.name for f in fields(PlaytimeRules)}
def test_a_legacy_carry_is_taken_out_of_todays_high_water(self) -> None:
# The live file on 2026-10-18 held 6.0h earned + 9994s carried into the
# day's high-water mark; held_today would otherwise keep paying it out.
legacy = {
"schema_version": 1,
"day_key": SUN,
"seconds": 6 * HOUR,
"budget_seconds": 6 * HOUR + 9994.0,
"carry": {SUN: 9994.0, MON: 9994.0},
}
state_path(demo=False).write_text(json.dumps(legacy), encoding="utf-8")
stored = load_state(demo=False)
assert stored is not None
assert stored.budget_seconds == 6 * HOUR
assert "carry" not in vars(stored)
@pytest.mark.parametrize(
"carry", [{MON: 9994.0}, {SUN: "9994"}, {SUN: 0.0}, [9994.0]]
)
def test_a_ledger_owing_today_nothing_leaves_the_budget(
self, carry: object
) -> None:
legacy = {
"schema_version": 1,
"day_key": SUN,
"budget_seconds": 6 * HOUR,
"carry": carry,
}
state_path(demo=False).write_text(json.dumps(legacy), encoding="utf-8")
stored = load_state(demo=False)
assert stored is not None
assert stored.budget_seconds == 6 * HOUR
class TestHeldToday:
"""The day's high-water budget, and only for the day it was recorded."""
NOW = datetime(2026, 10, 5, 0, 30, tzinfo=LOCAL) # still gaming day SUN
def test_no_state_holds_nothing(self) -> None:
assert held_today(None, self.NOW) == 0.0
def test_same_gaming_day_returns_the_high_water(self) -> None:
stored = PlaytimeState(day_key=SUN, budget_seconds=8 * HOUR)
assert held_today(stored, self.NOW) == 8 * HOUR
def test_yesterdays_record_holds_nothing(self) -> None:
stored = PlaytimeState(day_key=SAT, budget_seconds=8 * HOUR)
assert held_today(stored, self.NOW) == 0.0
class TestRulesHold:
"""rules_for never prices the day below what it already granted."""
def test_midnight_reset_is_held_and_says_so(self) -> None:
with patch(
"steam_backlog_enforcer._playtime_state.held_today",
return_value=10 * HOUR,
):
rules = rules_for(Config(), demo=False)
assert rules.budget_seconds == 10 * HOUR
assert rules.budget_reason.endswith("; held at 10.0h (earned earlier today)")
def test_a_higher_live_answer_wins_silently(self) -> None:
with patch(
"steam_backlog_enforcer._playtime_state.held_today", return_value=HOUR
):
rules = rules_for(Config(), demo=False)
assert rules.budget_seconds > HOUR
assert "held at" not in rules.budget_reason