package luckygift import "testing" // 分桶必须纯确定:room saga 在 Check 固化 strategy_version 且恢复不重新 Check, // 同一用户在 Check 与 Draw 的两次解析必须得到同一组。 func TestExperimentBucketPPMIsDeterministic(t *testing.T) { for _, userKey := range []string{"1", "42", "9007199254740993", "ext:abc-123"} { first := ExperimentBucketPPM("salt-a", userKey) for i := 0; i < 10; i++ { if again := ExperimentBucketPPM("salt-a", userKey); again != first { t.Fatalf("bucket for %q drifted: %d != %d", userKey, again, first) } } if first < 0 || first >= ExperimentTrafficPPMScale { t.Fatalf("bucket for %q out of range: %d", userKey, first) } } if ExperimentBucketPPM("salt-a", "1") == ExperimentBucketPPM("salt-b", "1") && ExperimentBucketPPM("salt-a", "2") == ExperimentBucketPPM("salt-b", "2") && ExperimentBucketPPM("salt-a", "3") == ExperimentBucketPPM("salt-b", "3") { t.Fatal("different salts should reshuffle buckets") } } // 放量必须单调:调大流量时既有 treatment 用户全部保留,不允许来回倒组打断用户体验状态。 func TestResolveExperimentArmMonotonicRamp(t *testing.T) { low := Experiment{Status: ExperimentStatusRunning, BucketSalt: "s", TreatmentTrafficPPM: 50_000} high := low high.TreatmentTrafficPPM = 300_000 treatmentAtLow := 0 for userID := int64(1); userID <= 2_000; userID++ { key := ExperimentUserKey(userID) lowArm := ResolveExperimentArm(low, key, "") highArm := ResolveExperimentArm(high, key, "") if lowArm == ExperimentArmTreatment { treatmentAtLow++ if highArm != ExperimentArmTreatment { t.Fatalf("user %d left treatment when traffic increased", userID) } } } if treatmentAtLow == 0 { t.Fatal("5% ramp assigned nobody to treatment in a 2000-user sample") } } // 分桶应大致均匀:50% 流量在 10000 用户样本上偏差不应超过 ±5 个百分点。 func TestExperimentBucketDistribution(t *testing.T) { experiment := Experiment{Status: ExperimentStatusRunning, BucketSalt: "distribution", TreatmentTrafficPPM: 500_000} treatment := 0 const sample = 10_000 for userID := int64(1); userID <= sample; userID++ { if ResolveExperimentArm(experiment, ExperimentUserKey(userID), "") == ExperimentArmTreatment { treatment++ } } if treatment < sample*45/100 || treatment > sample*55/100 { t.Fatalf("treatment share %d/%d deviates from 50%% beyond tolerance", treatment, sample) } } func TestResolveExperimentArmPrecedence(t *testing.T) { experiment := Experiment{Status: ExperimentStatusRunning, BucketSalt: "s", TreatmentTrafficPPM: 0} if arm := ResolveExperimentArm(experiment, "7", ExperimentArmTreatment); arm != ExperimentArmTreatment { t.Fatalf("override treatment ignored, got %s", arm) } full := experiment full.TreatmentTrafficPPM = ExperimentTrafficPPMScale if arm := ResolveExperimentArm(full, "7", ExperimentArmControl); arm != ExperimentArmControl { t.Fatalf("override control ignored at 100%% traffic, got %s", arm) } if arm := ResolveExperimentArm(full, "7", ""); arm != ExperimentArmTreatment { t.Fatalf("100%% traffic should send everyone to treatment, got %s", arm) } // paused 是紧急停闸:包括白名单在内的全部流量回落 control。 paused := full paused.Status = ExperimentStatusPaused if arm := ResolveExperimentArm(paused, "7", ExperimentArmTreatment); arm != ExperimentArmControl { t.Fatalf("paused experiment must send everyone to control, got %s", arm) } }