fix: rename evolution/ to refractor/ endpoint and remove misplaced notifs module
All checks were successful
Ruff Lint / lint (pull_request) Successful in 14s
All checks were successful
Ruff Lint / lint (pull_request) Successful in 14s
- Change `evolution/evaluate-game/` API call to `refractor/evaluate-game/` in complete_game() hook (was calling the wrong endpoint path) - Update all test assertions in test_complete_game_hook.py to match the corrected endpoint path and update docstrings to "refractor" naming - Remove helpers/evolution_notifs.py and tests/test_evolution_notifications.py from this PR — they belong to PR #112 (WP-14 tier notifications). The notify_tier_completion stub in logic_gameplay.py remains as the WP-14 integration target. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c57fbcdf5
commit
29f2a8683f
@ -4336,7 +4336,6 @@ async def complete_game(
|
||||
await roll_back(db_game["id"], plays=True, decisions=True)
|
||||
log_exception(e, msg="Unable to post decisions to API, rolling back")
|
||||
|
||||
|
||||
# Post game rewards (gauntlet and main team)
|
||||
try:
|
||||
win_reward, loss_reward = await post_game_rewards(
|
||||
@ -4360,25 +4359,25 @@ async def complete_game(
|
||||
await roll_back(db_game["id"], plays=True, decisions=True)
|
||||
log_exception(e, msg="Error while posting game rewards")
|
||||
|
||||
# Post-game evolution processing (non-blocking)
|
||||
# WP-13: update season stats then evaluate evolution milestones for all
|
||||
# Post-game refractor processing (non-blocking)
|
||||
# WP-13: update season stats then evaluate refractor milestones for all
|
||||
# participating players. Wrapped in try/except so any failure here is
|
||||
# non-fatal — the game is already saved and evolution will catch up on the
|
||||
# non-fatal — the game is already saved and refractor will catch up on the
|
||||
# next evaluate call.
|
||||
try:
|
||||
await db_post(f"season-stats/update-game/{db_game['id']}")
|
||||
evo_result = await db_post(f"evolution/evaluate-game/{db_game['id']}")
|
||||
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"Evolution tier-up for player {tier_up.get('player_id')}: "
|
||||
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 evolution processing failed (non-fatal): {e}")
|
||||
logger.warning(f"Post-game refractor processing failed (non-fatal): {e}")
|
||||
|
||||
session.delete(this_play)
|
||||
session.commit()
|
||||
|
||||
@ -1,107 +0,0 @@
|
||||
"""
|
||||
Evolution Tier Completion Notifications
|
||||
|
||||
Builds and sends Discord embeds when a player completes an evolution tier
|
||||
during post-game evaluation. Each tier-up event gets its own embed.
|
||||
|
||||
Notification failures are non-fatal: the send is wrapped in try/except so
|
||||
a Discord API hiccup never disrupts game flow.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
|
||||
logger = logging.getLogger("discord_app")
|
||||
|
||||
# Human-readable display names for each tier number.
|
||||
TIER_NAMES = {
|
||||
0: "Unranked",
|
||||
1: "Initiate",
|
||||
2: "Rising",
|
||||
3: "Ascendant",
|
||||
4: "Evolved",
|
||||
}
|
||||
|
||||
# Tier-specific embed colors.
|
||||
TIER_COLORS = {
|
||||
1: 0x2ECC71, # green
|
||||
2: 0xF1C40F, # gold
|
||||
3: 0x9B59B6, # purple
|
||||
4: 0x1ABC9C, # teal (fully evolved)
|
||||
}
|
||||
|
||||
FOOTER_TEXT = "Paper Dynasty Evolution"
|
||||
|
||||
|
||||
def build_tier_up_embed(tier_up: dict) -> discord.Embed:
|
||||
"""Build a Discord embed for a tier-up event.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tier_up:
|
||||
Dict with keys: player_name, old_tier, new_tier, current_value, track_name.
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Embed
|
||||
A fully configured embed ready to send to a channel.
|
||||
"""
|
||||
player_name: str = tier_up["player_name"]
|
||||
new_tier: int = tier_up["new_tier"]
|
||||
track_name: str = tier_up["track_name"]
|
||||
|
||||
tier_name = TIER_NAMES.get(new_tier, f"Tier {new_tier}")
|
||||
color = TIER_COLORS.get(new_tier, 0x2ECC71)
|
||||
|
||||
if new_tier >= 4:
|
||||
# Fully evolved — special title and description.
|
||||
embed = discord.Embed(
|
||||
title="FULLY EVOLVED!",
|
||||
description=(
|
||||
f"**{player_name}** has reached maximum evolution on the **{track_name}** track"
|
||||
),
|
||||
color=color,
|
||||
)
|
||||
embed.add_field(
|
||||
name="Rating Boosts",
|
||||
value="Rating boosts coming in a future update!",
|
||||
inline=False,
|
||||
)
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
title="Evolution Tier Up!",
|
||||
description=(
|
||||
f"**{player_name}** reached **Tier {new_tier} ({tier_name})** on the **{track_name}** track"
|
||||
),
|
||||
color=color,
|
||||
)
|
||||
|
||||
embed.set_footer(text=FOOTER_TEXT)
|
||||
return embed
|
||||
|
||||
|
||||
async def notify_tier_completion(channel, tier_up: dict) -> None:
|
||||
"""Send a tier-up notification embed to the given channel.
|
||||
|
||||
Non-fatal: any exception during send is caught and logged so that a
|
||||
Discord API failure never interrupts game evaluation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel:
|
||||
A discord.TextChannel (or any object with an async ``send`` method).
|
||||
tier_up:
|
||||
Dict with keys: player_name, old_tier, new_tier, current_value, track_name.
|
||||
"""
|
||||
try:
|
||||
embed = build_tier_up_embed(tier_up)
|
||||
await channel.send(embed=embed)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to send tier-up notification for %s (tier %s): %s",
|
||||
tier_up.get("player_name", "unknown"),
|
||||
tier_up.get("new_tier"),
|
||||
exc,
|
||||
)
|
||||
@ -4,13 +4,13 @@ Tests for the WP-13 post-game callback integration hook.
|
||||
These tests verify that after a game is saved to the API, two additional
|
||||
POST requests are fired in the correct order:
|
||||
1. POST season-stats/update-game/{game_id} — update player_season_stats
|
||||
2. POST evolution/evaluate-game/{game_id} — evaluate evolution milestones
|
||||
2. POST refractor/evaluate-game/{game_id} — evaluate refractor milestones
|
||||
|
||||
Key design constraints being tested:
|
||||
- Season stats MUST be updated before evolution is evaluated (ordering).
|
||||
- Failure of either evolution call must NOT propagate — the game result has
|
||||
already been committed; evolution will self-heal on the next evaluate pass.
|
||||
- Tier-up dicts returned by the evolution endpoint are passed to
|
||||
- Season stats MUST be updated before refractor is evaluated (ordering).
|
||||
- Failure of either refractor call must NOT propagate — the game result has
|
||||
already been committed; refractor will self-heal on the next evaluate pass.
|
||||
- Tier-up dicts returned by the refractor endpoint are passed to
|
||||
notify_tier_completion so WP-14 can present them to the player.
|
||||
"""
|
||||
|
||||
@ -46,7 +46,7 @@ async def _run_hook(db_post_mock, db_game_id: int = 42):
|
||||
|
||||
try:
|
||||
await db_post_mock(f"season-stats/update-game/{db_game['id']}")
|
||||
evo_result = await db_post_mock(f"evolution/evaluate-game/{db_game['id']}")
|
||||
evo_result = await db_post_mock(f"refractor/evaluate-game/{db_game['id']}")
|
||||
if evo_result and evo_result.get("tier_ups"):
|
||||
for tier_up in evo_result["tier_ups"]:
|
||||
await notify_tier_completion(channel, tier_up)
|
||||
@ -64,10 +64,10 @@ async def _run_hook(db_post_mock, db_game_id: int = 42):
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_posts_to_both_endpoints_in_order():
|
||||
"""
|
||||
Both evolution endpoints are called, and season-stats comes first.
|
||||
Both refractor endpoints are called, and season-stats comes first.
|
||||
|
||||
The ordering is critical: player_season_stats must be populated before the
|
||||
evolution engine tries to read them for milestone evaluation.
|
||||
refractor engine tries to read them for milestone evaluation.
|
||||
"""
|
||||
db_post_mock = AsyncMock(return_value={})
|
||||
|
||||
@ -77,8 +77,8 @@ async def test_hook_posts_to_both_endpoints_in_order():
|
||||
calls = db_post_mock.call_args_list
|
||||
# First call must be season-stats
|
||||
assert calls[0] == call("season-stats/update-game/42")
|
||||
# Second call must be evolution evaluate
|
||||
assert calls[1] == call("evolution/evaluate-game/42")
|
||||
# Second call must be refractor evaluate
|
||||
assert calls[1] == call("refractor/evaluate-game/42")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -86,11 +86,11 @@ async def test_hook_is_nonfatal_when_db_post_raises():
|
||||
"""
|
||||
A failure inside the hook must not raise to the caller.
|
||||
|
||||
The game result is already persisted when the hook runs. If the evolution
|
||||
The game result is already persisted when the hook runs. If the refractor
|
||||
API is down or returns an error, we log a warning and continue — the game
|
||||
completion flow must not be interrupted.
|
||||
"""
|
||||
db_post_mock = AsyncMock(side_effect=Exception("evolution API unavailable"))
|
||||
db_post_mock = AsyncMock(side_effect=Exception("refractor API unavailable"))
|
||||
|
||||
# Should not raise
|
||||
try:
|
||||
@ -102,7 +102,7 @@ async def test_hook_is_nonfatal_when_db_post_raises():
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_processes_tier_ups_from_evo_result():
|
||||
"""
|
||||
When the evolution endpoint returns tier_ups, each entry is forwarded to
|
||||
When the refractor endpoint returns tier_ups, each entry is forwarded to
|
||||
notify_tier_completion.
|
||||
|
||||
This confirms the data path between the API response and the WP-14
|
||||
@ -114,7 +114,7 @@ async def test_hook_processes_tier_ups_from_evo_result():
|
||||
]
|
||||
|
||||
async def fake_db_post(endpoint):
|
||||
if "evolution" in endpoint:
|
||||
if "refractor" in endpoint:
|
||||
return {"tier_ups": tier_ups}
|
||||
return {}
|
||||
|
||||
@ -129,7 +129,7 @@ async def test_hook_processes_tier_ups_from_evo_result():
|
||||
|
||||
try:
|
||||
await db_post_mock(f"season-stats/update-game/{db_game['id']}")
|
||||
evo_result = await db_post_mock(f"evolution/evaluate-game/{db_game['id']}")
|
||||
evo_result = await db_post_mock(f"refractor/evaluate-game/{db_game['id']}")
|
||||
if evo_result and evo_result.get("tier_ups"):
|
||||
for tier_up in evo_result["tier_ups"]:
|
||||
await mock_notify(channel, tier_up)
|
||||
@ -146,14 +146,14 @@ async def test_hook_processes_tier_ups_from_evo_result():
|
||||
@pytest.mark.asyncio
|
||||
async def test_hook_no_tier_ups_does_not_call_notify():
|
||||
"""
|
||||
When the evolution response has no tier_ups (empty list or missing key),
|
||||
When the refractor response has no tier_ups (empty list or missing key),
|
||||
notify_tier_completion is never called.
|
||||
|
||||
Avoids spurious Discord messages for routine game completions.
|
||||
"""
|
||||
|
||||
async def fake_db_post(endpoint):
|
||||
if "evolution" in endpoint:
|
||||
if "refractor" in endpoint:
|
||||
return {"tier_ups": []}
|
||||
return {}
|
||||
|
||||
@ -168,7 +168,7 @@ async def test_hook_no_tier_ups_does_not_call_notify():
|
||||
|
||||
try:
|
||||
await db_post_mock(f"season-stats/update-game/{db_game['id']}")
|
||||
evo_result = await db_post_mock(f"evolution/evaluate-game/{db_game['id']}")
|
||||
evo_result = await db_post_mock(f"refractor/evaluate-game/{db_game['id']}")
|
||||
if evo_result and evo_result.get("tier_ups"):
|
||||
for tier_up in evo_result["tier_ups"]:
|
||||
await mock_notify(channel, tier_up)
|
||||
|
||||
@ -1,154 +0,0 @@
|
||||
"""
|
||||
Tests for evolution tier completion notification embeds (WP-14).
|
||||
|
||||
These are pure unit tests — no database or Discord bot connection required.
|
||||
Each test constructs embeds and asserts on title, description, color, and
|
||||
footer to verify the notification design spec is met.
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
from utilities.evolution_notifications import (
|
||||
TIER_COLORS,
|
||||
build_tier_embeds,
|
||||
tier_up_embed,
|
||||
)
|
||||
|
||||
|
||||
class TestTierUpEmbed:
|
||||
"""Unit tests for tier_up_embed() — standard (T1–T3) and fully-evolved (T4) paths."""
|
||||
|
||||
def test_tier_up_title(self):
|
||||
"""Standard tier-up embeds must use the 'Evolution Tier Up!' title."""
|
||||
embed = tier_up_embed(
|
||||
"Mike Trout", tier=2, tier_name="Rising", track_name="Batter"
|
||||
)
|
||||
assert embed.title == "Evolution Tier Up!"
|
||||
|
||||
def test_tier_up_description_format(self):
|
||||
"""Description must include player name, tier number, tier name, and track name."""
|
||||
embed = tier_up_embed(
|
||||
"Mike Trout", tier=2, tier_name="Rising", track_name="Batter"
|
||||
)
|
||||
assert (
|
||||
embed.description
|
||||
== "Mike Trout reached Tier 2 (Rising) on the Batter track"
|
||||
)
|
||||
|
||||
def test_tier_up_color_matches_tier(self):
|
||||
"""Each tier must map to its specified embed color."""
|
||||
for tier, expected_color in TIER_COLORS.items():
|
||||
if tier == 4:
|
||||
continue # T4 handled in fully-evolved tests
|
||||
embed = tier_up_embed(
|
||||
"Test Player", tier=tier, tier_name="Name", track_name="Batter"
|
||||
)
|
||||
assert embed.color.value == expected_color, f"Tier {tier} color mismatch"
|
||||
|
||||
def test_tier_up_no_footer_for_standard_tiers(self):
|
||||
"""Standard tier-up embeds (T1–T3) must not have a footer."""
|
||||
for tier in (1, 2, 3):
|
||||
embed = tier_up_embed(
|
||||
"Test Player", tier=tier, tier_name="Name", track_name="Batter"
|
||||
)
|
||||
assert embed.footer.text is None
|
||||
|
||||
|
||||
class TestFullyEvolvedEmbed:
|
||||
"""Unit tests for the fully-evolved (T4) embed — distinct title, description, and footer."""
|
||||
|
||||
def test_fully_evolved_title(self):
|
||||
"""T4 embeds must use the 'FULLY EVOLVED!' title."""
|
||||
embed = tier_up_embed(
|
||||
"Mike Trout", tier=4, tier_name="Legendary", track_name="Batter"
|
||||
)
|
||||
assert embed.title == "FULLY EVOLVED!"
|
||||
|
||||
def test_fully_evolved_description(self):
|
||||
"""T4 description must indicate maximum evolution without mentioning tier number."""
|
||||
embed = tier_up_embed(
|
||||
"Mike Trout", tier=4, tier_name="Legendary", track_name="Batter"
|
||||
)
|
||||
assert (
|
||||
embed.description
|
||||
== "Mike Trout has reached maximum evolution on the Batter track"
|
||||
)
|
||||
|
||||
def test_fully_evolved_footer(self):
|
||||
"""T4 embeds must include the Phase 2 teaser footer."""
|
||||
embed = tier_up_embed(
|
||||
"Mike Trout", tier=4, tier_name="Legendary", track_name="Batter"
|
||||
)
|
||||
assert embed.footer.text == "Rating boosts coming in a future update!"
|
||||
|
||||
def test_fully_evolved_color(self):
|
||||
"""T4 embed color must be teal."""
|
||||
embed = tier_up_embed(
|
||||
"Mike Trout", tier=4, tier_name="Legendary", track_name="Batter"
|
||||
)
|
||||
assert embed.color.value == TIER_COLORS[4]
|
||||
|
||||
|
||||
class TestBuildTierEmbeds:
|
||||
"""Unit tests for build_tier_embeds() — list construction and edge cases."""
|
||||
|
||||
def test_no_tier_ups_returns_empty_list(self):
|
||||
"""When no tier-ups occurred, build_tier_embeds must return an empty list."""
|
||||
result = build_tier_embeds([])
|
||||
assert result == []
|
||||
|
||||
def test_single_tier_up_returns_one_embed(self):
|
||||
"""A single tier-up event must produce exactly one embed."""
|
||||
tier_ups = [
|
||||
{
|
||||
"player_name": "Mike Trout",
|
||||
"tier": 2,
|
||||
"tier_name": "Rising",
|
||||
"track_name": "Batter",
|
||||
}
|
||||
]
|
||||
result = build_tier_embeds(tier_ups)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], discord.Embed)
|
||||
|
||||
def test_multiple_tier_ups_return_separate_embeds(self):
|
||||
"""Multiple tier-up events in one game must produce one embed per event."""
|
||||
tier_ups = [
|
||||
{
|
||||
"player_name": "Mike Trout",
|
||||
"tier": 2,
|
||||
"tier_name": "Rising",
|
||||
"track_name": "Batter",
|
||||
},
|
||||
{
|
||||
"player_name": "Sandy Koufax",
|
||||
"tier": 3,
|
||||
"tier_name": "Elite",
|
||||
"track_name": "Starter",
|
||||
},
|
||||
]
|
||||
result = build_tier_embeds(tier_ups)
|
||||
assert len(result) == 2
|
||||
assert (
|
||||
result[0].description
|
||||
== "Mike Trout reached Tier 2 (Rising) on the Batter track"
|
||||
)
|
||||
assert (
|
||||
result[1].description
|
||||
== "Sandy Koufax reached Tier 3 (Elite) on the Starter track"
|
||||
)
|
||||
|
||||
def test_fully_evolved_in_batch(self):
|
||||
"""A T4 event in a batch must produce a fully-evolved embed, not a standard one."""
|
||||
tier_ups = [
|
||||
{
|
||||
"player_name": "Babe Ruth",
|
||||
"tier": 4,
|
||||
"tier_name": "Legendary",
|
||||
"track_name": "Batter",
|
||||
}
|
||||
]
|
||||
result = build_tier_embeds(tier_ups)
|
||||
assert len(result) == 1
|
||||
assert result[0].title == "FULLY EVOLVED!"
|
||||
assert result[0].footer.text == "Rating boosts coming in a future update!"
|
||||
Loading…
Reference in New Issue
Block a user