fix(multiplayer): fence client queue cancellation states

This commit is contained in:
Josh Creek
2026-09-03 00:15:41 +01:00
parent 1976c6eac6
commit a15368ed29
6 changed files with 75 additions and 6 deletions
+3
View File
@@ -131,6 +131,9 @@ func (q *Queue) Cancel(playerID, ticketID, idempotencyKey string, expectedRevisi
if ticket.Revision != expectedRevision {
return QueueTicket{}, ErrStaleRevision
}
if ticket.State != Queued && ticket.State != Proposed {
return QueueTicket{}, fmt.Errorf("%w: cancel in %s", ErrConflict, ticket.State)
}
if idempotencyKey == "" {
return QueueTicket{}, fmt.Errorf("%w: empty cancel key", ErrConflict)
}
+18
View File
@@ -50,6 +50,24 @@ func TestQueueHeartbeatExtendsExpiryExactlyAndRejectsStaleReplay(t *testing.T) {
}
}
func TestQueueCancelCannotOverrideMatchOwnedLifecycle(t *testing.T) {
q := NewQueue()
now := time.Unix(1000, 0)
candidate := Candidate{TicketID: "ticket-a", PlayerID: "player-a", EnqueuedAt: now}
if _, err := q.Create("player-a", "ticket-a", "create-key-123456", candidate, now); err != nil {
t.Fatal(err)
}
ticket := q.tickets["ticket-a"]
ticket.State = Live
q.tickets["ticket-a"] = ticket
if _, err := q.Cancel("player-a", "ticket-a", "cancel-key-123456", 0, now.Add(time.Second)); !errors.Is(err, ErrConflict) {
t.Fatalf("live ticket cancellation error = %v, want conflict", err)
}
if got := q.tickets["ticket-a"].State; got != Live {
t.Fatalf("live ticket state = %s after cancellation attempt", got)
}
}
func TestQueueExpiryReleasesOwnershipAndDoesNotReturnExpiredCandidates(t *testing.T) {
q := NewQueue()
now := time.Unix(1000, 0)