+52 IT/UT to close IT/UT-only coverage gaps (93% → 96%) — full suite 983 tests in 47s ; UTs in epic/tests/unit/test_models.py — TarotCardEmanationForTest (4) covers emanation_for(polarity) w. levity/gravity overrides + fallback to name_title for cards w.o a polarity split (cards 48-49 are the only polarity-split cards in the deck so this method is sparsely exercised by ITs); TarotCardReversalForTest (4) covers reversal_for(polarity) w. polarity-split + reversal_qualifier fallback + further fallthrough to emanation_for; TarotCardNameSplitTest (4) covers name_group/name_title colon-split parsing (prefix-w-colon / suffix / no-colon edge); TarotCardCautionsJsonTest (2) covers the cautions_json JSON serialiser ; UTs in epic/tests/unit/test_utils.py — PlanetHouseFallbackTest +1 happy-path test (degree=15 lands in house 1 w. sequential cusps) for the normal cusp-match branch alongside the existing pathological fallback test; TopCapacitorsTest (6) covers all top_capacitors() branches — empty dict / None / all-zero counts (the L56 max(counts.values()) <= 0 fallback that was uncovered) / single-winner / tie-clockwise-order / enriched dict {"count":N} input shape ; ITs in epic/tests/integrated/test_models.py — TarotDeckDrawTest extended w. 5 tests for remaining_count (happy + no-deck-variant fallback to 0) + draw() happy-path (returns n tuples of (TarotCard, bool) / appends to drawn_card_ids / never repeats cards across consecutive draws); existing ValueError + shuffle tests preserved ; ITs in epic/tests/integrated/test_views.py — SigEventRetractionTest (4 tests) covers the three data["retracted"] = True paths that the FT test_game_room_select_sig.py walks transitively but no IT pins directly: sig_unready retracts prior SIG_READY (L937), sig_ready retracts prior SIG_UNREADY (L907), sig_reserve action=release while ready retracts prior SIG_READY + records fresh SIG_UNREADY (L823); SigReserveInvalidCardIdTest (1) covers TarotCard.DoesNotExist → 400 (L840-841) ; SigSelectGravityContextTest (3) covers the user_polarity = 'gravity' branch (L322) + the gravity_sig_cards lookup (L357) — all existing SIG_SELECT context tests use the founder-as-PC-levity setup so these branches sat uncovered; logs in as gamers[5] (BC role) + asserts user_polarity + sig_cards match gravity_sig_cards() output ; SeaDeckViewTest (7) mirrors the test_game_room_select_sea.py FT but isolates the JSON contract — covers 403 when unseated, empty halves when seat has no deck_variant (L1255-1256 early-out), two-halves shape, ~even split, card_dict keys (id/name/arcana/corner_rank/suit_icon/name_group/name_title/reversed/qualifiers), reversed field is bool, claimed-significator exclusion via room.table_seats.exclude(significator__isnull=True) ; ITs in dashboard/tests/integrated/test_views.py — ProfileViewTest +2 (reserved-handle "adman" rejection — L116-117: username stays unchanged + redirect to /); KitBagViewTest (3) covers the kit_bag view's panel render w. TITHE-sort branch (L169-175) + login guard ; ITs in dashboard/tests/integrated/test_sky_views.py — SkyViewTest +2 (saved birth datetime renders in user's sky_birth_tz via astimezone L300-306 — 16:00 UTC → 12:00 EDT; invalid-tz string triggers ZoneInfoNotFoundError → swallowed pass → UTC fallback at 16:00) ; ITs in gameboard/tests/integrated/test_views.py — EquipTrinketViewTest +2 (POST equips trinket + returns 204 — L83-85; non-owner POST returns 404 via get_object_or_404); UnequipTrinketViewTest +2 (POST clears matching equipped_trinket — L107-110; POST of non-matching token is a 204 no-op, the implicit else branch) ; .coveragerc omit gains */reset_staging_db.py per user — mgmt cmd was the only 0%-stmt module that wasn't exercised by tests at all + we agreed it's deliberately untested staging-side code ; palette-monochrome-dark rebalance in rootvars.scss — --quiUser/--sixUser/--sepUser remapped to (secAg / quaAg / priPt) instead of (quaAg / terAg / secAg), shifting the secondary/subtle/deep-subtle anchors up the silver gradient so the palette reads more cleanly under the new sig-stage card colours from 3242873 ; uncovered remnants from earlier analysis intentionally left in place — consumers.py at 68% (channels-tag tests excluded; would need --tag=channels run), Carte Blanche slot navigation + sky_dice + tarot_deck preview view paths (the "bigger investments" tier from session triage; FT-covered + the IT setup is heavier than the immediate value), defensive except fallbacks that need contrived inputs to fire, and a handful of __str__s/pass branches not worth a test apiece — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-05-18 01:07:13 -04:00
parent 3242873625
commit bc77296dd4
9 changed files with 526 additions and 4 deletions

