package businessscope import ( "testing" "time" ) func validCandidate(row int, vehicleID, customerID int64, vin string) Candidate { return Candidate{ RowNumber: row, VehicleID: vehicleID, VehiclePresent: true, VIN: vin, PlateNumber: "沪A00001", CustomerID: customerID, CustomerPresent: true, CustomerProfileExists: true, ContractID: vehicleID + 1000, ContractPresent: true, ContractProfileExists: true, EffectiveCustomerID: customerID, EffectiveCustomerSet: true, ContractCode: "HT-1", ProjectName: "项目", OperationStatus: "1", ScopeStartAt: time.Date(2026, 7, 1, 8, 0, 0, 0, time.FixedZone("CST", 8*3600)), } } func TestBuildSnapshotClassifiesAndNormalizes(t *testing.T) { candidates := []Candidate{ validCandidate(1, 10, 100, " lvin0001 "), func() Candidate { value := validCandidate(2, 11, 101, "LVIN0002") value.CustomerProfileExists = false return value }(), func() Candidate { value := validCandidate(3, 12, 102, "LVIN0003") value.EffectiveCustomerID = 999 return value }(), } snapshot, err := BuildSnapshot(candidates, time.Now()) if err != nil { t.Fatal(err) } if snapshot.Candidates != 3 || len(snapshot.Items) != 1 || len(snapshot.Rejections) != 2 { t.Fatalf("unexpected accounting: %#v", snapshot) } if snapshot.Items[0].VIN != "LVIN0001" { t.Fatalf("VIN not normalized: %q", snapshot.Items[0].VIN) } if snapshot.Rejections[0].ReasonCode != ReasonCustomerProfileMissing || snapshot.Rejections[1].ReasonCode != ReasonCustomerMismatch { t.Fatalf("unexpected reasons: %#v", snapshot.Rejections) } } func TestBuildSnapshotRejectsEveryDuplicateVehicle(t *testing.T) { snapshot, err := BuildSnapshot([]Candidate{ validCandidate(1, 10, 100, "LVIN0001"), validCandidate(2, 10, 100, "LVIN0001"), }, time.Now()) if err != nil { t.Fatal(err) } if len(snapshot.Items) != 0 || len(snapshot.Rejections) != 2 { t.Fatalf("duplicates must all be quarantined: %#v", snapshot) } for _, rejection := range snapshot.Rejections { if rejection.ReasonCode != ReasonDuplicateVehicle { t.Fatalf("unexpected duplicate reason: %#v", rejection) } } } func TestSnapshotVersionIsContentAddressed(t *testing.T) { first := []Candidate{validCandidate(1, 10, 100, "LVIN0001"), validCandidate(2, 11, 101, "LVIN0002")} second := []Candidate{first[1], first[0]} a, err := BuildSnapshot(first, time.Now()) if err != nil { t.Fatal(err) } b, err := BuildSnapshot(second, time.Now().Add(time.Hour)) if err != nil { t.Fatal(err) } if a.SourceVersion != b.SourceVersion || a.Checksum != b.Checksum { t.Fatalf("same content produced different versions: %s %s", a.SourceVersion, b.SourceVersion) } } func TestValidateSnapshotUsesCountAndRatioGates(t *testing.T) { snapshot := Snapshot{Candidates: 639, Items: make([]ScopeItem, 613), Rejections: make([]Rejection, 26)} if err := ValidateSnapshot(snapshot, 100, 0.10); err != nil { t.Fatalf("production baseline should pass: %v", err) } if err := ValidateSnapshot(snapshot, 20, 0.10); err == nil { t.Fatal("rejection count limit should fail") } if err := ValidateSnapshot(snapshot, 100, 0.01); err == nil { t.Fatal("rejection ratio limit should fail") } }