major-domo-v2/tests/test_scorebug_bugs.py
Cal Corum f13b215324 hotfix: make ScorecardTracker methods async to match await callers
PR #106 added await to scorecard_tracker calls but the tracker
methods were still sync, causing TypeError in production:
- /scorebug: "object NoneType can't be used in 'await' expression"
- live_scorebug_tracker: "object list can't be used in 'await' expression"

Also fixes 5 missing awaits in cleanup_service.py and updates tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 13:34:02 -05:00

179 lines
6.6 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)."""
@pytest.mark.asyncio
async 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 await 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 = await tracker.get_all_scorecards()
assert len(result) == 1
assert result[0] == (111, "https://docs.google.com/spreadsheets/d/abc123")
@pytest.mark.asyncio
async 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 await 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 (
await 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