From 530c3ff395f9cdcf5fee8b117c3a9fecb185dac8 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 00:54:43 +0400 Subject: [PATCH] claim tests: band order, coverage, the Rome case (V-565) TestCoverageBeatsBand is the V-558 failure as an ordering assertion. The weather claim explains the whole utterance and the pending reminder explains one token of it, so coverage settles it before the band is consulted. TestBandOrder asserts the order rather than trusting the iota, and pins BandUnknown at the bottom: a builder that forgot to set a band is a bug and must not outrank a measured claim. --- internal/claim/claim_test.go | 120 +++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 internal/claim/claim_test.go 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") + } +}