View File

@@ -649,6 +649,57 @@ class TarotDeckDrawTest(TestCase):
td.refresh_from_db()
self.assertEqual(td.drawn_card_ids, [])
def test_remaining_count_subtracts_drawn_from_total(self):
from apps.epic.models import TarotDeck
deck_variant = DeckVariant.objects.first()
td = TarotDeck.objects.create(
room=self.room,
deck_variant=deck_variant,
drawn_card_ids=[],
)
self.assertEqual(td.remaining_count, deck_variant.card_count)
td.drawn_card_ids = list(
TarotCard.objects.filter(deck_variant=deck_variant).values_list('id', flat=True)[:5]
)
td.save()
self.assertEqual(td.remaining_count, deck_variant.card_count - 5)
def test_remaining_count_zero_when_no_deck_variant(self):
from apps.epic.models import TarotDeck
td = TarotDeck.objects.create(room=self.room, deck_variant=None)
self.assertEqual(td.remaining_count, 0)
def test_draw_returns_n_tuples_of_card_and_bool(self):
from apps.epic.models import TarotDeck
deck_variant = DeckVariant.objects.first()
td = TarotDeck.objects.create(room=self.room, deck_variant=deck_variant)
drawn = td.draw(3)
self.assertEqual(len(drawn), 3)
for card, is_reversed in drawn:
self.assertIsInstance(card, TarotCard)
self.assertIsInstance(is_reversed, bool)
def test_draw_appends_card_ids_to_drawn_card_ids(self):
from apps.epic.models import TarotDeck
deck_variant = DeckVariant.objects.first()
td = TarotDeck.objects.create(room=self.room, deck_variant=deck_variant)
drawn = td.draw(4)
td.refresh_from_db()
self.assertEqual(len(td.drawn_card_ids), 4)
for card, _ in drawn:
self.assertIn(card.id, td.drawn_card_ids)
def test_draw_excludes_already_drawn_cards(self):
"""Subsequent draws never repeat cards from the existing drawn_card_ids."""
from apps.epic.models import TarotDeck
deck_variant = DeckVariant.objects.first()
td = TarotDeck.objects.create(room=self.room, deck_variant=deck_variant)
first = td.draw(5)
first_ids = {card.id for card, _ in first}
second = td.draw(5)
second_ids = {card.id for card, _ in second}
self.assertFalse(first_ids & second_ids)
# ── sig_deck_cards with no equipped deck ─────────────────────────────────────

View File

