package onlineevent import ( "context" "errors" "fmt" "os" "sync" "testing" "time" dnfrepo "longheng.io/server/internal/modules/dnf/repository" "longheng.io/server/internal/modules/dnf/repository/memory" platformpvf "longheng.io/server/internal/platform/pvf" ) func TestRealPCRoomTimePointPVFCatalog(t *testing.T) { p := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE") if p == "" { t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE") } a, err := platformpvf.LoadArchive(platformpvf.Options{Path: p}) if err != nil { t.Fatal(err) } c, err := LoadPCRoomTimePointCatalog(context.Background(), a) if err != nil { t.Fatal(err) } if c.Seconds != [4]uint32{1800, 3600, 7200, 10800} || c.Counts != [4]uint32{5, 10, 15, 25} { t.Fatalf("catalog=%+v", c) } for i, id := range []int64{490008561, 490008562, 490008564, 490008565} { if c.Daily[i].ItemID != id || c.Daily[i].DefinitionPath == "" { t.Fatalf("daily[%d]=%+v", i, c.Daily[i]) } } for i, id := range []int64{490008566, 490008567, 490008568, 490008570} { if c.Period[i].ItemID != id || c.Period[i].DefinitionPath == "" { t.Fatalf("period[%d]=%+v", i, c.Period[i]) } } } func pcroomTestCatalog() (*PCRoomTimePointCatalog, Definition) { c := &PCRoomTimePointCatalog{Seconds: [4]uint32{1800, 3600, 7200, 10800}, Counts: [4]uint32{5, 10, 15, 25}} d := Definition{ID: "2347"} for i := range c.Daily { c.Daily[i] = AttendancePVFItemReward{ItemID: int64(100 + i), Count: 1} c.Period[i] = AttendancePVFItemReward{ItemID: int64(200 + i), Count: 1} d.Stages = append(d.Stages, Stage{ID: fmt.Sprintf("stage-%d", i+1), RequiredSeconds: uint64(c.Seconds[i]), Items: []ItemReward{{ItemID: int64(300 + i), Count: 1}}}) } return c, d } func TestPCRoomTimePointClockClaimsCrossDayAndRollback(t *testing.T) { ctx := context.Background() repos := memory.NewMemoryGroup() c, d := pcroomTestCatalog() if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: "a"}); err != nil { t.Fatal(err) } for _, id := range []string{"1", "2"} { if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: id, AccountID: "a"}); err != nil { t.Fatal(err) } } owner, err := NewOwner(repos) if err != nil { t.Fatal(err) } start := time.Date(2026, 9, 16, 6, 0, 0, 0, ChinaCalendar) cmd := PCRoomTimePointCommand{AccountID: "a", CharacterID: "1", Now: start, ClockDefinition: d, PeriodClaim: -1} fail := false cmd.Deliver = func(m dnfrepo.MailboxRepository, r AttendancePVFItemReward, key string) (string, error) { id, err := dnfrepo.AppendSystemMail(ctx, m, dnfrepo.SystemMailDelivery{RecipientCharacterID: cmd.CharacterID, Source: "pcroom-test", CompletionKey: key, CreatedAt: cmd.Now, Attachments: []dnfrepo.MailAttachment{{ItemID: r.ItemID, Count: r.Count}}}) if fail { return "", errors.New("failure after mail write") } return id, err } observe := func(from, to time.Time) { t.Helper() if _, err := owner.Observe(ctx, ObserveCommand{AccountID: "a", CharacterID: "1", Definition: d, IntervalFrom: from, IntervalTo: to}); err != nil { t.Fatal(err) } cmd.Now = to } settle := func() PCRoomTimePointSnapshot { t.Helper() s, err := SettlePCRoomTimePoint(ctx, repos, c, cmd) if err != nil { t.Fatal(err) } return s } cmd.DailyClaim = 1 observe(start, start.Add(1799*time.Second)) if s := settle(); s.DailyMask != 0 || len(s.MailIDs) != 0 { t.Fatalf("early claim=%+v", s) } observe(start, start.Add(1800*time.Second)) fail = true if _, err := SettlePCRoomTimePoint(ctx, repos, c, cmd); err == nil { t.Fatal("mail failure committed") } mb, _, _ := repos.Mailbox.Load(ctx, "1") if len(mb.Mails) != 0 { t.Fatal("failed mail leaked") } fail = false if s := settle(); s.DailyMask != 1 || len(s.MailIDs) != 1 { t.Fatalf("claim=%+v", s) } cmd.CharacterID = "2" if s := settle(); s.DailyMask != 1 || len(s.MailIDs) != 0 { t.Fatalf("cross-character replay=%+v", s) } cmd.DailyClaim = 0 observe(start, start.Add(3*time.Hour)) if s := settle(); s.Total != 1 || s.DailyMask != 1 { t.Fatalf("full day independent of claim=%+v", s) } if s := settle(); s.Total != 1 { t.Fatalf("double count=%+v", s) } for day := 1; day < 25; day++ { at := start.AddDate(0, 0, day) cmd.Now = at if s := settle(); s.Seconds != 0 || s.DailyMask != 0 || s.Total != uint32(day) { t.Fatalf("reset=%+v day=%d", s, day) } observe(at, at.Add(3*time.Hour)) s := settle() if s.Total != uint32(day+1) { t.Fatalf("count=%+v day=%d", s, day) } for i, n := range c.Counts { cmd.PeriodClaim = i s = settle() if s.Total < n && s.PeriodMask&(1<