feat(awards): refuse an ambiguous match on one-reference awards

Two DARC DOKs are both called "Gießen" — two clubs share the town. Exact
matching finds both and both are right as far as the matcher can tell, so
it kept both and the operator got a QSO credited to a DOK that may well
have been the other one.

Picking one is not an option: it writes a reference into the log with no
evidence behind it. So a Def can now be marked one reference per QSO, and
an ambiguous match on such an award assigns none, names the candidates in
the trace and leaves the contact under missing references — where the
operator's choice now sticks (06e3437).

Off by default, and deliberately NOT a revival of the old `Multi` flag: a
field holding several references (an n-fer POTA activation, a VUCC grid
line) is several references, correctly, and stays that way.
This commit is contained in:
2026-08-01 01:04:17 +02:00
parent 06e34372ed
commit a4623e9ea3
6 changed files with 95 additions and 13 deletions
+44
View File
@@ -54,3 +54,47 @@ func TestManualReferenceReplacesTheMatch(t *testing.T) {
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)
}
}