All checks were successful
Ruff Lint / lint (pull_request) Successful in 22s
Implements all gap tests identified by PO agents across three existing
test files. No new files created — tests added to existing modules.
T1-5 (test_refractor_notifs): Expose WP-14 integration bug — minimal
stub dict {player_id, old_tier, new_tier} causes KeyError in
build_tier_up_embed because player_name/track_name use bare dict access.
Documents the bug contract so WP-14 implementers know what to fix.
T1-6 (test_refractor_commands): Divergence tripwire — imports TIER_NAMES
from both cogs.refractor and helpers.refractor_notifs and asserts deep
equality. Will fail the moment the two copies fall out of sync.
T1-7 (test_card_embed_refractor): TIER_BADGES format contract — asserts
that wrapping helpers.main badge values in brackets produces cogs.refractor
badge values (e.g. "BC" -> "[BC]") for all tiers.
T2-7 (test_refractor_notifs): notify_tier_completion with None channel
must not raise — the try/except absorbs AttributeError from None.send().
T2-8 (test_refractor_commands): All-T4 apply_close_filter returns empty
list. Documents intended behaviour for tier=4 + progress="close" combo.
T3-2 (test_refractor_commands): Malformed API response handling —
format_refractor_entry must use fallbacks ("Unknown", 0) for missing keys.
T3-3 (test_refractor_commands): Progress bar boundary precision — 1/100,
99/100, 0/100, and negative current values.
T3-4 (test_refractor_commands): RP formula label — card_type="rp" shows
"IP+K" (previously only "sp" was tested).
T3-5 (test_refractor_commands): Unknown card_type falls back to raw string
as the formula label without crashing.
112 tests pass (23 new, 89 pre-existing).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
341 lines
13 KiB
Python
341 lines
13 KiB
Python
"""
|
|
Tests for WP-12: Tier Badge on Card Embed.
|
|
|
|
Verifies that get_card_embeds() prepends a tier badge to the card title when a
|
|
card has Refractor tier progression, and falls back gracefully when the Refractor
|
|
API is unavailable or returns no state.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import discord
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_card(card_id=1, player_name="Mike Trout", rarity_color="FFD700"):
|
|
"""Minimal card dict matching the API shape consumed by get_card_embeds."""
|
|
return {
|
|
"id": card_id,
|
|
"player": {
|
|
"player_id": 101,
|
|
"p_name": player_name,
|
|
"rarity": {"name": "MVP", "value": 5, "color": rarity_color},
|
|
"cost": 500,
|
|
"image": "https://example.com/card.png",
|
|
"image2": None,
|
|
"mlbclub": "Los Angeles Angels",
|
|
"franchise": "Los Angeles Angels",
|
|
"headshot": "https://example.com/headshot.jpg",
|
|
"cardset": {"name": "2023 Season"},
|
|
"pos_1": "CF",
|
|
"pos_2": None,
|
|
"pos_3": None,
|
|
"pos_4": None,
|
|
"pos_5": None,
|
|
"pos_6": None,
|
|
"pos_7": None,
|
|
"bbref_id": "troutmi01",
|
|
"strat_code": "420420",
|
|
"fangr_id": None,
|
|
"vanity_card": None,
|
|
},
|
|
"team": {
|
|
"id": 10,
|
|
"lname": "Paper Dynasty",
|
|
"logo": "https://example.com/logo.png",
|
|
"season": 7,
|
|
},
|
|
}
|
|
|
|
|
|
def _make_paperdex():
|
|
"""Minimal paperdex response."""
|
|
return {"count": 0, "paperdex": []}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers to patch the async dependencies of get_card_embeds
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _patch_db_get(evo_response=None, paperdex_response=None):
|
|
"""
|
|
Return a side_effect callable that routes db_get calls to the right mock
|
|
responses, so other get_card_embeds internals still behave.
|
|
"""
|
|
if paperdex_response is None:
|
|
paperdex_response = _make_paperdex()
|
|
|
|
async def _side_effect(endpoint, *args, **kwargs):
|
|
if str(endpoint).startswith("refractor/cards/"):
|
|
return evo_response
|
|
if endpoint == "paperdex":
|
|
return paperdex_response
|
|
# Fallback for any other endpoint (e.g. plays/batting, plays/pitching)
|
|
return None
|
|
|
|
return _side_effect
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTierBadgeFormat:
|
|
"""Unit: tier badge string format for each tier level."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_zero_no_badge(self):
|
|
"""T0 evolution state (current_tier=0) should produce no badge in title."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 0, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_one_badge(self):
|
|
"""current_tier=1 should prefix title with [BC] (Base Chrome)."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 1, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[BC] Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_two_badge(self):
|
|
"""current_tier=2 should prefix title with [R] (Refractor)."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 2, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[R] Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_three_badge(self):
|
|
"""current_tier=3 should prefix title with [GR] (Gold Refractor)."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 3, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[GR] Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_four_superfractor_badge(self):
|
|
"""current_tier=4 (Superfractor) should prefix title with [SF]."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 4, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "[SF] Mike Trout"
|
|
|
|
|
|
class TestTierBadgeInTitle:
|
|
"""Unit: badge appears correctly in the embed title."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_badge_prepended_to_player_name(self):
|
|
"""Badge should be prepended so title reads '[Tx] <player_name>'."""
|
|
card = _make_card(player_name="Juan Soto")
|
|
evo_state = {"current_tier": 2, "card_id": 1}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title.startswith("[R] ")
|
|
assert "Juan Soto" in embeds[0].title
|
|
|
|
|
|
class TestFullyEvolvedBadge:
|
|
"""Unit: fully evolved card shows [SF] badge (Superfractor)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fully_evolved_badge(self):
|
|
"""T4 card should show [SF] prefix, not [T4]."""
|
|
card = _make_card()
|
|
evo_state = {"current_tier": 4}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title.startswith("[SF] ")
|
|
assert "[T4]" not in embeds[0].title
|
|
|
|
|
|
class TestNoBadgeGracefulFallback:
|
|
"""Unit: embed renders correctly when evolution state is absent or API fails."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_evolution_state_no_badge(self):
|
|
"""When evolution API returns None (404), title has no badge."""
|
|
card = _make_card()
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=None)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "Mike Trout"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_api_exception_no_badge(self):
|
|
"""When evolution API raises an exception, card display is unaffected."""
|
|
card = _make_card()
|
|
|
|
async def _failing_db_get(endpoint, *args, **kwargs):
|
|
if str(endpoint).startswith("refractor/cards/"):
|
|
raise ConnectionError("API unreachable")
|
|
if endpoint == "paperdex":
|
|
return _make_paperdex()
|
|
return None
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _failing_db_get
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].title == "Mike Trout"
|
|
|
|
|
|
class TestEmbedColorUnchanged:
|
|
"""Unit: embed color comes from card rarity, not affected by evolution state."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_embed_color_from_rarity_with_evolution(self):
|
|
"""Color is still derived from rarity even when a tier badge is present."""
|
|
rarity_color = "FF0000"
|
|
card = _make_card(rarity_color=rarity_color)
|
|
evo_state = {"current_tier": 2}
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=evo_state)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].color == discord.Color(int(rarity_color, 16))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_embed_color_from_rarity_without_evolution(self):
|
|
"""Color is derived from rarity when no evolution state exists."""
|
|
rarity_color = "00FF00"
|
|
card = _make_card(rarity_color=rarity_color)
|
|
|
|
with patch("helpers.main.db_get", new_callable=AsyncMock) as mock_db:
|
|
mock_db.side_effect = _patch_db_get(evo_response=None)
|
|
embeds = await _call_get_card_embeds(card)
|
|
|
|
assert embeds[0].color == discord.Color(int(rarity_color, 16))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# T1-7: TIER_BADGES format consistency check across modules
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestTierBadgesFormatConsistency:
|
|
"""
|
|
T1-7: Assert that TIER_BADGES in cogs.refractor (format: "[BC]") and
|
|
helpers.main (format: "BC") are consistent — wrapping the helpers.main
|
|
value in brackets must produce the cogs.refractor value.
|
|
|
|
Why: The two modules intentionally use different formats for different
|
|
rendering contexts:
|
|
- helpers.main uses bare strings ("BC") because get_card_embeds
|
|
wraps them in brackets when building the embed title.
|
|
- cogs.refractor uses bracket strings ("[BC]") because
|
|
format_refractor_entry inlines them directly into the display string.
|
|
|
|
If either definition is updated without updating the other, embed titles
|
|
and /refractor status output will display inconsistent badges. This test
|
|
acts as an explicit contract check so any future change to either dict
|
|
is immediately surfaced here.
|
|
"""
|
|
|
|
def test_cogs_badge_equals_bracketed_helpers_badge_for_all_tiers(self):
|
|
"""
|
|
For every tier in cogs.refractor TIER_BADGES, wrapping the
|
|
helpers.main TIER_BADGES value in square brackets must produce
|
|
the cogs.refractor value.
|
|
|
|
i.e., f"[{helpers_badge}]" == cog_badge for all tiers.
|
|
"""
|
|
from cogs.refractor import TIER_BADGES as cog_badges
|
|
from helpers.main import TIER_BADGES as helpers_badges
|
|
|
|
assert set(cog_badges.keys()) == set(helpers_badges.keys()), (
|
|
"TIER_BADGES key sets differ between cogs.refractor and helpers.main. "
|
|
f"cogs keys: {set(cog_badges.keys())}, helpers keys: {set(helpers_badges.keys())}"
|
|
)
|
|
|
|
for tier, cog_badge in cog_badges.items():
|
|
helpers_badge = helpers_badges[tier]
|
|
expected = f"[{helpers_badge}]"
|
|
assert cog_badge == expected, (
|
|
f"Tier {tier} badge mismatch: "
|
|
f"cogs.refractor={cog_badge!r}, "
|
|
f"helpers.main={helpers_badge!r} "
|
|
f"(expected cog badge to equal '[{helpers_badge}]')"
|
|
)
|
|
|
|
def test_t1_badge_relationship(self):
|
|
"""T1: helpers.main 'BC' wrapped in brackets equals cogs.refractor '[BC]'."""
|
|
from cogs.refractor import TIER_BADGES as cog_badges
|
|
from helpers.main import TIER_BADGES as helpers_badges
|
|
|
|
assert f"[{helpers_badges[1]}]" == cog_badges[1]
|
|
|
|
def test_t2_badge_relationship(self):
|
|
"""T2: helpers.main 'R' wrapped in brackets equals cogs.refractor '[R]'."""
|
|
from cogs.refractor import TIER_BADGES as cog_badges
|
|
from helpers.main import TIER_BADGES as helpers_badges
|
|
|
|
assert f"[{helpers_badges[2]}]" == cog_badges[2]
|
|
|
|
def test_t3_badge_relationship(self):
|
|
"""T3: helpers.main 'GR' wrapped in brackets equals cogs.refractor '[GR]'."""
|
|
from cogs.refractor import TIER_BADGES as cog_badges
|
|
from helpers.main import TIER_BADGES as helpers_badges
|
|
|
|
assert f"[{helpers_badges[3]}]" == cog_badges[3]
|
|
|
|
def test_t4_badge_relationship(self):
|
|
"""T4: helpers.main 'SF' wrapped in brackets equals cogs.refractor '[SF]'."""
|
|
from cogs.refractor import TIER_BADGES as cog_badges
|
|
from helpers.main import TIER_BADGES as helpers_badges
|
|
|
|
assert f"[{helpers_badges[4]}]" == cog_badges[4]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: call get_card_embeds and return embed list
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def _call_get_card_embeds(card):
|
|
"""Import and call get_card_embeds, returning the list of embeds."""
|
|
from helpers.main import get_card_embeds
|
|
|
|
result = await get_card_embeds(card)
|
|
if isinstance(result, list):
|
|
return result
|
|
return [result]
|