mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
105 lines
6.6 KiB
GDScript
105 lines
6.6 KiB
GDScript
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_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_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")
|
|
|
|
|
|
func test_ticket_projection_rejects_gap_and_wrong_ticket_without_mutation() -> void:
|
|
var state := MatchmakingState.new()
|
|
assert_true(state.begin_queue("ticket-1", "casual"), "queue setup succeeds")
|
|
assert_eq(state.ticket_id, "ticket-1", "queue setup retains ticket identity")
|
|
var resync_ids: Array[String] = [""]
|
|
state.resync_required.connect(func(id: String) -> void: resync_ids[0] = 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_ids[0], "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")
|
|
|
|
|
|
func test_authoritative_enqueue_time_survives_wait_projection_and_restore() -> void:
|
|
var state := MatchmakingState.new()
|
|
assert_true(state.begin_queue("ticket-wait", "casual"), "queue setup succeeds")
|
|
assert_true(state.apply_ticket_update({"ticket_id": "ticket-wait", "revision": 0, "state": "QUEUED", "playlist": "casual", "enqueued_at_unix": 1000}), "server enqueue timestamp applies")
|
|
assert_eq(state.waited_seconds(1065), 65, "wait uses server enqueue time")
|
|
var restored := MatchmakingState.new()
|
|
assert_true(restored.restore_snapshot(state.snapshot()), "snapshot restores")
|
|
assert_eq(restored.waited_seconds(1065), 65, "authoritative wait survives restore")
|