package store import ( "encoding/json" "testing" "time" ) func TestMarshalStateChangedEnvelopeProducesDeliverableShape(t *testing.T) { now := time.Date(2026, 9, 5, 12, 0, 0, 0, time.UTC) payload, err := MarshalStateChangedEnvelope("match-1", 7, "LIVE", now, []string{"player-a", "player-b"}) if err != nil { t.Fatalf("marshal: %v", err) } // Decode with exactly the struct api.deliverStateOutboxEvent uses, then // apply exactly its acceptance predicate. This is the regression guard for // the initial-connect payload that omitted every one of these keys. var envelope struct { Event string `json:"event"` Revision uint64 `json:"revision"` ResourceID string `json:"resource_id"` OccurredAt time.Time `json:"occurred_at"` State string `json:"state"` MatchID string `json:"match_id"` PlayerIDs []string `json:"player_ids"` } if err := json.Unmarshal(payload, &envelope); err != nil { t.Fatalf("decode: %v", err) } if envelope.Event != "state_changed" || envelope.ResourceID != "match-1" || envelope.Revision != 7 || envelope.State == "" || len(envelope.PlayerIDs) == 0 { t.Fatalf("envelope would be rejected by the dispatcher: %+v", envelope) } if envelope.MatchID != "match-1" || !envelope.OccurredAt.Equal(now) { t.Fatalf("unexpected envelope: %+v", envelope) } } func TestMarshalOutboxEnvelopeRejectsUndeliverablePayloads(t *testing.T) { now := time.Date(2026, 9, 5, 12, 0, 0, 0, time.UTC) valid := OutboxEnvelope{ Event: "state_changed", ResourceID: "match-1", Revision: 1, OccurredAt: now, State: "LIVE", MatchID: "match-1", PlayerIDs: []string{"player-a"}, } if _, err := MarshalOutboxEnvelope(valid); err != nil { t.Fatalf("baseline envelope must be valid: %v", err) } for name, mutate := range map[string]func(*OutboxEnvelope){ "no event": func(e *OutboxEnvelope) { e.Event = "" }, "no resource": func(e *OutboxEnvelope) { e.ResourceID = "" }, "no state": func(e *OutboxEnvelope) { e.State = "" }, "no timestamp": func(e *OutboxEnvelope) { e.OccurredAt = time.Time{} }, "no recipients": func(e *OutboxEnvelope) { e.PlayerIDs = nil }, "empty recipient": func(e *OutboxEnvelope) { e.PlayerIDs = []string{"player-a", ""} }, "duplicate recipient": func(e *OutboxEnvelope) { e.PlayerIDs = []string{"player-a", "player-a"} }, // -1 is the "nothing matched" sentinel several CTEs return. Untyped as // uint64 it would become 18446744073709551615 in the payload. "sentinel revision": func(e *OutboxEnvelope) { e.Revision = -1 }, "reserved extra": func(e *OutboxEnvelope) { e.Extra = map[string]any{"state": "CANCELLED"} }, } { t.Run(name, func(t *testing.T) { envelope := valid mutate(&envelope) if _, err := MarshalOutboxEnvelope(envelope); err == nil { t.Fatalf("expected %s to be rejected at construction", name) } }) } } func TestMarshalOutboxEnvelopeOmitsMatchIDForProposals(t *testing.T) { now := time.Date(2026, 9, 5, 12, 0, 0, 0, time.UTC) payload, err := MarshalOutboxEnvelope(OutboxEnvelope{ Event: "proposal_changed", ResourceID: "proposal-1", Revision: 0, OccurredAt: now, State: "OPEN", PlayerIDs: []string{"player-a"}, }) if err != nil { t.Fatalf("marshal: %v", err) } var decoded map[string]any if err := json.Unmarshal(payload, &decoded); err != nil { t.Fatalf("decode: %v", err) } if _, present := decoded["match_id"]; present { t.Fatalf("proposal envelope must not carry a match_id: %s", payload) } }