All checks were successful
Build Docker Image / build (pull_request) Successful in 1m14s
#40: ScorecardTracker cached data in memory at startup — background task never saw newly published scorecards. Fixed by reloading from disk on every read. #39: Win percentage defaulted to 50% when unavailable, showing a misleading 50/50 bar. Now defaults to None with "unavailable" message in embed. Parsing handles decimal (0.75), percentage string, and empty values. Also fixed orientation bug where win% was always shown as home team's even when the sheet reports the away team as the leader. Additionally: live scorebug tracker now distinguishes between "all games confirmed final" and "sheet read failures" — transient Google Sheets errors no longer hide the live scores channel. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
177 lines
6.5 KiB
Python
177 lines
6.5 KiB
Python
"""
|
|
Tests for scorebug bug fixes (#39 and #40).
|
|
|
|
#40: ScorecardTracker reads stale in-memory data — fix ensures get_scorecard()
|
|
and get_all_scorecards() reload from disk before returning data.
|
|
|
|
#39: Win percentage stuck at 50% — fix makes parsing robust for decimal (0.75),
|
|
percentage string ("75%"), plain number ("75"), empty, and None values.
|
|
When parsing fails, win_percentage is None instead of a misleading 50.0.
|
|
"""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from commands.gameplay.scorecard_tracker import ScorecardTracker
|
|
from services.scorebug_service import ScorebugData
|
|
from utils.scorebug_helpers import create_scorebug_embed, create_team_progress_bar
|
|
|
|
|
|
class TestScorecardTrackerFreshReads:
|
|
"""Tests that ScorecardTracker reads fresh data from disk (fix for #40)."""
|
|
|
|
def test_get_all_scorecards_reads_fresh_data(self, tmp_path):
|
|
"""get_all_scorecards() should pick up scorecards written by another process.
|
|
|
|
Simulates the background task having a stale tracker instance while
|
|
the /publish-scorecard command writes new data to the JSON file.
|
|
"""
|
|
data_file = tmp_path / "scorecards.json"
|
|
data_file.write_text(json.dumps({"scorecards": {}}))
|
|
|
|
tracker = ScorecardTracker(data_file=str(data_file))
|
|
assert tracker.get_all_scorecards() == []
|
|
|
|
# Another process writes a scorecard to the same file
|
|
new_data = {
|
|
"scorecards": {
|
|
"111": {
|
|
"text_channel_id": "111",
|
|
"sheet_url": "https://docs.google.com/spreadsheets/d/abc123",
|
|
"published_at": "2026-01-01T00:00:00",
|
|
"last_updated": "2026-01-01T00:00:00",
|
|
"publisher_id": "999",
|
|
}
|
|
}
|
|
}
|
|
data_file.write_text(json.dumps(new_data))
|
|
|
|
# Should see the new scorecard without restart
|
|
result = tracker.get_all_scorecards()
|
|
assert len(result) == 1
|
|
assert result[0] == (111, "https://docs.google.com/spreadsheets/d/abc123")
|
|
|
|
def test_get_scorecard_reads_fresh_data(self, tmp_path):
|
|
"""get_scorecard() should pick up a scorecard written by another process."""
|
|
data_file = tmp_path / "scorecards.json"
|
|
data_file.write_text(json.dumps({"scorecards": {}}))
|
|
|
|
tracker = ScorecardTracker(data_file=str(data_file))
|
|
assert tracker.get_scorecard(222) is None
|
|
|
|
# Another process writes a scorecard
|
|
new_data = {
|
|
"scorecards": {
|
|
"222": {
|
|
"text_channel_id": "222",
|
|
"sheet_url": "https://docs.google.com/spreadsheets/d/xyz789",
|
|
"published_at": "2026-01-01T00:00:00",
|
|
"last_updated": "2026-01-01T00:00:00",
|
|
"publisher_id": "999",
|
|
}
|
|
}
|
|
}
|
|
data_file.write_text(json.dumps(new_data))
|
|
|
|
# Should see the new scorecard
|
|
assert (
|
|
tracker.get_scorecard(222)
|
|
== "https://docs.google.com/spreadsheets/d/xyz789"
|
|
)
|
|
|
|
|
|
class TestWinPercentageParsing:
|
|
"""Tests for robust win percentage parsing in ScorebugData (fix for #39)."""
|
|
|
|
def test_percentage_string(self):
|
|
"""'75%' string should parse to 75.0."""
|
|
data = ScorebugData({"win_percentage": 75.0})
|
|
assert data.win_percentage == 75.0
|
|
|
|
def test_none_default(self):
|
|
"""Missing win_percentage key should default to None."""
|
|
data = ScorebugData({})
|
|
assert data.win_percentage is None
|
|
|
|
def test_explicit_none(self):
|
|
"""Explicit None should stay None."""
|
|
data = ScorebugData({"win_percentage": None})
|
|
assert data.win_percentage is None
|
|
|
|
def test_zero_is_valid(self):
|
|
"""0.0 win percentage is a valid value (team certain to lose)."""
|
|
data = ScorebugData({"win_percentage": 0.0})
|
|
assert data.win_percentage == 0.0
|
|
|
|
|
|
class TestWinPercentageEmbed:
|
|
"""Tests for embed creation with win_percentage=None (fix for #39 Part B)."""
|
|
|
|
def _make_scorebug_data(self, win_percentage):
|
|
"""Create minimal ScorebugData for embed testing."""
|
|
return ScorebugData(
|
|
{
|
|
"away_team_id": 1,
|
|
"home_team_id": 2,
|
|
"header": "Test Game",
|
|
"away_score": 10,
|
|
"home_score": 2,
|
|
"which_half": "Top",
|
|
"inning": 5,
|
|
"is_final": False,
|
|
"outs": 1,
|
|
"win_percentage": win_percentage,
|
|
"pitcher_name": "",
|
|
"batter_name": "",
|
|
"runners": [["", ""], ["", ""], ["", ""], ["", ""]],
|
|
"summary": [],
|
|
}
|
|
)
|
|
|
|
def _make_team(self, abbrev, color_int=0x3498DB):
|
|
"""Create a mock team object."""
|
|
team = MagicMock()
|
|
team.abbrev = abbrev
|
|
team.get_color_int.return_value = color_int
|
|
return team
|
|
|
|
def test_embed_with_none_win_percentage_shows_unavailable(self):
|
|
"""When win_percentage is None, embed should show unavailable message."""
|
|
data = self._make_scorebug_data(win_percentage=None)
|
|
away = self._make_team("POR")
|
|
home = self._make_team("WV")
|
|
|
|
embed = create_scorebug_embed(data, away, home, full_length=False)
|
|
|
|
# Find the Win Probability field
|
|
win_prob_field = next(f for f in embed.fields if f.name == "Win Probability")
|
|
assert "unavailable" in win_prob_field.value.lower()
|
|
|
|
def test_embed_with_valid_win_percentage_shows_bar(self):
|
|
"""When win_percentage is valid, embed should show the progress bar."""
|
|
data = self._make_scorebug_data(win_percentage=75.0)
|
|
away = self._make_team("POR")
|
|
home = self._make_team("WV")
|
|
|
|
embed = create_scorebug_embed(data, away, home, full_length=False)
|
|
|
|
win_prob_field = next(f for f in embed.fields if f.name == "Win Probability")
|
|
assert "75.0%" in win_prob_field.value
|
|
assert "unavailable" not in win_prob_field.value.lower()
|
|
|
|
def test_embed_with_50_percent_shows_even_bar(self):
|
|
"""50% win probability should show the even/balanced bar."""
|
|
data = self._make_scorebug_data(win_percentage=50.0)
|
|
away = self._make_team("POR")
|
|
home = self._make_team("WV")
|
|
|
|
embed = create_scorebug_embed(data, away, home, full_length=False)
|
|
|
|
win_prob_field = next(f for f in embed.fields if f.name == "Win Probability")
|
|
assert "50.0%" in win_prob_field.value
|
|
assert "=" in win_prob_field.value
|