diff --git a/internal/claim/claim_test.go b/internal/claim/claim_test.go new file mode 100644 index 0000000..16c4310 --- /dev/null +++ b/internal/claim/claim_test.go @@ -0,0 +1,120 @@ +package claim + +import ( + "reflect" + "testing" +) + +func TestTokensStripsPunctuationAndCase(t *testing.T) { + got := Tokens("Какая сейчас погода в Риме?") + want := []string{"какая", "сейчас", "погода", "в", "риме"} + if !reflect.DeepEqual(got, want) { + t.Errorf("Tokens = %q, want %q", got, want) + } +} + +// The dash forms matter: an STT transcript routinely carries "а, да, прости - +// на 9", and a stray dash counted as a token would dilute every coverage score +// in the sentence. +func TestTokensDropsBareDashes(t *testing.T) { + got := Tokens("а, да, прости — на 9") + want := []string{"а", "да", "прости", "на", "9"} + if !reflect.DeepEqual(got, want) { + t.Errorf("Tokens = %q, want %q", got, want) + } +} + +func TestSplitPartitionsInOrder(t *testing.T) { + consumed, unexplained := Split("напомни позвонить маме", "позвонить маме") + if want := []string{"позвонить", "маме"}; !reflect.DeepEqual(consumed, want) { + t.Errorf("consumed = %q, want %q", consumed, want) + } + if want := []string{"напомни"}; !reflect.DeepEqual(unexplained, want) { + t.Errorf("unexplained = %q, want %q", unexplained, want) + } +} + +func TestCoverageIsZeroWithoutTokens(t *testing.T) { + if got := (Claim{}).Coverage(); got != 0 { + t.Errorf("Coverage of an empty claim = %v, want 0", got) + } +} + +func TestCoverageFraction(t *testing.T) { + c := Claim{Consumed: []string{"a", "b", "c"}, Unexplained: []string{"d"}} + if got := c.Coverage(); got != 0.75 { + t.Errorf("Coverage = %v, want 0.75", got) + } +} + +// The band order is load-bearing, so it is asserted rather than assumed from +// the iota. BandUnknown must sit at the bottom: a claim whose builder forgot to +// set a band is a bug and must not outrank a measured one. +func TestBandOrder(t *testing.T) { + ordered := []Band{BandUnknown, BandVetoed, BandNearest, BandStructural, BandAnchored} + for i := 1; i < len(ordered); i++ { + if !(ordered[i-1] < ordered[i]) { + t.Errorf("%v is not below %v", ordered[i-1], ordered[i]) + } + } + for _, b := range ordered { + if b.String() == "" { + t.Errorf("band %d has no name", b) + } + } + if BandUnknown.String() != "unknown" { + t.Errorf("BandUnknown.String() = %q", BandUnknown.String()) + } +} + +// The failure V-558 opened with, as an ordering test. A pending reminder eats +// "какая сейчас погода в Риме?" as a time answer and explains none of it; the +// weather source explains all of it. Coverage decides, and the band never gets +// consulted, which is the point: the pending claimant is structural and the +// weather claim is anchored, but even if the bands were equal the weather claim +// wins. +func TestCoverageBeatsBand(t *testing.T) { + utterance := "какая сейчас погода в Риме?" + pendingConsumed, pendingRest := Split(utterance, "сейчас") + pending := Claim{ + Claimant: "reminder-followup", Intent: "reminder", + Consumed: pendingConsumed, Unexplained: pendingRest, + Band: BandStructural, + } + weatherConsumed, weatherRest := Split(utterance, utterance) + weather := Claim{ + Claimant: "weather", Intent: "query", + Consumed: weatherConsumed, Unexplained: weatherRest, + Band: BandNearest, + } + if !weather.MoreSpecificThan(pending) { + t.Errorf("weather (%v cover) did not beat pending (%v cover)", + weather.Coverage(), pending.Coverage()) + } + if pending.MoreSpecificThan(weather) { + t.Error("pending beat weather, so the ordering is not asymmetric") + } +} + +// Where coverage ties, the band decides. Two grammars claiming the same +// utterance is the stage-0 contention case (ru-query-019 on the fixture), and +// today list order settles it with nothing recorded. +func TestBandBreaksACoverageTie(t *testing.T) { + anchored := Claim{Claimant: "calendar-query", Consumed: []string{"a"}, Band: BandAnchored} + nearest := Claim{Claimant: "classifier", Consumed: []string{"a"}, Band: BandNearest} + if !anchored.MoreSpecificThan(nearest) { + t.Error("anchored did not beat nearest on equal coverage") + } + if nearest.MoreSpecificThan(anchored) { + t.Error("nearest beat anchored on equal coverage") + } +} + +func TestVetoed(t *testing.T) { + if (Claim{}).Vetoed() { + t.Error("a claim with no veto reports itself vetoed") + } + if !(Claim{Veto: "fact with no key"}).Vetoed() { + t.Error("a claim with a veto reason does not report itself vetoed") + } +}