mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
fix(multiplayer): enforce client state transitions
This commit is contained in:
@@ -14,7 +14,8 @@ func test_ticket_projection_accepts_ordered_updates_and_exposes_cancel() -> void
|
||||
func test_ticket_projection_accepts_authoritative_accepted_phase() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
assert_true(state.begin_queue("ticket-accepted", "ranked"), "queue setup succeeds")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-accepted", "revision": 1, "state": "ACCEPTED", "playlist": "ranked"}), "accepted queue phase is valid")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-accepted", "revision": 1, "state": "PROPOSED", "playlist": "ranked"}), "proposal phase applies")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-accepted", "revision": 2, "state": "ACCEPTED", "playlist": "ranked"}), "accepted queue phase is valid")
|
||||
assert_eq(state.phase, MatchmakingState.ACCEPTED, "accepted phase remains visible instead of forcing resync")
|
||||
assert_true(not state.can_cancel(), "accepted match cannot be cancelled as a queue ticket")
|
||||
|
||||
@@ -23,7 +24,12 @@ func test_ticket_projection_accepts_post_match_lifecycle_states() -> void:
|
||||
for phase in ["ASSIGNED", "CONNECTING", "LIVE", "RESULT_PENDING", "COMPLETED"]:
|
||||
var state := MatchmakingState.new()
|
||||
assert_true(state.begin_queue("ticket-" + phase, "casual"), "queue setup succeeds for " + phase)
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-" + phase, "revision": 1, "state": phase, "playlist": "casual"}), "post-match phase is valid: " + phase)
|
||||
var revision := 1
|
||||
for next_phase in ["PROPOSED", "ACCEPTED", "ALLOCATING", "PROCESS_READY", "ASSIGNMENT_READY", "ASSIGNED", "CONNECTING", "LIVE", "RESULT_PENDING", "COMPLETED"]:
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-" + phase, "revision": revision, "state": next_phase, "playlist": "casual"}), "lifecycle phase applies: " + next_phase)
|
||||
revision += 1
|
||||
if next_phase == phase:
|
||||
break
|
||||
assert_eq(state.phase, phase, "post-match phase remains visible: " + phase)
|
||||
assert_true(not state.can_cancel(), "post-match phase cannot cancel: " + phase)
|
||||
|
||||
@@ -56,6 +62,19 @@ func test_duplicate_conflict_and_stale_updates_are_safe() -> void:
|
||||
assert_eq(state.phase, MatchmakingState.PROPOSED, "stale update cannot mutate state")
|
||||
|
||||
|
||||
func test_higher_revision_cannot_jump_or_rewind_the_authoritative_lifecycle() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
state.begin_queue("ticket-transition", "casual")
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-transition", "revision": 1, "state": "PROPOSED", "playlist": "casual"}), "legal transition applies")
|
||||
assert_true(not state.apply_ticket_update({"ticket_id": "ticket-transition", "revision": 2, "state": "LIVE", "playlist": "casual"}), "higher revision cannot jump phases")
|
||||
assert_eq(state.phase, MatchmakingState.PROPOSED, "illegal jump cannot mutate phase")
|
||||
state.needs_resync = false
|
||||
assert_true(state.apply_ticket_update({"ticket_id": "ticket-transition", "revision": 2, "state": "ACCEPTED", "playlist": "casual"}), "legal next transition applies")
|
||||
state.needs_resync = false
|
||||
assert_true(not state.apply_ticket_update({"ticket_id": "ticket-transition", "revision": 3, "state": "PROPOSED", "playlist": "casual"}), "higher revision cannot rewind after acceptance")
|
||||
assert_eq(state.phase, MatchmakingState.ACCEPTED, "illegal rewind cannot mutate phase")
|
||||
|
||||
|
||||
func test_proposal_terminal_states_are_visible_and_not_cancellable() -> void:
|
||||
var state := MatchmakingState.new()
|
||||
state.begin_queue("ticket-1", "casual")
|
||||
|
||||
Reference in New Issue
Block a user