diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index 660de07..bf158cf 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -1,5 +1,6 @@ import asyncio import copy +import datetime import logging import discord from discord import SelectOption @@ -4243,6 +4244,34 @@ async def get_game_summary_embed( return game_embed +async def _trigger_variant_renders(tier_ups: list) -> None: + """Fire-and-forget: hit card render URLs to trigger S3 upload for new variants. + + Each tier-up with a variant_created value gets a GET request to the card + render endpoint, which triggers Playwright render + S3 upload as a side effect. + Failures are logged but never raised. + """ + today = datetime.date.today().isoformat() + for tier_up in tier_ups: + variant = tier_up.get("variant_created") + if not variant: + continue + player_id = tier_up["player_id"] + track = tier_up.get("track_name", "Batter") + card_type = "pitching" if track.lower() == "pitcher" else "batting" + try: + await db_get( + f"players/{player_id}/{card_type}card/{today}/{variant}", + none_okay=True, + ) + except Exception: + logger.warning( + "Failed to trigger variant render for player %d variant %d (non-fatal)", + player_id, + variant, + ) + + async def complete_game( session: Session, interaction: discord.Interaction, @@ -4353,6 +4382,7 @@ async def complete_game( if evo_result and evo_result.get("tier_ups"): for tier_up in evo_result["tier_ups"]: await notify_tier_completion(interaction.channel, tier_up) + await _trigger_variant_renders(evo_result["tier_ups"]) except Exception as e: logger.warning(f"Post-game refractor processing failed (non-fatal): {e}") diff --git a/tests/test_post_game_render.py b/tests/test_post_game_render.py new file mode 100644 index 0000000..2470dfe --- /dev/null +++ b/tests/test_post_game_render.py @@ -0,0 +1,65 @@ +"""Tests for post-game refractor card render trigger.""" + +from unittest.mock import AsyncMock, patch + +import pytest + +from command_logic.logic_gameplay import _trigger_variant_renders + + +class TestTriggerVariantRenders: + """Fire-and-forget card render calls after tier-ups.""" + + @pytest.mark.asyncio + async def test_calls_render_url_for_each_tier_up(self): + """Each tier-up with variant_created triggers a card render GET request.""" + tier_ups = [ + {"player_id": 100, "variant_created": 7, "track_name": "Batter"}, + {"player_id": 200, "variant_created": 3, "track_name": "Pitcher"}, + ] + + with patch( + "command_logic.logic_gameplay.db_get", new_callable=AsyncMock + ) as mock_get: + mock_get.return_value = None + await _trigger_variant_renders(tier_ups) + + assert mock_get.call_count == 2 + call_args_list = [call.args[0] for call in mock_get.call_args_list] + assert any("100" in url and "7" in url for url in call_args_list) + assert any("200" in url and "3" in url for url in call_args_list) + + @pytest.mark.asyncio + async def test_skips_tier_ups_without_variant(self): + """Tier-ups without variant_created are skipped.""" + tier_ups = [ + {"player_id": 100, "track_name": "Batter"}, + ] + + with patch( + "command_logic.logic_gameplay.db_get", new_callable=AsyncMock + ) as mock_get: + await _trigger_variant_renders(tier_ups) + mock_get.assert_not_called() + + @pytest.mark.asyncio + async def test_api_failure_does_not_raise(self): + """Render trigger failures are swallowed — fire-and-forget.""" + tier_ups = [ + {"player_id": 100, "variant_created": 7, "track_name": "Batter"}, + ] + + with patch( + "command_logic.logic_gameplay.db_get", new_callable=AsyncMock + ) as mock_get: + mock_get.side_effect = Exception("API down") + await _trigger_variant_renders(tier_ups) + + @pytest.mark.asyncio + async def test_empty_tier_ups_is_noop(self): + """Empty tier_ups list does nothing.""" + with patch( + "command_logic.logic_gameplay.db_get", new_callable=AsyncMock + ) as mock_get: + await _trigger_variant_renders([]) + mock_get.assert_not_called()