fix(server): deliver attendance rewards through idempotent mail and notify new messages

This commit is contained in:
2026-10-05 14:18:34 +08:00
parent 9ece9d9afa
commit 8d657bf694
12 changed files with 705 additions and 140 deletions
@@ -23,26 +23,28 @@ func claimEntries(b []byte) int {
}
func TestAutomaticAttendanceLoginMarkerReplayAndRestart(t *testing.T) {
s, eco, store := setup(t)
m := s.attendanceMail.(*attendanceMailStub)
if _, b, _, err := s.Handle("/LoginEvent", req(1)); err != nil || claimEntries(b) != 0 || eco.calls != 0 {
t.Fatal("login event silently granted", err)
}
_, response, _, err := s.Handle("/Attendance", req(2))
if err != nil || eco.calls != 1 || claimEntries(response) != 1 || s.state.Attendance["1"].Count != 1 {
if err != nil || m.calls != 1 || eco.calls != 0 || claimEntries(response) != 1 || s.state.Attendance["1"].Count != 1 {
t.Fatal("shared marker prevented actual grant", err)
}
reopened, err := Open(store, s.design, s.registry, eco)
if err != nil {
t.Fatal(err)
}
reopened.AttachAttendanceMail(m)
reopened.now = s.now
reopened.SetSession("test")
_, replay, _, err := reopened.Handle("/Attendance", req(2))
if err != nil || !bytes.Equal(response, replay) || eco.calls != 1 {
if err != nil || !bytes.Equal(response, replay) || m.calls != 1 || eco.calls != 0 {
t.Fatal("durable replay changed", err)
}
reopened.SetSession("reconnect")
_, response, _, err = reopened.Handle("/Attendance", req(1))
if err != nil || claimEntries(response) != 0 || eco.calls != 1 {
if err != nil || claimEntries(response) != 0 || m.calls != 1 || eco.calls != 0 {
t.Fatal("history returned as new reward", err)
}
if _, present, _ := wire.Bytes(response, 1001); present {
@@ -51,6 +53,7 @@ func TestAutomaticAttendanceLoginMarkerReplayAndRestart(t *testing.T) {
}
func TestAutomaticLimitAttendanceUsesScheduleDateAndSkipsInactive(t *testing.T) {
s, eco, _ := setup(t)
m := s.attendanceMail.(*attendanceMailStub)
now := s.now()
reg := events.NewRegistry()
rows := []events.Schedule{
@@ -65,8 +68,8 @@ func TestAutomaticLimitAttendanceUsesScheduleDateAndSkipsInactive(t *testing.T)
s.design.LimitRewards = map[[2]uint64]uint64{{7, 1}: 100, {7, 2}: 200, {7, 3}: 300, {7, 4}: 400, {8, 1}: 500, {9, 3}: 600}
s.state.LoginDays[now.Add(-24*time.Hour).Format("2006-01-02")] = now.Add(-24 * time.Hour).UnixMilli()
_, response, _, err := s.Handle("/Attendance", req(1))
if err != nil || claimEntries(response) != 1 || eco.calls != 1 || len(eco.rewards) != 1 || eco.rewards[0].ID != 300 {
t.Fatal("wrong calendar day granted", eco.rewards, err)
if err != nil || claimEntries(response) != 1 || m.calls != 1 || eco.calls != 0 || len(m.rewards) != 1 || m.rewards[0].ID != 300 {
t.Fatal("wrong calendar day granted", m.rewards, err)
}
if len(s.state.Attendance) != 1 {
t.Fatal("inactive calendar mutated")
@@ -74,12 +77,13 @@ func TestAutomaticLimitAttendanceUsesScheduleDateAndSkipsInactive(t *testing.T)
now = now.Add(24 * time.Hour)
s.now = func() time.Time { return now }
_, response, _, err = s.Handle("/Attendance", req(2))
if err != nil || claimEntries(response) != 1 || eco.rewards[len(eco.rewards)-1].ID != 400 {
if err != nil || claimEntries(response) != 1 || m.rewards[len(m.rewards)-1].ID != 400 {
t.Fatal("next calendar day missing", err)
}
}
func TestAutomaticAttendanceUIDZeroSeparateDesignsAndSingleSettlement(t *testing.T) {
s, eco, _ := setup(t)
m := s.attendanceMail.(*attendanceMailStub)
now := s.now()
reg := events.NewRegistry()
if err := reg.Replace([]events.Schedule{{Type: 0, ID: 1, Start: now.Add(-time.Hour).UnixMilli(), End: now.Add(time.Hour).UnixMilli()}, {Type: 0, ID: 2, Start: now.Add(-time.Hour).UnixMilli(), End: now.Add(time.Hour).UnixMilli()}}); err != nil {
@@ -89,27 +93,28 @@ func TestAutomaticAttendanceUIDZeroSeparateDesignsAndSingleSettlement(t *testing
s.design.Attendance[2] = gamedata.EventAttendance{ID: 2, Group: 2}
s.design.AttendanceRewards[2] = []gamedata.EventAttendanceReward{{ID: 1, Group: 2, Day: 1, Basic: gamedata.Reward{Type: 4, Count: 20}}, {ID: 2, Group: 2, Day: 2, Basic: gamedata.Reward{Type: 4, Count: 30}}}
_, response, _, err := s.Handle("/Attendance", req(1))
if err != nil || eco.calls != 1 || len(eco.rewards) != 2 || claimEntries(response) != 2 || len(s.state.Attendance) != 2 {
if err != nil || m.calls != 1 || eco.calls != 0 || len(m.rewards) != 2 || claimEntries(response) != 2 || len(s.state.Attendance) != 2 {
t.Fatal("public zero identities collided", err)
}
}
func TestAutomaticAttendanceEconomyFailureRestoresProgressAndClaims(t *testing.T) {
func TestAutomaticAttendanceMailFailureRestoresProgressAndClaims(t *testing.T) {
s, eco, _ := setup(t)
eco.err = errors.New("settlement failure")
m := s.attendanceMail.(*attendanceMailStub)
m.err = errors.New("settlement failure")
if _, _, _, err := s.Handle("/Attendance", req(1)); err == nil {
t.Fatal("failed settlement accepted")
}
if len(s.state.Attendance) != 0 || len(s.state.LoginDays) != 0 || len(s.state.Receipts) != 0 {
t.Fatal("failed settlement persisted markers")
}
eco.err = nil
m.err = nil
_, response, _, err := s.Handle("/Attendance", req(1))
if err != nil || claimEntries(response) != 1 || !s.state.Attendance["1"].Obtained["1/1"] {
if err != nil || eco.calls != 0 || m.calls != 1 || claimEntries(response) != 1 || !s.state.Attendance["1"].Obtained["1/1"] {
t.Fatal("retry lost eligibility", err)
}
}
// The account transaction restores economic state; this store exercises the
// The account transaction restores mailbox state; this store exercises the
// domain restoration that must also drop newly inserted map entries.
type attendanceFailStore struct{ stateio.Store }
@@ -128,3 +133,43 @@ func TestAutomaticAttendanceSaveFailureRestoresState(t *testing.T) {
t.Fatal("failed save persisted attendance", err)
}
}
func TestAttendancePreviouslyGrantedRewardsAreNotMailedAgain(t *testing.T) {
s, eco, _ := setup(t)
m := s.attendanceMail.(*attendanceMailStub)
a := s.attendance(s.registry.List()[0])
a.Group, a.Count, a.LastDay = 1, 1, s.day()
a.Obtained["1/1"], a.History["1/1"] = true, true
_, response, _, err := s.Handle("/Attendance", req(1))
if err != nil || claimEntries(response) != 0 || m.calls != 0 || eco.calls != 0 {
t.Fatal("existing direct reward mailed again", err)
}
}
func TestExplicitAttendanceClaimUsesMailboxAndSharedLedger(t *testing.T) {
s, eco, _ := setup(t)
m := s.attendanceMail.(*attendanceMailStub)
if _, _, _, err := s.Handle("/LoginEvent", req(1)); err != nil {
t.Fatal(err)
}
claim := wire.AppendVarint(req(2), 2, 1)
claim = wire.AppendVarint(claim, 3, 1)
claim = wire.AppendVarint(claim, 4, 1)
_, response, _, err := s.Handle("/EventReward", claim)
if err != nil || len(response) != 0 || m.calls != 1 || eco.calls != 0 || m.identity != "test:/EventReward:2" {
t.Fatal("explicit claim bypassed mailbox", err)
}
_, response, _, err = s.Handle("/Attendance", req(3))
if err != nil || claimEntries(response) != 0 || m.calls != 1 || eco.calls != 0 {
t.Fatal("auto duplicated explicit mail", err)
}
}
func TestAttendanceMissingMailboxDoesNotGrantOrPersist(t *testing.T) {
s, eco, _ := setup(t)
s.AttachAttendanceMail(nil)
if _, _, _, err := s.Handle("/Attendance", req(1)); err == nil {
t.Fatal("missing mailbox accepted")
}
if eco.calls != 0 || len(s.state.Attendance) != 0 || len(s.state.LoginDays) != 0 || len(s.state.Receipts) != 0 {
t.Fatal("missing mailbox granted or retained state")
}
}