mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 18:07:07 +01:00
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
113 lines
3.9 KiB
Python
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
|