my-sea voice: cap visitors at 5 (6 seats) + free a seat on leave — TDD
Phase 5a of the my-sea voice batch (user-spec 2026-05-29). The owner holds 1C; at most 5 visitors fill 2C–6C, which also caps the voice mesh (voice requires a deposited seat, so seat-capping caps membership). - SeaInvite: MY_SEA_MAX_VISITORS=5 + present_count(owner) / table_has_room(owner) classmethods (present = ACCEPTED + deposited + not LEFT). - my_sea_visit_insert_token: a fresh deposit into a full table is bounced (?full=1, no token spent, no seat); a visitor who BYEs frees their seat (is_present → False) for the next visitor. - my_sea_visit_gate: context → the gate shows 'TABLE FULL' + inert rails instead of INSERT TOKEN for a not-yet-present visitor. - 6 capacity ITs (count/room, full-table bounce, leave-frees-seat, gate flag, already-seated not blocked). 291 gameboard ITs green. Remaining Phase 5 (live-verify / needs a spec call): disconnect visuals (--priRd/.fa-ban, item 7) + the true Web-Audio equalizer (item 5) + consumer- level voice-member enforcement + multi-seat (3C–6C) spectator viz. Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -268,6 +268,85 @@ class MySeaVisitLeaveTest(TestCase):
|
||||
self.assertEqual(self.client.post(self.url).status_code, 403)
|
||||
|
||||
|
||||
class MySeaVisitCapacityTest(TestCase):
|
||||
"""Phase 5 (2026-05-29) — at most 5 visitors seat at a bud's my-sea
|
||||
(owner 1C + 2C–6C). A full table bounces a fresh deposit; a visitor who
|
||||
leaves frees their seat for someone else."""
|
||||
|
||||
def setUp(self):
|
||||
from apps.gameboard.models import MY_SEA_MAX_VISITORS
|
||||
self.MAX = MY_SEA_MAX_VISITORS
|
||||
self.owner = _owner_with_sig()
|
||||
|
||||
def _accepted_invitee(self, n, present=False):
|
||||
u = User.objects.create(email=f"v{n}@test.io", username=f"v{n}")
|
||||
inv = SeaInvite.objects.create(
|
||||
owner=self.owner, invitee=u, invitee_email=u.email,
|
||||
status=SeaInvite.ACCEPTED, accepted_at=timezone.now(),
|
||||
)
|
||||
if present:
|
||||
inv.token_deposited_at = timezone.now()
|
||||
inv.voice_until = timezone.now() + timedelta(hours=24)
|
||||
inv.save()
|
||||
return u, inv
|
||||
|
||||
def _fill_table(self):
|
||||
# MAX present visitors → the table is full.
|
||||
for n in range(self.MAX):
|
||||
self._accepted_invitee(n, present=True)
|
||||
|
||||
def test_present_count_and_table_has_room(self):
|
||||
self._fill_table()
|
||||
self.assertEqual(SeaInvite.present_count(self.owner), self.MAX)
|
||||
self.assertFalse(SeaInvite.table_has_room(self.owner))
|
||||
|
||||
def test_full_table_bounces_a_fresh_deposit(self):
|
||||
self._fill_table()
|
||||
latecomer, inv = self._accepted_invitee(99, present=False)
|
||||
self.client.force_login(latecomer)
|
||||
resp = self.client.post(
|
||||
reverse("my_sea_visit_insert_token", args=[self.owner.id]))
|
||||
inv.refresh_from_db()
|
||||
self.assertIsNone(inv.token_deposited_at) # no seat taken
|
||||
self.assertFalse(inv.is_present)
|
||||
self.assertIn("full=1", resp["Location"])
|
||||
|
||||
def test_leaving_frees_a_seat_for_the_next_visitor(self):
|
||||
self._fill_table()
|
||||
# One present visitor leaves → a seat opens.
|
||||
leaver = SeaInvite.objects.filter(
|
||||
owner=self.owner, token_deposited_at__isnull=False).first()
|
||||
self.client.force_login(leaver.invitee)
|
||||
self.client.post(reverse("my_sea_visit_leave", args=[self.owner.id]))
|
||||
self.assertTrue(SeaInvite.table_has_room(self.owner))
|
||||
# The latecomer can now deposit into the freed seat.
|
||||
latecomer, inv = self._accepted_invitee(99, present=False)
|
||||
self.client.force_login(latecomer)
|
||||
self.client.post(
|
||||
reverse("my_sea_visit_insert_token", args=[self.owner.id]))
|
||||
inv.refresh_from_db()
|
||||
self.assertTrue(inv.is_present)
|
||||
|
||||
def test_gate_flags_table_full_for_a_non_present_visitor(self):
|
||||
self._fill_table()
|
||||
latecomer, _ = self._accepted_invitee(99, present=False)
|
||||
self.client.force_login(latecomer)
|
||||
ctx = self.client.get(
|
||||
reverse("my_sea_visit_gate", args=[self.owner.id])).context
|
||||
self.assertTrue(ctx["table_full"])
|
||||
|
||||
def test_already_present_visitor_is_not_blocked_by_a_full_table(self):
|
||||
# The 5th present visitor re-POSTing insert is a no-op (already seated),
|
||||
# never a "full" bounce.
|
||||
self._fill_table()
|
||||
seated = SeaInvite.objects.filter(
|
||||
owner=self.owner, token_deposited_at__isnull=False).first()
|
||||
self.client.force_login(seated.invitee)
|
||||
ctx = self.client.get(
|
||||
reverse("my_sea_visit_gate", args=[self.owner.id])).context
|
||||
self.assertFalse(ctx["table_full"])
|
||||
|
||||
|
||||
class MySeaVoiceContextTest(TestCase):
|
||||
"""Phase C — the #id_voice_btn lights up (voice_active) for both the
|
||||
owner and the present invitee while the 24h voice window is open, keyed on
|
||||
|
||||
Reference in New Issue
Block a user