From 571a86fe7ebbaa90c75ded2154f8925107c5f694 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Tue, 24 Mar 2026 23:43:05 -0500 Subject: [PATCH] fix: wire WP-14 tier-up notification embeds into post-game hook Replace the logging-only stub in logic_gameplay.py with the real notify_tier_completion from helpers/refractor_notifs.py. Tier-up events now send Discord embeds instead of just logging. - Import notify_tier_completion from helpers.refractor_notifs - Remove 16-line stub function and redundant inline logging - Update tests: verify real embed-sending behavior, replace bug-documenting T1-5 diagnostic with shape validation guards Co-Authored-By: Claude Opus 4.6 (1M context) --- command_logic/logic_gameplay.py | 25 +-------------- tests/test_complete_game_hook.py | 34 ++++++++++++--------- tests/test_refractor_notifs.py | 52 ++++++++++---------------------- 3 files changed, 36 insertions(+), 75 deletions(-) diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index d679cc4..660de07 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -23,6 +23,7 @@ from helpers import ( position_name_to_abbrev, team_role, ) +from helpers.refractor_notifs import notify_tier_completion from in_game.ai_manager import get_starting_lineup from in_game.game_helpers import PUBLIC_FIELDS_CATEGORY_NAME, legal_check from in_game.gameplay_models import ( @@ -4242,24 +4243,6 @@ async def get_game_summary_embed( return game_embed -async def notify_tier_completion(channel: discord.TextChannel, tier_up: dict) -> None: - """Stub for WP-14: log evolution tier-up events. - - WP-14 will replace this with a full Discord embed notification. For now we - only log the event so that the WP-13 hook has a callable target and the - tier-up data is visible in the application log. - - Args: - channel: The Discord channel where the game was played. - tier_up: Dict from the evolution API, expected to contain at minimum - 'player_id', 'old_tier', and 'new_tier' keys. - """ - logger.info( - f"[WP-14 stub] notify_tier_completion called for channel={channel.id if channel else 'N/A'} " - f"tier_up={tier_up}" - ) - - async def complete_game( session: Session, interaction: discord.Interaction, @@ -4369,12 +4352,6 @@ async def complete_game( evo_result = await db_post(f"refractor/evaluate-game/{db_game['id']}") if evo_result and evo_result.get("tier_ups"): for tier_up in evo_result["tier_ups"]: - # WP-14 will implement full Discord notification; stub for now - logger.info( - f"Refractor tier-up for player {tier_up.get('player_id')}: " - f"{tier_up.get('old_tier')} -> {tier_up.get('new_tier')} " - f"(game {db_game['id']})" - ) await notify_tier_completion(interaction.channel, tier_up) except Exception as e: logger.warning(f"Post-game refractor processing failed (non-fatal): {e}") diff --git a/tests/test_complete_game_hook.py b/tests/test_complete_game_hook.py index b04b689..eceec82 100644 --- a/tests/test_complete_game_hook.py +++ b/tests/test_complete_game_hook.py @@ -14,8 +14,6 @@ Key design constraints being tested: notify_tier_completion so WP-14 can present them to the player. """ -import asyncio -import logging import pytest from unittest.mock import AsyncMock, MagicMock, call, patch @@ -179,23 +177,29 @@ async def test_hook_no_tier_ups_does_not_call_notify(): @pytest.mark.asyncio -async def test_notify_tier_completion_stub_logs_and_does_not_raise(caplog): +async def test_notify_tier_completion_sends_embed_and_does_not_raise(): """ - The WP-14 stub must log the event and return cleanly. + notify_tier_completion sends a Discord embed and does not raise. - Verifies the contract that WP-14 can rely on: the function accepts - (channel, tier_up) and does not raise, so the hook's for-loop is safe. + Now that WP-14 is wired, the function imported via logic_gameplay is the + real embed-sending implementation from helpers.refractor_notifs. """ from command_logic.logic_gameplay import notify_tier_completion - channel = _make_channel(channel_id=123) - tier_up = {"player_id": 77, "old_tier": 0, "new_tier": 1} + channel = AsyncMock() + # Full API response shape — the evaluate-game endpoint returns all these keys + tier_up = { + "player_id": 77, + "team_id": 1, + "player_name": "Mike Trout", + "old_tier": 0, + "new_tier": 1, + "current_value": 45.0, + "track_name": "Batter Track", + } - with caplog.at_level(logging.INFO): - await notify_tier_completion(channel, tier_up) + await notify_tier_completion(channel, tier_up) - # At minimum one log message should reference the channel or tier_up data - assert any( - "notify_tier_completion" in rec.message or "77" in rec.message - for rec in caplog.records - ) + channel.send.assert_called_once() + embed = channel.send.call_args.kwargs["embed"] + assert "Mike Trout" in embed.description diff --git a/tests/test_refractor_notifs.py b/tests/test_refractor_notifs.py index 688ff13..20bf2ac 100644 --- a/tests/test_refractor_notifs.py +++ b/tests/test_refractor_notifs.py @@ -260,63 +260,43 @@ class TestNotifyTierCompletion: # --------------------------------------------------------------------------- -# T1-5: tier_up dict shape mismatch — WP-14 integration blocker +# T1-5: tier_up dict shape validation # --------------------------------------------------------------------------- -class TestTierUpDictShapeMismatch: +class TestTierUpDictShapeValidation: """ - T1-5: Expose the latent integration bug where the post-game hook passes a - minimal tier_up dict (only player_id, old_tier, new_tier) but - build_tier_up_embed expects player_name, old_tier, new_tier, track_name, - and current_value. + T1-5: Verify build_tier_up_embed handles valid API shapes correctly and + rejects malformed input. - Why this matters: the hook test (test_complete_game_hook.py) confirms the - plumbing forwards tier_up dicts from the API response to notify_tier_completion. - However, the real API response may omit player_name/track_name. If - build_tier_up_embed does a bare dict access (tier_up["player_name"]) without - a fallback, it will raise KeyError in production. This test documents the - current behaviour (crash vs. graceful degradation) so WP-14 implementers - know to either harden the embed builder or ensure the API always returns - the full shape. + The evaluate-game API endpoint returns the full shape (player_name, + old_tier, new_tier, track_name, current_value). These tests guard the + contract between the API response and the embed builder. """ - def test_minimal_stub_shape_raises_key_error(self): + def test_empty_dict_raises_key_error(self): """ - Calling build_tier_up_embed with only {player_id, old_tier, new_tier} - (the minimal shape used by the post-game hook stub) raises KeyError - because player_name and track_name are accessed via bare dict lookup. - - This is the latent bug: the hook passes stub-shaped dicts but the embed - builder expects the full notification shape. WP-14 must ensure either - (a) the API returns the full shape or (b) build_tier_up_embed degrades - gracefully with .get() fallbacks. + An empty dict must raise KeyError — guards against callers passing + unrelated or completely malformed data. """ - minimal_stub = { - "player_id": 101, - "old_tier": 1, - "new_tier": 2, - } - # Document that this raises — it's the bug we're exposing, not a passing test. with pytest.raises(KeyError): - build_tier_up_embed(minimal_stub) + build_tier_up_embed({}) - def test_full_shape_does_not_raise(self): + def test_full_api_shape_builds_embed(self): """ - Confirm that supplying the full expected shape (player_name, old_tier, - new_tier, track_name, current_value) does NOT raise, establishing the - correct contract for callers. + The full shape returned by the evaluate-game endpoint builds a valid + embed without error. """ full_shape = make_tier_up( player_name="Mike Trout", old_tier=1, new_tier=2, - track_name="Batter", + track_name="Batter Track", current_value=150, ) - # Must not raise embed = build_tier_up_embed(full_shape) assert embed is not None + assert "Mike Trout" in embed.description # ---------------------------------------------------------------------------