[READY][Gameplay] #310 Host transition idempotency and error catalog for scoreboard -> next round / finish #320

Merged
agw merged 45 commits from dev/issue-310-host-transition-idempotency-v2 into main 2026-03-18 06:52:04 +01:00
2 changed files with 44 additions and 1 deletions
Showing only changes of commit a102a72a77 - Show all commits

View File

@@ -2,6 +2,7 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from django.db import transaction from django.db import transaction
from django.utils import timezone
from .models import GameSession, Guess, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ScoreEvent from .models import GameSession, Guess, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ScoreEvent
@@ -40,9 +41,16 @@ def get_current_round_question(session: GameSession) -> RoundQuestion | None:
def reset_round_question_bootstrap_state(round_question: RoundQuestion) -> RoundQuestion: def reset_round_question_bootstrap_state(round_question: RoundQuestion) -> RoundQuestion:
Guess.objects.filter(round_question=round_question).delete() Guess.objects.filter(round_question=round_question).delete()
LieAnswer.objects.filter(round_question=round_question).delete() LieAnswer.objects.filter(round_question=round_question).delete()
update_fields: list[str] = []
if round_question.mixed_answers: if round_question.mixed_answers:
round_question.mixed_answers = [] round_question.mixed_answers = []
round_question.save(update_fields=["mixed_answers"]) update_fields.append("mixed_answers")
round_question.shown_at = timezone.now()
update_fields.append("shown_at")
round_question.save(update_fields=update_fields)
return round_question return round_question

View File

@@ -1,7 +1,9 @@
from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.test import TestCase from django.test import TestCase
from django.utils import timezone
from fupogfakta.models import Category, GameSession, Guess, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ScoreEvent from fupogfakta.models import Category, GameSession, Guess, LieAnswer, Player, Question, RoundConfig, RoundQuestion, ScoreEvent
from fupogfakta.payloads import build_lie_started_payload, build_reveal_payload from fupogfakta.payloads import build_lie_started_payload, build_reveal_payload
@@ -100,6 +102,39 @@ class FupOgFaktaExtractionSliceTests(TestCase):
with self.assertRaisesMessage(ValueError, "next_round_invalid_phase"): with self.assertRaisesMessage(ValueError, "next_round_invalid_phase"):
start_next_round(self.session) start_next_round(self.session)
def test_start_next_round_refreshes_shown_at_for_reused_bootstrap_question(self):
self.session.status = GameSession.Status.SCOREBOARD
self.session.save(update_fields=["status"])
stale_shown_at = timezone.now() - timedelta(minutes=10)
stale_round_question = RoundQuestion.objects.create(
session=self.session,
round_number=2,
question=self.question_two,
correct_answer=self.question_two.correct_answer,
shown_at=stale_shown_at,
mixed_answers=["Stale truth", "Stale lie"],
)
LieAnswer.objects.create(round_question=stale_round_question, player=self.alice, text="Stale lie")
Guess.objects.create(
round_question=stale_round_question,
player=self.bob,
selected_text="Stale truth",
is_correct=True,
)
before_transition = timezone.now()
result = start_next_round(self.session)
after_transition = timezone.now()
stale_round_question.refresh_from_db()
self.assertEqual(result.round_question.id, stale_round_question.id)
self.assertGreaterEqual(stale_round_question.shown_at, before_transition)
self.assertLessEqual(stale_round_question.shown_at, after_transition)
self.assertNotEqual(stale_round_question.shown_at, stale_shown_at)
self.assertEqual(stale_round_question.mixed_answers, [])
self.assertEqual(stale_round_question.lies.count(), 0)
self.assertEqual(stale_round_question.guesses.count(), 0)
def test_finish_game_moves_scoreboard_transition_into_service(self): def test_finish_game_moves_scoreboard_transition_into_service(self):
self.session.status = GameSession.Status.SCOREBOARD self.session.status = GameSession.Status.SCOREBOARD
self.session.save(update_fields=["status"]) self.session.save(update_fields=["status"])