package domain import ( "errors" "testing" "time" ) func TestProposalRequiresUnanimousAcceptance(t *testing.T) { now := time.Unix(1000, 0) p, err := NewProposal("proposal-123456789", Casual, []string{"b", "a"}, now) if err != nil { t.Fatal(err) } if _, err = p.Respond("a", "response-a-123456", true, 0, now); err != nil { t.Fatal(err) } if p.State != Open || p.Revision != 1 { t.Fatalf("partial acceptance closed proposal: %+v", p) } if _, err = p.Respond("b", "response-b-123456", true, 1, now); err != nil { t.Fatal(err) } if p.State != Accepted || p.Revision != 2 { t.Fatalf("unanimous acceptance not committed: %+v", p) } } func TestProposalResponseReplayIsStableAndPayloadReuseConflicts(t *testing.T) { now := time.Unix(1000, 0) p, err := NewProposal("proposal-123456789", Casual, []string{"a", "b"}, now) if err != nil { t.Fatal(err) } first, err := p.Respond("a", "response-a-123456", true, 0, now) if err != nil { t.Fatal(err) } replay, err := p.Respond("a", "response-a-123456", true, 0, now.Add(20*time.Second)) if err != nil || replay.Revision != first.Revision { t.Fatalf("replay = %+v, %v", replay, err) } if _, err = p.Respond("a", "response-a-123456", false, 1, now); !errors.Is(err, ErrConflict) { t.Fatalf("payload reuse error = %v", err) } } func TestProposalExpiryTimesOutPendingParticipantsAndClosesRace(t *testing.T) { now := time.Unix(1000, 0) p, err := NewProposal("proposal-123456789", Ranked, []string{"a", "b", "c", "d", "e", "f"}, now) if err != nil { t.Fatal(err) } if !p.Expire(now.Add(ProposalWindow)) || p.State != Expired || p.Revision != 1 { t.Fatalf("expiry failed: %+v", p) } for _, participant := range p.Participants { if participant.Response != TimedOutResponse { t.Fatalf("pending participant not timed out: %+v", participant) } } if _, err = p.Respond("a", "late-response-123", true, 1, now.Add(ProposalWindow)); !errors.Is(err, ErrProposalClosed) { t.Fatalf("late response error = %v", err) } } func TestRankedProposalAndCooldownEscalation(t *testing.T) { now := time.Unix(1000, 0) if _, err := NewProposal("proposal-123456789", Ranked, []string{"a", "b"}, now); err == nil { t.Fatal("ranked proposal accepted fewer than six") } if got := CooldownUntil([]CooldownEvent{{At: now, Playlist: Ranked, Kind: DeclinedResponse}}, Ranked, now); !got.Equal(now.Add(2 * time.Minute)) { t.Fatalf("ranked decline cooldown = %v", got) } events := []CooldownEvent{{At: now, Playlist: Ranked, Kind: TimedOutResponse}, {At: now.Add(time.Minute), Playlist: Ranked, Kind: DeclinedResponse}, {At: now.Add(2 * time.Minute), Playlist: Ranked, Kind: TimedOutResponse}} if got := CooldownUntil(events, Ranked, now.Add(2*time.Minute)); !got.Equal(now.Add(17 * time.Minute)) { t.Fatalf("ranked escalation cooldown = %v", got) } }