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") func test_expiry_is_distinct_from_generic_failure_and_remains_visible() -> void: var state := MatchmakingState.new() state.begin_queue("ticket-1", "casual") state.expire("Queue ticket expired") assert_eq(state.phase, MatchmakingState.EXPIRED, "expired ticket has a terminal expiry state") assert_eq(state.message, "Queue ticket expired", "expiry reason is visible") assert_true(not state.can_cancel(), "expired ticket cannot be cancelled") func test_restart_restore_requires_valid_identity_and_requests_authoritative_recovery() -> void: var state := MatchmakingState.new() assert_true(state.restore_snapshot({"phase": "QUEUED", "ticket_id": "ticket-1", "playlist": "casual", "revision": 2}), "valid active snapshot restores") assert_true(state.needs_resync, "restored active state must recover from the server") assert_eq(state.revision, 2, "revision is retained for diagnostics") assert_true(not state.restore_snapshot({"phase": "QUEUED", "ticket_id": "", "playlist": "casual"}), "missing ticket identity is rejected") assert_eq(state.phase, MatchmakingState.IDLE, "invalid restore cannot leave stale active state") assert_true(not state.restore_snapshot({"phase": "NOT_A_STATE", "ticket_id": "ticket-1", "playlist": "casual"}), "unknown state is rejected")