diff --git a/server/api/errors_test.go b/server/api/errors_test.go new file mode 100644 index 00000000..8e5e6e29 --- /dev/null +++ b/server/api/errors_test.go @@ -0,0 +1,21 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/cosmic-clash/cosmic-clash/server/domain" +) + +func TestCooldownDomainErrorIsRetryableButNotAConflict(t *testing.T) { + recorder := httptest.NewRecorder() + writeDomainError(recorder, domain.ErrPlayerCooldown) + if recorder.Code != http.StatusTooManyRequests { + t.Fatalf("cooldown status = %d, want 429", recorder.Code) + } + if !strings.Contains(recorder.Body.String(), "matchmaking_cooldown") { + t.Fatalf("cooldown response = %q", recorder.Body.String()) + } +} diff --git a/server/store/proposal_recovery_sql.go b/server/store/proposal_recovery_sql.go index 1b3ee52c..8f242cfc 100644 --- a/server/store/proposal_recovery_sql.go +++ b/server/store/proposal_recovery_sql.go @@ -116,15 +116,10 @@ SET state = 'DECLINED', revision = revision + 1 WHERE proposal_id = $1 AND state = 'OPEN'` // ProposalDeclineRequeueSQL requeues every participant's ticket, including -// the decliner's own: nothing yet enforces the decline cooldown §8.17 -// documents as a separate, not-yet-built feature, so leaving any ticket -// behind at PROPOSED here isn't "cooldown behaviour", it's just a stranded -// ticket -- invisible to the matcher (which only ever reads state='QUEUED'), -// still counted as this player's one active ticket (blocking a fresh -// queue_create), and renewable forever by an ordinary heartbeat, so a player -// left in this state has no path back into matchmaking without realising -// they need to cancel and start over. Once §8.17's cooldown exists, it can -// exempt the decliner from this immediate requeue; today nothing does. +// the decliner's own. The durable decline penalty separately prevents that +// player from immediately creating a replacement ticket; leaving this ticket +// at PROPOSED would not implement a cooldown, it would strand the player and +// hide the ticket from the matcher. const ProposalDeclineRequeueSQL = `UPDATE queue_tickets q SET state = 'QUEUED', expires_at = $2, revision = revision + 1 FROM proposal_participants pp