fix: wire WP-14 tier-up notification embeds into post-game hook
All checks were successful
Ruff Lint / lint (pull_request) Successful in 25s

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) <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-24 23:43:05 -05:00
parent 8c0c2eb21a
commit 571a86fe7e
3 changed files with 36 additions and 75 deletions

View File

@ -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}")

View File

@ -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)
# 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

View File

@ -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
# ---------------------------------------------------------------------------