package creator import ( "math" "testing" "time" ) func TestMetricScheduleUsesUTCAndCumulativeIntervals(t *testing.T) { published := time.Date(2026, 1, 1, 0, 0, 0, 0, time.FixedZone("CST", 8*60*60)) points := MetricSchedule(published, time.Hour, 24*time.Hour, 2, 6) want := []time.Duration{time.Hour, 3 * time.Hour, 7 * time.Hour, 15 * time.Hour, 31 * time.Hour, 55 * time.Hour} for index, duration := range want { if got := points[index].Sub(published.UTC()); got != duration { t.Fatalf("point %d: got %s, want %s", index, got, duration) } } if next, reason := NextMetricAt(published, published.Add(3*time.Hour), time.Hour, 24*time.Hour, 2, 30*24*time.Hour); next != published.UTC().Add(7*time.Hour) || reason != "" { t.Fatalf("strict next point: got %s/%q", next, reason) } if next, reason := NextMetricAt(published, published.Add(30*24*time.Hour), time.Hour, 24*time.Hour, 2, 30*24*time.Hour); !next.IsZero() || reason != "monitoring_age_reached" { t.Fatalf("age boundary: got %s/%q", next, reason) } } func TestKeywordMatchingDoesNotNormalizeText(t *testing.T) { if got := TrimKeyword(" \tAI \r\n"); got != "AI" { t.Fatalf("trimmed keyword = %q", got) } if ok, _ := MatchKeywords("AI 买 车", []string{"AI"}, nil); !ok { t.Fatal("expected exact substring match") } if ok, _ := MatchKeywords("ai 买车", []string{"AI"}, nil); ok { t.Fatal("case should remain significant") } if ok, _ := MatchKeywords("AI 买车", []string{"买 车"}, nil); ok { t.Fatal("internal whitespace should remain significant") } if ok, _ := MatchKeywords("AI 买 车", []string{"AI"}, []string{"买 车"}); ok { t.Fatal("exclude keyword should veto the match") } } func TestValidateSettingsRejectsUnsafeNumbers(t *testing.T) { base := SettingsUpdate{LookbackDays: 30, NewWorkIntervalSeconds: 1800, MetricInitialIntervalSeconds: 3600, MetricMultiplier: 2, MetricMaxIntervalSeconds: 86400, MetricAgeSeconds: 30 * 24 * 60 * 60} for _, mutate := range []func(*SettingsUpdate){ func(value *SettingsUpdate) { value.MetricMultiplier = math.NaN() }, func(value *SettingsUpdate) { value.MetricAgeSeconds = maxDurationSeconds + 1 }, func(value *SettingsUpdate) { value.LookbackDays = int(maxDurationSeconds/(24*60*60) + 1) }, } { value := base mutate(&value) if err := ValidateSettings(value); err != ErrInvalid { t.Fatalf("expected invalid settings, got %v", err) } } } func TestNextMetricIntervalRejectsNonFiniteMultiplier(t *testing.T) { if _, err := nextMetricInterval(3600, math.NaN(), 86400); err != ErrInvalid { t.Fatalf("expected NaN multiplier to be invalid, got %v", err) } if next, err := nextMetricInterval(3600, math.Inf(1), 86400); err != nil || next != 86400 { t.Fatalf("expected infinite growth to clamp at maximum, got %d/%v", next, err) } } func TestActionTargetAndBusinessStatusGates(t *testing.T) { if ActionTargetValid(ActionReplyComment, "uid-1", "", "", "comment") { t.Fatal("reply without a comment target must be blocked") } profile := AccountProfile{AuthorizationStatus: "authorized", LoginStatus: "logged_in", BusinessStatus: "muted"} if err := CanWrite(profile, false, ActionDM); err == nil { t.Fatal("muted accounts cannot manually send DMs") } profile.BusinessStatus = "normal" if err := CanWrite(profile, false, ActionDM); err != nil { t.Fatalf("normal logged-in account should be writable: %v", err) } } func TestValidMessageTypeKeepsNonTextMessagesExplicit(t *testing.T) { for _, messageType := range []string{MessageTypeText, MessageTypeImage, MessageTypeVoice, MessageTypeVideo, MessageTypeSticker, MessageTypeUnknown} { if !ValidMessageType(messageType) { t.Fatalf("message type %q should be accepted", messageType) } } if ValidMessageType("text-like") { t.Fatal("unknown message type must not be accepted") } }