package activitymq import ( "strings" "testing" "time" ) func TestActivityTemplateFactRoundTrip(t *testing.T) { occurredAt := time.Date(2026, time.July, 14, 8, 30, 0, 0, time.UTC).UnixMilli() want := ActivityTemplateFactMessage{ AppCode: "lalu", EventID: "activity:g1", EventType: EventTypeGiftScored, TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3, StatDay: "2026-07-14", RegionID: 9, UserID: 1001, GiftID: "rose", GiftCountDelta: 2, CoinAmountDelta: 200, OccurredAtMS: occurredAt, } body, err := EncodeActivityTemplateFactMessage(want) if err != nil { t.Fatalf("encode: %v", err) } got, err := DecodeActivityTemplateFactMessage(body) if err != nil { t.Fatalf("decode: %v", err) } if got.MessageType != MessageTypeActivityTemplateFact || got.EventID != want.EventID || got.CoinAmountDelta != 200 { t.Fatalf("unexpected round trip: %+v", got) } } func TestActivityTemplateFactRejectsMismatchedDayAndIncompleteDimensions(t *testing.T) { base := ActivityTemplateFactMessage{ AppCode: "lalu", EventID: "activity:t1", EventType: EventTypeTaskProgressed, TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 1, StatDay: "2026-07-13", UserID: 1001, TaskKey: "send_gift", TaskType: "gift", ProgressDelta: 1, OccurredAtMS: time.Date(2026, time.July, 14, 0, 0, 0, 0, time.UTC).UnixMilli(), } if _, err := EncodeActivityTemplateFactMessage(base); err == nil || !strings.Contains(err.Error(), "does not match") { t.Fatalf("expected stat day mismatch, got %v", err) } base.StatDay = "2026-07-14" base.TaskKey = "" if _, err := EncodeActivityTemplateFactMessage(base); err == nil { t.Fatal("expected incomplete task identity rejection") } } func TestTaskClaimedUsesImmutableEarningAttributionDay(t *testing.T) { earningAt := time.Date(2026, time.July, 14, 23, 50, 0, 0, time.UTC).UnixMilli() claimedAt := time.Date(2026, time.July, 17, 9, 15, 0, 0, time.UTC).UnixMilli() want := ActivityTemplateFactMessage{ AppCode: "lalu", EventID: "activity:claim:delayed", EventType: EventTypeTaskClaimed, TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3, StatDay: "2026-07-14", RegionID: 9, UserID: 1001, TaskKey: "send_gift", TaskType: "gift", ClaimedDelta: 1, OccurredAtMS: claimedAt, AttributionAtMS: earningAt, } body, err := EncodeActivityTemplateFactMessage(want) if err != nil { t.Fatalf("encode delayed task claim: %v", err) } got, err := DecodeActivityTemplateFactMessage(body) if err != nil { t.Fatalf("decode delayed task claim: %v", err) } if got.StatDay != "2026-07-14" || got.OccurredAtMS != claimedAt || got.AttributionAtMS != earningAt { t.Fatalf("task claim attribution changed: %+v", got) } // A claim cannot relabel its earning day independently from the persisted // attribution timestamp, otherwise retry time or a moved user region could // rewrite already-earned statistics. want.AttributionAtMS = time.Date(2026, time.July, 15, 0, 0, 0, 0, time.UTC).UnixMilli() if _, err := EncodeActivityTemplateFactMessage(want); err == nil || !strings.Contains(err.Error(), "attribution") { t.Fatalf("expected attribution mismatch rejection, got %v", err) } } func TestActivityVisitedRequiresPositiveVisitDelta(t *testing.T) { visitedAt := time.Date(2026, time.July, 14, 8, 30, 0, 0, time.UTC).UnixMilli() fact := ActivityTemplateFactMessage{ AppCode: "lalu", EventID: "activity:visit:1", EventType: EventTypeActivityVisited, TemplateID: "template-1", TemplateCode: "summer_rank", PublishedVersion: 3, StatDay: "2026-07-14", RegionID: 9, UserID: 1001, OccurredAtMS: visitedAt, } if _, err := EncodeActivityTemplateFactMessage(fact); err == nil { t.Fatal("expected zero visit delta rejection") } fact.VisitCountDelta = 1 if _, err := EncodeActivityTemplateFactMessage(fact); err != nil { t.Fatalf("encode activity visit: %v", err) } } func TestActivityTemplateFactTagExpressionIsSafeAndDeterministic(t *testing.T) { expression, err := LegacyCompatibleTagExpression(EventTypeTaskClaimed, EventTypeGiftScored, EventTypeTaskClaimed) if err != nil { t.Fatalf("expression: %v", err) } if expression != "activity_template_fact || GiftScored || TaskClaimed" { t.Fatalf("unexpected expression: %s", expression) } if _, err := EventTypeTag("GiftScored || *"); err == nil { t.Fatal("expected unsafe tag rejection") } }