package award import ( "strings" "testing" "hamlog/internal/qso" ) // An operator's correction must survive a recompute. // // Reported on the DARC DOK award: a German address matches two or three DOKs, // the operator deletes the wrong ones and assigns the right one, and the next // recompute puts the wrong ones straight back. The manual reference was being // ADDED to what the matcher found instead of replacing it, so the correction // could never win. func TestManualReferenceReplacesTheMatch(t *testing.T) { // An award that reads the address and matches a reference's description — // the shape the DOK award uses, and the shape that over-matches. def := Def{ Code: "DLD", Name: "DARC DOK", Field: "address", MatchBy: "description", Type: "QSOFIELDS", Valid: true, } metas := []RefMeta{ {Code: "A01", Name: "Berlin", Valid: true}, {Code: "B05", Name: "Berlin Mitte", Valid: true}, } // An address that legitimately matches both. q := &qso.QSO{ Callsign: "DL2FDM", Address: "Berlin Mitte, 10115 Berlin", } auto := Explain(def, metas, q) if len(auto.Result) < 2 { t.Fatalf("the test needs an over-matching case; got %v", auto.Result) } // The operator picks the right one. q.Extras = map[string]string{ManualRefsKey: "DLD@B05"} fixed := Explain(def, metas, q) if got := strings.Join(fixed.Result, ","); got != "B05" { t.Errorf("after the correction the QSO counts for %q — want B05 alone", got) } if len(fixed.Superseded) == 0 { t.Error("the trace does not say what the correction replaced") } // A QSO with no correction still gets everything the matcher found: the // override must not disable matching for everyone else. q2 := &qso.QSO{Callsign: "DL3XYZ", Address: "Berlin Mitte, 10115 Berlin"} if len(Explain(def, metas, q2).Result) < 2 { t.Error("an untouched QSO lost its automatic references") } } // On an award where a QSO can only count for ONE reference, an ambiguous match // assigns none. // // Two DARC DOKs are called "Gießen" because two clubs share the town. Exact // matching finds both, and both are right as far as the matcher can tell. // Picking one would write a reference into the log that may well be the other — // so the contact is left for the operator, who now has the last word. func TestAmbiguousMatchIsRefusedWhenOnlyOneReferenceIsAllowed(t *testing.T) { def := Def{ Code: "DLD", Name: "DARC DOK", Field: "qth", MatchBy: "description", ExactMatch: true, Type: "QSOFIELDS", Valid: true, OneRefPerQSO: true, } metas := []RefMeta{ {Code: "F07", Name: "Gießen", Valid: true}, {Code: "F61", Name: "Gießen", Valid: true}, {Code: "B11", Name: "Kassel", Valid: true}, } amb := Explain(def, metas, &qso.QSO{Callsign: "DL2FDM", QTH: "Gießen"}) if len(amb.Result) != 0 { t.Errorf("an ambiguous match produced %v — it should produce nothing", amb.Result) } if len(amb.Ambiguous) != 2 { t.Errorf("the trace lists %v — it should name both candidates", amb.Ambiguous) } // An unambiguous one is untouched: this must not make the award stricter in // general, only where the data genuinely does not decide. ok := Explain(def, metas, &qso.QSO{Callsign: "DL3XYZ", QTH: "Kassel"}) if len(ok.Result) != 1 || ok.Result[0] != "B11" { t.Errorf("an unambiguous QSO gave %v — want B11", ok.Result) } // And the operator's choice still wins over the ambiguity. fixed := Explain(def, metas, &qso.QSO{ Callsign: "DL2FDM", QTH: "Gießen", Extras: map[string]string{ManualRefsKey: "DLD@F61"}, }) if len(fixed.Result) != 1 || fixed.Result[0] != "F61" { t.Errorf("after the operator picked F61 the QSO counts for %v", fixed.Result) } }