paper-dynasty-discord/tests/test_refractor_commands.py
cal 8c0c2eb21a test: refractor system comprehensive test coverage (#117)
test: add comprehensive refractor system test coverage (23 tests)

Covers TIER_NAMES/TIER_BADGES cross-module consistency, WP-14 tier_up dict
shape mismatch (latent KeyError documented), None channel handling, filter
combinations, progress bar boundaries, and malformed API response handling.
2026-03-24 21:06:13 +00:00

803 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Unit tests for refractor command helper functions (WP-11).
Tests cover:
- render_progress_bar: ASCII bar rendering at various fill levels
- format_refractor_entry: Full card state formatting including fully evolved case
- apply_close_filter: 80% proximity filter logic
- paginate: 1-indexed page slicing and total-page calculation
- TIER_NAMES: Display names for all tiers
- Slash command: empty roster and no-team responses (async, uses mocks)
All tests are pure-unit unless marked otherwise; no network calls are made.
"""
import sys
import os
import pytest
from unittest.mock import AsyncMock, Mock, patch
import discord
from discord.ext import commands
# Make the repo root importable
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from cogs.refractor import (
render_progress_bar,
format_refractor_entry,
apply_close_filter,
paginate,
TIER_NAMES,
TIER_BADGES,
PAGE_SIZE,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def batter_state():
"""A mid-progress batter card state."""
return {
"player_name": "Mike Trout",
"card_type": "batter",
"current_tier": 1,
"formula_value": 120,
"next_threshold": 149,
}
@pytest.fixture
def evolved_state():
"""A fully evolved card state (T4)."""
return {
"player_name": "Shohei Ohtani",
"card_type": "batter",
"current_tier": 4,
"formula_value": 300,
"next_threshold": None,
}
@pytest.fixture
def sp_state():
"""A starting pitcher card state at T2."""
return {
"player_name": "Sandy Alcantara",
"card_type": "sp",
"current_tier": 2,
"formula_value": 95,
"next_threshold": 120,
}
# ---------------------------------------------------------------------------
# render_progress_bar
# ---------------------------------------------------------------------------
class TestRenderProgressBar:
"""
Tests for render_progress_bar().
Verifies width, fill character, empty character, boundary conditions,
and clamping when current exceeds threshold.
"""
def test_empty_bar(self):
"""current=0 → all dashes."""
assert render_progress_bar(0, 100) == "[----------]"
def test_full_bar(self):
"""current == threshold → all equals."""
assert render_progress_bar(100, 100) == "[==========]"
def test_partial_fill(self):
"""120/149 ≈ 80.5% → 8 filled of 10."""
bar = render_progress_bar(120, 149)
assert bar == "[========--]"
def test_half_fill(self):
"""50/100 = 50% → 5 filled."""
assert render_progress_bar(50, 100) == "[=====-----]"
def test_over_threshold_clamps_to_full(self):
"""current > threshold should not overflow the bar."""
assert render_progress_bar(200, 100) == "[==========]"
def test_zero_threshold_returns_full_bar(self):
"""threshold=0 avoids division by zero and returns full bar."""
assert render_progress_bar(0, 0) == "[==========]"
def test_custom_width(self):
"""Width parameter controls bar length."""
bar = render_progress_bar(5, 10, width=4)
assert bar == "[==--]"
# ---------------------------------------------------------------------------
# format_refractor_entry
# ---------------------------------------------------------------------------
class TestFormatRefractorEntry:
"""
Tests for format_refractor_entry().
Verifies player name, tier label, progress bar, formula label,
and the special fully-evolved formatting.
"""
def test_player_name_in_output(self, batter_state):
"""Player name appears bold in the first line (badge may prefix it)."""
result = format_refractor_entry(batter_state)
assert "Mike Trout" in result
assert "**" in result
def test_tier_label_in_output(self, batter_state):
"""Current tier name (Base Chrome for T1) appears in output."""
result = format_refractor_entry(batter_state)
assert "(Base Chrome)" in result
def test_progress_values_in_output(self, batter_state):
"""current/threshold values appear in output."""
result = format_refractor_entry(batter_state)
assert "120/149" in result
def test_formula_label_batter(self, batter_state):
"""Batter formula label PA+TB×2 appears in output."""
result = format_refractor_entry(batter_state)
assert "PA+TB×2" in result
def test_tier_progression_arrow(self, batter_state):
"""T1 → T2 arrow progression appears for non-evolved cards."""
result = format_refractor_entry(batter_state)
assert "T1 → T2" in result
def test_sp_formula_label(self, sp_state):
"""SP formula label IP+K appears for starting pitchers."""
result = format_refractor_entry(sp_state)
assert "IP+K" in result
def test_fully_evolved_no_threshold(self, evolved_state):
"""T4 card with next_threshold=None shows FULLY EVOLVED."""
result = format_refractor_entry(evolved_state)
assert "FULLY EVOLVED" in result
def test_fully_evolved_by_tier(self, batter_state):
"""current_tier=4 triggers fully evolved display even with a threshold."""
batter_state["current_tier"] = 4
batter_state["next_threshold"] = 200
result = format_refractor_entry(batter_state)
assert "FULLY EVOLVED" in result
def test_fully_evolved_no_arrow(self, evolved_state):
"""Fully evolved cards don't show a tier arrow."""
result = format_refractor_entry(evolved_state)
assert "" not in result
def test_two_line_output(self, batter_state):
"""Output always has exactly two lines (name line + bar line)."""
result = format_refractor_entry(batter_state)
lines = result.split("\n")
assert len(lines) == 2
# ---------------------------------------------------------------------------
# TIER_BADGES
# ---------------------------------------------------------------------------
class TestTierBadges:
"""
Verify TIER_BADGES values and that format_refractor_entry prepends badges
correctly for T1-T4. T0 cards should have no badge prefix.
"""
def test_t1_badge_value(self):
"""T1 badge is [BC] (Base Chrome)."""
assert TIER_BADGES[1] == "[BC]"
def test_t2_badge_value(self):
"""T2 badge is [R] (Refractor)."""
assert TIER_BADGES[2] == "[R]"
def test_t3_badge_value(self):
"""T3 badge is [GR] (Gold Refractor)."""
assert TIER_BADGES[3] == "[GR]"
def test_t4_badge_value(self):
"""T4 badge is [SF] (Superfractor)."""
assert TIER_BADGES[4] == "[SF]"
def test_t0_no_badge(self):
"""T0 has no badge entry in TIER_BADGES."""
assert 0 not in TIER_BADGES
def test_format_entry_t1_badge_present(self, batter_state):
"""format_refractor_entry prepends [BC] badge for T1 cards."""
result = format_refractor_entry(batter_state)
assert "[BC]" in result
def test_format_entry_t2_badge_present(self, sp_state):
"""format_refractor_entry prepends [R] badge for T2 cards."""
result = format_refractor_entry(sp_state)
assert "[R]" in result
def test_format_entry_t4_badge_present(self, evolved_state):
"""format_refractor_entry prepends [SF] badge for T4 cards."""
result = format_refractor_entry(evolved_state)
assert "[SF]" in result
def test_format_entry_t0_no_badge(self):
"""format_refractor_entry does not prepend any badge for T0 cards."""
state = {
"player_name": "Rookie Player",
"card_type": "batter",
"current_tier": 0,
"formula_value": 10,
"next_threshold": 50,
}
result = format_refractor_entry(state)
assert "[BC]" not in result
assert "[R]" not in result
assert "[GR]" not in result
assert "[SF]" not in result
def test_format_entry_badge_before_name(self, batter_state):
"""Badge appears before the player name in the bold section."""
result = format_refractor_entry(batter_state)
first_line = result.split("\n")[0]
badge_pos = first_line.find("[BC]")
name_pos = first_line.find("Mike Trout")
assert badge_pos < name_pos
# ---------------------------------------------------------------------------
# apply_close_filter
# ---------------------------------------------------------------------------
class TestApplyCloseFilter:
"""
Tests for apply_close_filter().
'Close' means formula_value >= 80% of next_threshold.
Fully evolved (T4 or no threshold) cards are excluded from results.
"""
def test_close_card_included(self):
"""Card at exactly 80% is included."""
state = {"current_tier": 1, "formula_value": 80, "next_threshold": 100}
assert apply_close_filter([state]) == [state]
def test_above_80_percent_included(self):
"""Card above 80% is included."""
state = {"current_tier": 0, "formula_value": 95, "next_threshold": 100}
assert apply_close_filter([state]) == [state]
def test_below_80_percent_excluded(self):
"""Card below 80% threshold is excluded."""
state = {"current_tier": 1, "formula_value": 79, "next_threshold": 100}
assert apply_close_filter([state]) == []
def test_fully_evolved_excluded(self):
"""T4 cards are never returned by close filter."""
state = {"current_tier": 4, "formula_value": 300, "next_threshold": None}
assert apply_close_filter([state]) == []
def test_none_threshold_excluded(self):
"""Cards with no next_threshold (regardless of tier) are excluded."""
state = {"current_tier": 3, "formula_value": 200, "next_threshold": None}
assert apply_close_filter([state]) == []
def test_mixed_list(self):
"""Only qualifying cards are returned from a mixed list."""
close = {"current_tier": 1, "formula_value": 90, "next_threshold": 100}
not_close = {"current_tier": 1, "formula_value": 50, "next_threshold": 100}
evolved = {"current_tier": 4, "formula_value": 300, "next_threshold": None}
result = apply_close_filter([close, not_close, evolved])
assert result == [close]
def test_empty_list(self):
"""Empty input returns empty list."""
assert apply_close_filter([]) == []
# ---------------------------------------------------------------------------
# paginate
# ---------------------------------------------------------------------------
class TestPaginate:
"""
Tests for paginate().
Verifies 1-indexed page slicing, total page count calculation,
page clamping, and PAGE_SIZE default.
"""
def _items(self, n):
return list(range(n))
def test_single_page_all_items(self):
"""Fewer items than page size returns all on page 1."""
items, total = paginate(self._items(5), page=1)
assert items == [0, 1, 2, 3, 4]
assert total == 1
def test_first_page(self):
"""Page 1 returns first PAGE_SIZE items."""
items, total = paginate(self._items(25), page=1)
assert items == list(range(10))
assert total == 3
def test_second_page(self):
"""Page 2 returns next PAGE_SIZE items."""
items, total = paginate(self._items(25), page=2)
assert items == list(range(10, 20))
def test_last_page_partial(self):
"""Last page returns remaining items (fewer than PAGE_SIZE)."""
items, total = paginate(self._items(25), page=3)
assert items == [20, 21, 22, 23, 24]
assert total == 3
def test_page_clamp_low(self):
"""Page 0 or negative is clamped to page 1."""
items, _ = paginate(self._items(15), page=0)
assert items == list(range(10))
def test_page_clamp_high(self):
"""Page beyond total is clamped to last page."""
items, total = paginate(self._items(15), page=99)
assert items == [10, 11, 12, 13, 14]
assert total == 2
def test_empty_list_returns_empty_page(self):
"""Empty input returns empty page with total_pages=1."""
items, total = paginate([], page=1)
assert items == []
assert total == 1
def test_exact_page_boundary(self):
"""Exactly PAGE_SIZE items → 1 full page."""
items, total = paginate(self._items(PAGE_SIZE), page=1)
assert len(items) == PAGE_SIZE
assert total == 1
# ---------------------------------------------------------------------------
# TIER_NAMES
# ---------------------------------------------------------------------------
class TestTierNames:
"""
Verify all tier display names are correctly defined.
T0=Base Card, T1=Base Chrome, T2=Refractor, T3=Gold Refractor, T4=Superfractor
"""
def test_t0_base_card(self):
assert TIER_NAMES[0] == "Base Card"
def test_t1_base_chrome(self):
assert TIER_NAMES[1] == "Base Chrome"
def test_t2_refractor(self):
assert TIER_NAMES[2] == "Refractor"
def test_t3_gold_refractor(self):
assert TIER_NAMES[3] == "Gold Refractor"
def test_t4_superfractor(self):
assert TIER_NAMES[4] == "Superfractor"
# ---------------------------------------------------------------------------
# Slash command: empty roster / no-team scenarios
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_bot():
bot = AsyncMock(spec=commands.Bot)
return bot
@pytest.fixture
def mock_interaction():
interaction = AsyncMock(spec=discord.Interaction)
interaction.response = AsyncMock()
interaction.response.defer = AsyncMock()
interaction.edit_original_response = AsyncMock()
interaction.user = Mock()
interaction.user.id = 12345
return interaction
# ---------------------------------------------------------------------------
# T1-6: TIER_NAMES duplication divergence check
# ---------------------------------------------------------------------------
class TestTierNamesDivergenceCheck:
"""
T1-6: Assert that TIER_NAMES in cogs.refractor and helpers.refractor_notifs
are identical (same keys, same values).
Why: TIER_NAMES is duplicated in two modules. If one is updated and the
other is not (e.g. a tier is renamed or a new tier is added), tier labels
in the /refractor status embed and the tier-up notification embed will
diverge silently. This test acts as a divergence tripwire — it will fail
the moment the two copies fall out of sync, forcing an explicit fix.
"""
def test_tier_names_are_identical_across_modules(self):
"""
Import TIER_NAMES from both modules and assert deep equality.
The test imports the name at call-time rather than at module level to
ensure it always reads the current definition and is not affected by
module-level caching or monkeypatching in other tests.
"""
from cogs.refractor import TIER_NAMES as cog_tier_names
from helpers.refractor_notifs import TIER_NAMES as notifs_tier_names
assert cog_tier_names == notifs_tier_names, (
"TIER_NAMES differs between cogs.refractor and helpers.refractor_notifs. "
"Both copies must be kept in sync. "
f"cogs.refractor: {cog_tier_names!r} "
f"helpers.refractor_notifs: {notifs_tier_names!r}"
)
def test_tier_names_have_same_keys(self):
"""Keys (tier numbers) must be identical in both modules."""
from cogs.refractor import TIER_NAMES as cog_tier_names
from helpers.refractor_notifs import TIER_NAMES as notifs_tier_names
assert set(cog_tier_names.keys()) == set(notifs_tier_names.keys()), (
"TIER_NAMES key sets differ between modules."
)
def test_tier_names_have_same_values(self):
"""Display strings (values) must be identical for every shared key."""
from cogs.refractor import TIER_NAMES as cog_tier_names
from helpers.refractor_notifs import TIER_NAMES as notifs_tier_names
for tier, name in cog_tier_names.items():
assert notifs_tier_names.get(tier) == name, (
f"Tier {tier} name mismatch: "
f"cogs.refractor={name!r}, "
f"helpers.refractor_notifs={notifs_tier_names.get(tier)!r}"
)
# ---------------------------------------------------------------------------
# T2-8: Filter combination — tier=4 + progress="close" yields empty result
# ---------------------------------------------------------------------------
class TestApplyCloseFilterWithAllT4Cards:
"""
T2-8: When all cards in the list are T4 (fully evolved), apply_close_filter
must return an empty list.
Why: T4 cards have no next tier to advance to, so they have no threshold.
The close filter explicitly excludes fully evolved cards (tier >= 4 or
next_threshold is None). If a user passes both tier=4 and progress="close"
to /refractor status, the combined result should be empty — the command
already handles this by showing "No cards are currently close to a tier
advancement." This test documents and protects that behaviour.
"""
def test_all_t4_cards_returns_empty(self):
"""
A list of T4-only card states should produce an empty result from
apply_close_filter, because T4 cards are fully evolved and have no
next threshold to be "close" to.
This is the intended behaviour when tier=4 and progress="close" are
combined: there are no qualifying cards, and the command should show
the "no cards close to advancement" message rather than an empty embed.
"""
t4_cards = [
{"current_tier": 4, "formula_value": 300, "next_threshold": None},
{"current_tier": 4, "formula_value": 500, "next_threshold": None},
{"current_tier": 4, "formula_value": 275, "next_threshold": None},
]
result = apply_close_filter(t4_cards)
assert result == [], (
"apply_close_filter must return [] for fully evolved T4 cards — "
"they have no next threshold and cannot be 'close' to advancement."
)
def test_t4_cards_excluded_even_with_high_formula_value(self):
"""
T4 cards are excluded regardless of their formula_value, since the
filter is based on tier (>= 4) and threshold (None), not raw values.
"""
t4_high_value = {
"current_tier": 4,
"formula_value": 9999,
"next_threshold": None,
}
assert apply_close_filter([t4_high_value]) == []
# ---------------------------------------------------------------------------
# T3-2: Malformed API response handling in format_refractor_entry
# ---------------------------------------------------------------------------
class TestFormatRefractorEntryMalformedInput:
"""
T3-2: format_refractor_entry should not crash when given a card state dict
that is missing expected keys.
Why: API responses can be incomplete due to migration states, partially
populated records, or future schema changes. format_refractor_entry uses
.get() with fallbacks for all keys, so missing fields should gracefully
degrade to sensible defaults ("Unknown" for name, 0 for values) rather than
raising a KeyError or TypeError.
"""
def test_missing_player_name_uses_unknown(self):
"""
When player_name is absent, the output should contain "Unknown" rather
than crashing with a KeyError.
"""
state = {
"card_type": "batter",
"current_tier": 1,
"formula_value": 100,
"next_threshold": 150,
}
result = format_refractor_entry(state)
assert "Unknown" in result
def test_missing_formula_value_uses_zero(self):
"""
When formula_value is absent, the progress calculation should use 0
without raising a TypeError.
"""
state = {
"player_name": "Test Player",
"card_type": "batter",
"current_tier": 1,
"next_threshold": 150,
}
result = format_refractor_entry(state)
assert "0/150" in result
def test_completely_empty_dict_does_not_crash(self):
"""
An entirely empty dict should produce a valid (if sparse) string using
all fallback values, not raise any exception.
"""
result = format_refractor_entry({})
# Should not raise; output should be a string with two lines
assert isinstance(result, str)
lines = result.split("\n")
assert len(lines) == 2
def test_missing_card_type_uses_raw_fallback(self):
"""
When card_type is absent, the code defaults to 'batter' internally
(via .get("card_type", "batter")), so "PA+TB×2" should appear as the
formula label.
"""
state = {
"player_name": "Test Player",
"current_tier": 1,
"formula_value": 50,
"next_threshold": 100,
}
result = format_refractor_entry(state)
assert "PA+TB×2" in result
# ---------------------------------------------------------------------------
# T3-3: Progress bar boundary precision
# ---------------------------------------------------------------------------
class TestRenderProgressBarBoundaryPrecision:
"""
T3-3: Verify the progress bar behaves correctly at edge values — near zero,
near full, exactly at extremes, and for negative input.
Why: Off-by-one errors in rounding or integer truncation can make a nearly-
full bar look full (or vice versa), confusing users about how close their
card is to a tier advancement. Defensive handling of negative values ensures
no bar is rendered longer than its declared width.
"""
def test_one_of_hundred_shows_mostly_empty(self):
"""
1/100 = 1% — should produce a bar with 0 or 1 filled segment and the
rest empty. The bar must not appear more than minimally filled.
"""
bar = render_progress_bar(1, 100)
# Interior is 10 chars: count '=' vs '-'
interior = bar[1:-1] # strip '[' and ']'
filled_count = interior.count("=")
assert filled_count <= 1, (
f"1/100 should show 0 or 1 filled segment, got {filled_count}: {bar!r}"
)
def test_ninety_nine_of_hundred_is_nearly_full(self):
"""
99/100 = 99% — should produce a bar with 9 or 10 filled segments.
The bar must NOT be completely empty or show fewer than 9 filled.
"""
bar = render_progress_bar(99, 100)
interior = bar[1:-1]
filled_count = interior.count("=")
assert filled_count >= 9, (
f"99/100 should show 9 or 10 filled segments, got {filled_count}: {bar!r}"
)
# But it must not overflow the bar width
assert len(interior) == 10
def test_zero_of_hundred_is_completely_empty(self):
"""0/100 = all dashes — re-verify the all-empty baseline."""
assert render_progress_bar(0, 100) == "[----------]"
def test_negative_current_does_not_overflow_bar(self):
"""
A negative formula_value (data anomaly) must not produce a bar with
more filled segments than the width. The min(..., 1.0) clamp in
render_progress_bar should handle this, but this test guards against
a future refactor removing the clamp.
"""
bar = render_progress_bar(-5, 100)
interior = bar[1:-1]
# No filled segments should exist for a negative value
filled_count = interior.count("=")
assert filled_count == 0, (
f"Negative current should produce 0 filled segments, got {filled_count}: {bar!r}"
)
# Bar width must be exactly 10
assert len(interior) == 10
# ---------------------------------------------------------------------------
# T3-4: RP formula label
# ---------------------------------------------------------------------------
class TestRPFormulaLabel:
"""
T3-4: Verify that relief pitchers (card_type="rp") show the "IP+K" formula
label in format_refractor_entry output.
Why: FORMULA_LABELS maps both "sp" and "rp" to "IP+K". The existing test
suite only verifies "sp" (via the sp_state fixture). Adding "rp" explicitly
prevents a future refactor from accidentally giving RPs a different label
or falling through to the raw card_type fallback.
"""
def test_rp_formula_label_is_ip_plus_k(self):
"""
A card with card_type="rp" must show "IP+K" as the formula label
in its progress line.
"""
rp_state = {
"player_name": "Edwin Diaz",
"card_type": "rp",
"current_tier": 1,
"formula_value": 45,
"next_threshold": 60,
}
result = format_refractor_entry(rp_state)
assert "IP+K" in result, (
f"Relief pitcher card should show 'IP+K' formula label, got: {result!r}"
)
# ---------------------------------------------------------------------------
# T3-5: Unknown card_type fallback
# ---------------------------------------------------------------------------
class TestUnknownCardTypeFallback:
"""
T3-5: format_refractor_entry should use the raw card_type string as the
formula label when the type is not in FORMULA_LABELS, rather than crashing.
Why: FORMULA_LABELS only covers "batter", "sp", and "rp". If the API
introduces a new card type (e.g. "util" for utility players) before the
bot is updated, FORMULA_LABELS.get(card_type, card_type) will fall back to
the raw string. This test ensures that fallback path produces readable
output rather than an error, and explicitly documents what to expect.
"""
def test_unknown_card_type_uses_raw_string_as_label(self):
"""
card_type="util" is not in FORMULA_LABELS. The output should include
"util" as the formula label (the raw fallback) and must not raise.
"""
util_state = {
"player_name": "Ben Zobrist",
"card_type": "util",
"current_tier": 2,
"formula_value": 80,
"next_threshold": 120,
}
result = format_refractor_entry(util_state)
assert "util" in result, (
f"Unknown card_type should appear verbatim as the formula label, got: {result!r}"
)
def test_unknown_card_type_does_not_crash(self):
"""
Any unknown card_type must produce a valid two-line string without
raising an exception.
"""
state = {
"player_name": "Test Player",
"card_type": "dh",
"current_tier": 1,
"formula_value": 30,
"next_threshold": 50,
}
result = format_refractor_entry(state)
assert isinstance(result, str)
assert len(result.split("\n")) == 2
# ---------------------------------------------------------------------------
# Slash command: empty roster / no-team scenarios
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_refractor_status_no_team(mock_bot, mock_interaction):
"""
When the user has no team, the command replies with a signup prompt
and does not call db_get.
Why: get_team_by_owner returning None means the user is unregistered;
the command must short-circuit before hitting the API.
"""
from cogs.refractor import Refractor
cog = Refractor(mock_bot)
with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=None)):
with patch("cogs.refractor.db_get", new=AsyncMock()) as mock_db:
await cog.refractor_status.callback(cog, mock_interaction)
mock_db.assert_not_called()
call_kwargs = mock_interaction.edit_original_response.call_args
content = call_kwargs.kwargs.get("content", "")
assert "newteam" in content.lower() or "team" in content.lower()
@pytest.mark.asyncio
async def test_refractor_status_empty_roster(mock_bot, mock_interaction):
"""
When the API returns an empty card list, the command sends an
informative 'no data' message rather than an empty embed.
Why: An empty list is valid (team has no refractor cards yet);
the command should not crash or send a blank embed.
"""
from cogs.refractor import Refractor
cog = Refractor(mock_bot)
team = {"id": 1, "sname": "Test"}
with patch("cogs.refractor.get_team_by_owner", new=AsyncMock(return_value=team)):
with patch("cogs.refractor.db_get", new=AsyncMock(return_value={"cards": []})):
await cog.refractor_status.callback(cog, mock_interaction)
call_kwargs = mock_interaction.edit_original_response.call_args
content = call_kwargs.kwargs.get("content", "")
assert "no refractor data" in content.lower()