@@ -2259,3 +2259,222 @@ class SkySaveViewTest(TestCase):
char = Character.objects.get(seat=pc_seat)
self.assertEqual(char.significator, sig_card)
# ── SIG event-retraction branches ────────────────────────────────────────────
# The provenance scrolls use a `data["retracted"] = True` flag to soft-cancel
# prior events when a gamer reverses themselves (WAIT NVM after SAVE SIG, etc).
# These three branches in sig_reserve / sig_ready are the load-bearing ones —
# without them a recanted action stays visible in the billboard scrollback.
class SigEventRetractionTest(TestCase):
"""`data["retracted"] = True` writes on the three reverse-direction paths."""
def setUp(self):
self.room, self.gamers, self.earthman, self.card = _full_sig_setUp(self)
# PC (founder) already logged in; reserve + go ready so subsequent
# actions have prior SIG_READY events to retract.
self.reserve_url = reverse("epic:sig_reserve", kwargs={"room_id": self.room.id})
self.ready_url = reverse("epic:sig_ready", kwargs={"room_id": self.room.id})
def _reserve(self):
return self.client.post(self.reserve_url, data={
"card_id": self.card.id, "action": "reserve",
})
def _ready(self, action="ready"):
return self.client.post(self.ready_url, data={"action": action})
def test_sig_unready_retracts_prior_sig_ready_event(self):
"""sig_ready action=unready flips `data["retracted"]=True` on the most
recent un-retracted SIG_READY event for this actor (views.py L937)."""
self._reserve()
self._ready(action="ready")
prior = self.room.events.filter(
actor=self.gamers[0], verb=GameEvent.SIG_READY
).last()
self.assertFalse(prior.data.get("retracted"), "precondition: not yet retracted")
self._ready(action="unready")
prior.refresh_from_db()
self.assertTrue(prior.data.get("retracted"))
def test_sig_ready_retracts_prior_sig_unready_event(self):
"""sig_ready action=ready retracts the most recent un-retracted
SIG_UNREADY event (views.py L907) — the cancellation is now moot."""
self._reserve()
self._ready(action="ready")
self._ready(action="unready")
prior_unready = self.room.events.filter(
actor=self.gamers[0], verb=GameEvent.SIG_UNREADY
).last()
self.assertFalse(prior_unready.data.get("retracted"))
self._ready(action="ready")
prior_unready.refresh_from_db()
self.assertTrue(prior_unready.data.get("retracted"))
def test_sig_release_while_ready_retracts_prior_sig_ready_event(self):
"""sig_reserve action=release on a ready reservation acts as implicit
WAIT NVM — retracts the most recent SIG_READY (views.py L823)."""
self._reserve()
self._ready(action="ready")
prior = self.room.events.filter(
actor=self.gamers[0], verb=GameEvent.SIG_READY
).last()
self.assertFalse(prior.data.get("retracted"))
self.client.post(self.reserve_url, data={
"card_id": self.card.id, "action": "release",
})
prior.refresh_from_db()
self.assertTrue(prior.data.get("retracted"))
def test_sig_release_while_ready_records_sig_unready_event(self):
"""Same release-while-ready path also records a fresh SIG_UNREADY
(the implicit cancellation event)."""
self._reserve()
self._ready(action="ready")
unready_count_before = self.room.events.filter(
actor=self.gamers[0], verb=GameEvent.SIG_UNREADY
).count()
self.client.post(self.reserve_url, data={
"card_id": self.card.id, "action": "release",
})
self.assertEqual(
self.room.events.filter(
actor=self.gamers[0], verb=GameEvent.SIG_UNREADY
).count(),
unready_count_before + 1,
)
# ── SIG_RESERVE invalid card-id branch ───────────────────────────────────────
class SigReserveInvalidCardIdTest(TestCase):
"""sig_reserve POSTed with a card_id that doesn't exist returns 400."""
def setUp(self):
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
self.url = reverse("epic:sig_reserve", kwargs={"room_id": self.room.id})
def test_unknown_card_id_returns_400(self):
"""TarotCard.DoesNotExist branch (views.py L840-841)."""
response = self.client.post(self.url, data={
"card_id": 999999, "action": "reserve",
})
self.assertEqual(response.status_code, 400)
# ── SIG_SELECT gravity-polarity rendering ────────────────────────────────────
class SigSelectGravityContextTest(TestCase):
"""SIG_SELECT room context for a gravity-polarity gamer.
Covers the `user_polarity = 'gravity'` branch (views.py L322) and the
gravity_sig_cards lookup (L357) — both fall through the cracks of the
default founder-as-PC-levity tests."""
def setUp(self):
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
# gamers[5] is BC → gravity polarity
self.bc = self.gamers[5]
self.client.force_login(self.bc)
self.url = reverse("epic:room", kwargs={"room_id": self.room.id})
def test_gravity_gamer_room_context_has_gravity_polarity(self):
response = self.client.get(self.url)
self.assertEqual(response.context["user_polarity"], "gravity")
def test_gravity_gamer_sees_gravity_sig_cards(self):
"""Levity + gravity get the same 16 court cards (filtered by major-arcana
Note unlocks); this test just asserts the gravity branch was taken."""
from apps.epic.models import gravity_sig_cards
response = self.client.get(self.url)
# Same underlying card set; assertion is that the context was populated
# (the gravity branch returned, vs falling into the empty `else`).
self.assertEqual(
list(response.context["sig_cards"]),
list(gravity_sig_cards(self.room, self.bc)),
)
def test_gravity_gamer_sig_card_set_non_empty(self):
response = self.client.get(self.url)
self.assertGreater(len(response.context["sig_cards"]), 0)
# ── SEA_DECK draw view ───────────────────────────────────────────────────────
class SeaDeckViewTest(TestCase):
"""sea_deck — JSON view returning shuffled levity + gravity halves.
Mirrors the FT in test_game_room_select_sea.py:DRAW SEA — that test walks
the full UI; this one isolates the JSON contract + filter semantics."""
def setUp(self):
self.room, self.gamers, self.earthman, _ = _full_sig_setUp(self)
self.room.table_status = Room.SKY_SELECT
self.room.save()
# Use PC seat (founder) — already logged in by _full_sig_setUp
self.url = reverse("epic:sea_deck", kwargs={"room_id": self.room.id})
def test_returns_403_when_not_seated(self):
outsider = User.objects.create(email="outsider@test.io")
self.client.force_login(outsider)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 403)
def test_returns_empty_halves_when_seat_has_no_deck_variant(self):
"""sea_deck early-outs to {levity:[],gravity:[]} if the seat hasn't
committed a deck — guards against null deck_variant FK access."""
TableSeat.objects.filter(room=self.room).update(deck_variant=None)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data, {"levity": [], "gravity": []})
def test_returns_two_halves(self):
response = self.client.get(self.url)
data = response.json()
self.assertIn("levity", data)
self.assertIn("gravity", data)
def test_card_count_roughly_split_between_halves(self):
"""Total card pool is split in half — within 1 of perfectly even."""
response = self.client.get(self.url)
data = response.json()
self.assertAlmostEqual(len(data["levity"]), len(data["gravity"]), delta=1)
def test_card_dict_contains_expected_keys(self):
response = self.client.get(self.url)
data = response.json()
sample = data["levity"][0]
for key in (
"id", "name", "arcana", "corner_rank", "suit_icon",
"name_group", "name_title", "reversed",
"levity_qualifier", "gravity_qualifier",
):
self.assertIn(key, sample, f"missing key {key!r} in card dict")
def test_reversed_field_is_boolean(self):
response = self.client.get(self.url)
data = response.json()
for card in data["levity"] + data["gravity"]:
self.assertIsInstance(card["reversed"], bool)
def test_excludes_claimed_significators(self):
"""A card already set as a seat.significator must not appear in either
half — it's been claimed for the game and is out of the sea-draw pool."""
sig_card = TarotCard.objects.get(
deck_variant=self.earthman, arcana="MIDDLE", suit="BRANDS", number=11
)
pc_seat = TableSeat.objects.get(room=self.room, role="PC")
pc_seat.significator = sig_card
pc_seat.save()
response = self.client.get(self.url)
data = response.json()
all_ids = {c["id"] for c in data["levity"]} | {c["id"] for c in data["gravity"]}
self.assertNotIn(sig_card.id, all_ids)