All checks were successful
Ruff Lint / lint (pull_request) Successful in 24s
Closes #147 Adds a "Refractor Progress" field to the game summary embed showing: - Cards that tiered up during this game (⬆ Name → Tier Name) - Cards currently ≥80% toward their next tier on either team (◈ Name (pct%)) The field is omitted entirely when there is nothing to show. Implementation: - _run_post_game_refractor_hook() now returns evo_result (or None on failure) - New _build_refractor_progress_text() fetches close-to-tier cards from both teams via refractor/cards?progress=close and formats the combined output - complete_game() adds the field between Rewards and Highlights sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
258 lines
9.3 KiB
Python
258 lines
9.3 KiB
Python
"""
|
|
Tests for the post-game Refractor Progress embed field (#147).
|
|
|
|
Covers _build_refractor_progress_text() which formats tier-ups and
|
|
near-threshold cards into the summary embed field value, and the updated
|
|
_run_post_game_refractor_hook() return value.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from command_logic.logic_gameplay import (
|
|
_build_refractor_progress_text,
|
|
_run_post_game_refractor_hook,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _build_refractor_progress_text
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestBuildRefractorProgressText:
|
|
"""_build_refractor_progress_text formats tier-ups and close cards."""
|
|
|
|
async def test_returns_none_when_no_tier_ups_and_no_close_cards(self):
|
|
"""Returns None when evaluate-game had no tier-ups and refractor/cards returns empty.
|
|
|
|
Caller uses None to skip adding the field to the embed entirely.
|
|
"""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = {"items": [], "count": 0}
|
|
result = await _build_refractor_progress_text(
|
|
evo_result={"tier_ups": []},
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is None
|
|
|
|
async def test_returns_none_when_evo_result_is_none(self):
|
|
"""Returns None gracefully when the hook returned None (e.g. on API failure).
|
|
|
|
Near-threshold fetch still runs; returns None when that also yields nothing.
|
|
"""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = None
|
|
result = await _build_refractor_progress_text(
|
|
evo_result=None,
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is None
|
|
|
|
async def test_tier_up_shows_player_name_and_tier_name(self):
|
|
"""Tier-ups are formatted as '⬆ **Name** → Tier Name'.
|
|
|
|
The tier name comes from TIER_NAMES (e.g. new_tier=1 → 'Base Chrome').
|
|
"""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = None
|
|
result = await _build_refractor_progress_text(
|
|
evo_result={
|
|
"tier_ups": [
|
|
{
|
|
"player_id": 10,
|
|
"player_name": "Mike Trout",
|
|
"new_tier": 1,
|
|
}
|
|
]
|
|
},
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is not None
|
|
assert "⬆" in result
|
|
assert "Mike Trout" in result
|
|
assert "Base Chrome" in result
|
|
|
|
async def test_multiple_tier_ups_each_on_own_line(self):
|
|
"""Each tier-up gets its own line in the output."""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = None
|
|
result = await _build_refractor_progress_text(
|
|
evo_result={
|
|
"tier_ups": [
|
|
{"player_id": 10, "player_name": "Mike Trout", "new_tier": 1},
|
|
{
|
|
"player_id": 11,
|
|
"player_name": "Shohei Ohtani",
|
|
"new_tier": 2,
|
|
},
|
|
]
|
|
},
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is not None
|
|
assert "Mike Trout" in result
|
|
assert "Shohei Ohtani" in result
|
|
assert "Base Chrome" in result
|
|
assert "Refractor" in result
|
|
|
|
async def test_near_threshold_card_shows_percentage(self):
|
|
"""Near-threshold cards appear as '◈ Name (pct%)'.
|
|
|
|
The percentage is current_value / next_threshold rounded to nearest integer.
|
|
"""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = {
|
|
"items": [
|
|
{
|
|
"player_name": "Sandy Koufax",
|
|
"current_value": 120,
|
|
"next_threshold": 149,
|
|
}
|
|
],
|
|
"count": 1,
|
|
}
|
|
result = await _build_refractor_progress_text(
|
|
evo_result=None,
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is not None
|
|
assert "◈" in result
|
|
assert "Sandy Koufax" in result
|
|
assert "81%" in result # 120/149 = ~80.5% → 81%
|
|
|
|
async def test_near_threshold_fetch_queried_for_both_teams(self):
|
|
"""refractor/cards is called once per team with progress=close."""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = None
|
|
await _build_refractor_progress_text(
|
|
evo_result=None,
|
|
winning_team_id=3,
|
|
losing_team_id=7,
|
|
)
|
|
|
|
team_ids_queried = []
|
|
for call in mock_get.call_args_list:
|
|
params = dict(call.kwargs.get("params", []))
|
|
if "team_id" in params:
|
|
team_ids_queried.append(params["team_id"])
|
|
|
|
assert 3 in team_ids_queried
|
|
assert 7 in team_ids_queried
|
|
|
|
async def test_near_threshold_api_failure_is_non_fatal(self):
|
|
"""An exception during the near-threshold fetch does not propagate.
|
|
|
|
Tier-ups are still shown; close cards silently dropped.
|
|
"""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.side_effect = RuntimeError("API down")
|
|
result = await _build_refractor_progress_text(
|
|
evo_result={
|
|
"tier_ups": [
|
|
{"player_id": 10, "player_name": "Mike Trout", "new_tier": 1}
|
|
]
|
|
},
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is not None
|
|
assert "Mike Trout" in result
|
|
|
|
async def test_close_cards_capped_at_five(self):
|
|
"""At most 5 near-threshold entries are included across both teams."""
|
|
many_cards = [
|
|
{"player_name": f"Player {i}", "current_value": 90, "next_threshold": 100}
|
|
for i in range(10)
|
|
]
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_get", new_callable=AsyncMock
|
|
) as mock_get:
|
|
mock_get.return_value = {"items": many_cards, "count": len(many_cards)}
|
|
result = await _build_refractor_progress_text(
|
|
evo_result=None,
|
|
winning_team_id=1,
|
|
losing_team_id=2,
|
|
)
|
|
assert result is not None
|
|
assert result.count("◈") <= 5
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _run_post_game_refractor_hook return value
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRefractorHookReturnValue:
|
|
"""_run_post_game_refractor_hook returns evo_result on success, None on failure."""
|
|
|
|
async def test_returns_evo_result_when_successful(self):
|
|
"""The evaluate-game response dict is returned so complete_game can use it."""
|
|
evo_response = {
|
|
"tier_ups": [{"player_id": 1, "player_name": "Babe Ruth", "new_tier": 2}]
|
|
}
|
|
|
|
def _side_effect(url, *args, **kwargs):
|
|
if url.startswith("season-stats"):
|
|
return None
|
|
return evo_response
|
|
|
|
with (
|
|
patch(
|
|
"command_logic.logic_gameplay.db_post", new_callable=AsyncMock
|
|
) as mock_post,
|
|
patch(
|
|
"command_logic.logic_gameplay._trigger_variant_renders",
|
|
new_callable=AsyncMock,
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"command_logic.logic_gameplay.notify_tier_completion",
|
|
new_callable=AsyncMock,
|
|
),
|
|
):
|
|
mock_post.side_effect = _side_effect
|
|
result = await _run_post_game_refractor_hook(42, MagicMock())
|
|
|
|
assert result == evo_response
|
|
|
|
async def test_returns_none_on_exception(self):
|
|
"""Hook returns None when an exception occurs (game result is unaffected)."""
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_post", new_callable=AsyncMock
|
|
) as mock_post:
|
|
mock_post.side_effect = RuntimeError("db unreachable")
|
|
result = await _run_post_game_refractor_hook(42, MagicMock())
|
|
|
|
assert result is None
|
|
|
|
async def test_returns_evo_result_when_no_tier_ups(self):
|
|
"""Returns the full evo_result even when tier_ups is empty or absent."""
|
|
evo_response = {"tier_ups": []}
|
|
|
|
with patch(
|
|
"command_logic.logic_gameplay.db_post", new_callable=AsyncMock
|
|
) as mock_post:
|
|
mock_post.return_value = evo_response
|
|
result = await _run_post_game_refractor_hook(42, MagicMock())
|
|
|
|
assert result == evo_response
|