fix(sports): harden the live game-over check and font/log init

Follow-up review findings on the promoted base-class methods.

_is_game_really_over:
- `period` present-but-None raised TypeError on `None >= FINAL_PERIOD`,
  taking down the whole live-update pass (_detect_stale_games has no
  try/except). Same failure shape as the null `period_text` already fixed.
- An expired clock spelled "00:00" normalizes to "0000", which matched
  none of the hand-listed literals, so a finished game with a two-digit
  minute clock stayed on the scoreboard forever. Compare numerically.

SportsCore:
- _load_fonts kept the cwd-relative "assets/fonts/..." literals the
  _font_root() seam exists to remove, so every scoreboard font degraded
  to PIL's default face outside the install root.
- _should_log read self._last_warning_time unguarded while only an
  unrelated method initialized it lazily; the first warning of a run
  raised AttributeError. Initialize it in __init__.

Also documents that game_update_timestamps is written by subclasses, not
by the base class, so the staleness branch is inert until B5 adoption.

14 new tests. Gates: 463 core unit, 60 plugin safety.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4
This commit is contained in:
Claude
2026-08-01 16:45:04 +00:00
parent 2486bdb249
commit 98a7c769ec
5 changed files with 102 additions and 13 deletions
+21 -1
View File
@@ -254,12 +254,32 @@ class TestIsGameReallyOver:
manager = build_manager(_LiveHarness)
assert manager._is_game_really_over(game(clock="12:00", period=2)) is False
@pytest.mark.parametrize("clock", ["0:00", ":00", "00", "000", " 0:00 "])
@pytest.mark.parametrize(
"clock", ["0:00", ":00", "00", "000", " 0:00 ", "00:00", "0", "0000"])
def test_expired_clock_at_final_period_is_over(self, build_manager, clock):
"""Every spelling of a zeroed clock counts, not a hand-listed few.
"00:00" is the one that motivated comparing numerically: it normalizes
to "0000", which matched none of the literals the plugin copies listed,
so a two-digit-minute expired clock kept the game on screen forever.
"""
manager = build_manager(_LiveHarness)
assert manager._is_game_really_over(
game(clock=clock, period=4, period_text="Q4")) is True
def test_none_period_at_expired_clock_does_not_raise(self, build_manager):
"""`period` present-but-None: `None >= FINAL_PERIOD` is a TypeError, and
`_detect_stale_games` has no try/except — the same failure shape as the
`period_text` case above."""
manager = build_manager(_LiveHarness)
assert manager._is_game_really_over(
game(clock="0:00", period=None, period_text="Q4")) is False
def test_none_period_with_running_clock_does_not_raise(self, build_manager):
manager = build_manager(_LiveHarness)
assert manager._is_game_really_over(
game(clock="8:12", period=None, period_text="Q2")) is False
def test_expired_clock_after_final_period_is_over(self, build_manager):
manager = build_manager(_LiveHarness)
assert manager._is_game_really_over(