From f13b215324e5836d67cc280f449bc9f5dee1ffda Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 20 Mar 2026 13:34:02 -0500 Subject: [PATCH] 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) --- commands/gameplay/scorecard_tracker.py | 12 ++++++------ commands/voice/cleanup_service.py | 18 +++++++++++------- tests/test_scorebug_bugs.py | 14 ++++++++------ 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/commands/gameplay/scorecard_tracker.py b/commands/gameplay/scorecard_tracker.py index b5fd6db..353aad8 100644 --- a/commands/gameplay/scorecard_tracker.py +++ b/commands/gameplay/scorecard_tracker.py @@ -61,7 +61,7 @@ class ScorecardTracker: except Exception as e: logger.error(f"Failed to save scorecard data: {e}") - def publish_scorecard( + async def publish_scorecard( self, text_channel_id: int, sheet_url: str, publisher_id: int ) -> None: """ @@ -82,7 +82,7 @@ class ScorecardTracker: self.save_data() logger.info(f"Published scorecard to channel {text_channel_id}: {sheet_url}") - def unpublish_scorecard(self, text_channel_id: int) -> bool: + async def unpublish_scorecard(self, text_channel_id: int) -> bool: """ Remove scorecard from a text channel. @@ -103,7 +103,7 @@ class ScorecardTracker: return False - def get_scorecard(self, text_channel_id: int) -> Optional[str]: + async def get_scorecard(self, text_channel_id: int) -> Optional[str]: """ Get scorecard URL for a text channel. @@ -118,7 +118,7 @@ class ScorecardTracker: scorecard_data = scorecards.get(str(text_channel_id)) return scorecard_data["sheet_url"] if scorecard_data else None - def get_all_scorecards(self) -> List[Tuple[int, str]]: + async def get_all_scorecards(self) -> List[Tuple[int, str]]: """ Get all published scorecards. @@ -132,7 +132,7 @@ class ScorecardTracker: for channel_id, data in scorecards.items() ] - def update_timestamp(self, text_channel_id: int) -> None: + async def update_timestamp(self, text_channel_id: int) -> None: """ Update the last_updated timestamp for a scorecard. @@ -146,7 +146,7 @@ class ScorecardTracker: scorecards[channel_key]["last_updated"] = datetime.now(UTC).isoformat() self.save_data() - def cleanup_stale_entries(self, valid_channel_ids: List[int]) -> int: + async def cleanup_stale_entries(self, valid_channel_ids: List[int]) -> int: """ Remove tracking entries for text channels that no longer exist. diff --git a/commands/voice/cleanup_service.py b/commands/voice/cleanup_service.py index c7343d9..b88d55c 100644 --- a/commands/voice/cleanup_service.py +++ b/commands/voice/cleanup_service.py @@ -128,7 +128,7 @@ class VoiceChannelCleanupService: if channel_data and channel_data.get("text_channel_id"): try: text_channel_id_int = int(channel_data["text_channel_id"]) - was_unpublished = self.scorecard_tracker.unpublish_scorecard( + was_unpublished = await self.scorecard_tracker.unpublish_scorecard( text_channel_id_int ) if was_unpublished: @@ -218,8 +218,10 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard( - text_channel_id_int + was_unpublished = ( + await self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) ) if was_unpublished: self.logger.info( @@ -244,8 +246,10 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard( - text_channel_id_int + was_unpublished = ( + await self.scorecard_tracker.unpublish_scorecard( + text_channel_id_int + ) ) if was_unpublished: self.logger.info( @@ -330,7 +334,7 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard( + was_unpublished = await self.scorecard_tracker.unpublish_scorecard( text_channel_id_int ) if was_unpublished: @@ -358,7 +362,7 @@ class VoiceChannelCleanupService: if text_channel_id: try: text_channel_id_int = int(text_channel_id) - was_unpublished = self.scorecard_tracker.unpublish_scorecard( + was_unpublished = await self.scorecard_tracker.unpublish_scorecard( text_channel_id_int ) if was_unpublished: diff --git a/tests/test_scorebug_bugs.py b/tests/test_scorebug_bugs.py index c36023b..385fe65 100644 --- a/tests/test_scorebug_bugs.py +++ b/tests/test_scorebug_bugs.py @@ -24,7 +24,8 @@ from utils.scorebug_helpers import create_scorebug_embed, create_team_progress_b class TestScorecardTrackerFreshReads: """Tests that ScorecardTracker reads fresh data from disk (fix for #40).""" - def test_get_all_scorecards_reads_fresh_data(self, tmp_path): + @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 @@ -34,7 +35,7 @@ class TestScorecardTrackerFreshReads: data_file.write_text(json.dumps({"scorecards": {}})) tracker = ScorecardTracker(data_file=str(data_file)) - assert tracker.get_all_scorecards() == [] + assert await tracker.get_all_scorecards() == [] # Another process writes a scorecard to the same file new_data = { @@ -51,17 +52,18 @@ class TestScorecardTrackerFreshReads: data_file.write_text(json.dumps(new_data)) # Should see the new scorecard without restart - result = tracker.get_all_scorecards() + result = await 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): + @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 tracker.get_scorecard(222) is None + assert await tracker.get_scorecard(222) is None # Another process writes a scorecard new_data = { @@ -79,7 +81,7 @@ class TestScorecardTrackerFreshReads: # Should see the new scorecard assert ( - tracker.get_scorecard(222) + await tracker.get_scorecard(222) == "https://docs.google.com/spreadsheets/d/xyz789" )