extends "res://tests/test_case.gd" const MatchmakingState = preload("res://scripts/matchmaking_state.gd") func test_ticket_projection_accepts_ordered_updates_and_exposes_cancel() -> void: var state := MatchmakingState.new() assert_true(state.begin_queue("ticket-1", "ranked"), "valid queue starts in QUEUED") assert_true(state.apply_ticket_update({"ticket_id": "ticket-1", "revision": 1, "state": "PROPOSED", "playlist": "ranked"}), "next revision applies") assert_eq(state.phase, MatchmakingState.PROPOSED, "proposal is visible") assert_true(state.can_cancel(), "authoritative cancel remains available before allocation") func test_ticket_projection_rejects_gap_and_wrong_ticket_without_mutation() -> void: var state := MatchmakingState.new() state.begin_queue("ticket-1", "casual") var resync_id := "" state.resync_required.connect(func(id: String) -> void: resync_id = id) assert_true(not state.apply_ticket_update({"ticket_id": "ticket-2", "revision": 1, "state": "PROPOSED"}), "another player's ticket is rejected") assert_eq(resync_id, "ticket-1", "wrong resource requests recovery for current ticket") assert_eq(state.phase, MatchmakingState.QUEUED, "invalid update cannot mutate phase") assert_true(state.needs_resync, "invalid identity is visible to recovery") state.needs_resync = false assert_true(not state.apply_ticket_update({"ticket_id": "ticket-1", "revision": 3, "state": "ALLOCATING"}), "revision gap is rejected") assert_eq(state.phase, MatchmakingState.QUEUED, "gap cannot skip authoritative state") func test_duplicate_conflict_and_stale_updates_are_safe() -> void: var state := MatchmakingState.new() state.begin_queue("ticket-1", "casual") var update := {"ticket_id": "ticket-1", "revision": 1, "state": "PROPOSED", "playlist": "casual", "expires_at_unix": 100} assert_true(state.apply_ticket_update(update), "first update applies") assert_true(state.apply_ticket_update(update), "identical duplicate is idempotent") assert_true(not state.apply_ticket_update({"ticket_id": "ticket-1", "revision": 1, "state": "QUEUED", "playlist": "casual", "expires_at_unix": 100}), "same-revision conflict requests recovery") assert_eq(state.phase, MatchmakingState.PROPOSED, "conflicting replay cannot rewind state") assert_true(not state.apply_ticket_update({"ticket_id": "ticket-1", "revision": 0, "state": "QUEUED"}), "stale update is ignored") assert_eq(state.phase, MatchmakingState.PROPOSED, "stale update cannot mutate state") func test_proposal_terminal_states_are_visible_and_not_cancellable() -> void: var state := MatchmakingState.new() state.begin_queue("ticket-1", "casual") assert_true(state.apply_proposal_update({"proposal_id": "proposal-1", "revision": 1, "state": "OPEN"}), "open proposal applies") assert_eq(state.phase, MatchmakingState.PROPOSED, "open proposal is visible") assert_true(state.apply_proposal_update({"proposal_id": "proposal-1", "revision": 2, "state": "DECLINED"}), "declined proposal applies") assert_eq(state.phase, MatchmakingState.FAILED, "decline is terminal and visible") assert_true(not state.can_cancel(), "terminal proposal cannot issue queue cancel") var expired := MatchmakingState.new() expired.begin_queue("ticket-2", "casual") assert_true(expired.apply_proposal_update({"proposal_id": "proposal-2", "revision": 1, "state": "OPEN"}), "second proposal opens") assert_true(expired.apply_proposal_update({"proposal_id": "proposal-2", "revision": 2, "state": "EXPIRED"}), "expired proposal applies") assert_eq(expired.phase, MatchmakingState.EXPIRED, "expiry is visible") assert_true(not expired.can_cancel(), "expired proposal cannot be cancelled") func test_assignment_lifecycle_has_explicit_connecting_and_live_states() -> void: var state := MatchmakingState.new() state.begin_queue("ticket-1", "ranked") state.mark_assignment_ready() assert_eq(state.phase, MatchmakingState.ASSIGNMENT_READY, "assignment readiness is visible") state.mark_connecting() assert_eq(state.phase, MatchmakingState.CONNECTING, "transport connection is visible") state.mark_live() assert_eq(state.phase, MatchmakingState.LIVE, "live match is visible")