router: add TryFastPath tests and Route identity invariant (slice 10)

This commit is contained in:
2026-09-07 01:24:28 +04:00
parent 2ef470c2ba
commit a1e4743492
+216
View File
@@ -0,0 +1,216 @@
package router
import (
"context"
"testing"
)
func TestTryFastPathMatchesReminderGrammar(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, ReminderGrammar())
now := refNow()
fast, err := r.TryFastPath(context.Background(), NormalizedInput{Text: "напомни позвонить маме завтра"}, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if !fast.Matched {
t.Fatal("expected Matched=true")
}
d := fast.Decision
if d.Intent != IntentReminder {
t.Errorf("Intent = %q, want %q", d.Intent, IntentReminder)
}
if d.Stage != 0 {
t.Errorf("Stage = %d, want 0", d.Stage)
}
if d.Producer != RouteProducerGrammar {
t.Errorf("Producer = %q, want %q", d.Producer, RouteProducerGrammar)
}
if d.Confidence != 1.0 {
t.Errorf("Confidence = %f, want 1.0", d.Confidence)
}
}
func TestTryFastPathMatchesWithWakeToken(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
now := refNow()
fast, err := r.TryFastPath(context.Background(), NormalizedInput{Text: "Мэйвен который час"}, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if !fast.Matched {
t.Fatal("expected Matched=true")
}
d := fast.Decision
if d.Intent != IntentSystem {
t.Errorf("Intent = %q, want %q", d.Intent, IntentSystem)
}
if d.Stage != 0 {
t.Errorf("Stage = %d, want 0", d.Stage)
}
}
func TestTryFastPathMissFallsThrough(t *testing.T) {
r := newTestRouter(t, 0.0)
now := refNow()
fast, err := r.TryFastPath(context.Background(), NormalizedInput{Text: "как дела"}, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if fast.Matched {
t.Fatal("expected Matched=false for unmatched utterance")
}
}
func TestTryFastPathGrammarOrderPreserved(t *testing.T) {
acts := DefaultActMatcher{Fns: []string{"restart"}}
grammars := StageZeroGrammars(acts)
if len(grammars) == 0 {
t.Fatal("StageZeroGrammars returned empty list")
}
r := New(Config{
Grammars: grammars,
Classifier: nil,
Extractor: Extractor{Time: StubDateTimeParser{}, Acts: acts},
Threshold: 0.55,
})
// Verify TryFastPath uses the same ordered list by checking grammar names.
// We can't read r.grammars directly from outside the package, but we can
// verify the count matches.
if len(r.grammars) != len(grammars) {
t.Errorf("Router.grammars length = %d, want %d", len(r.grammars), len(grammars))
}
}
func TestTryFastPathCapabilitySelection(t *testing.T) {
r := newTestRouter(t, 0.0)
now := refNow()
fast, err := r.TryFastPath(context.Background(), NormalizedInput{Text: "maven, restart nginx"}, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if !fast.Matched {
t.Fatal("expected Matched=true")
}
d := fast.Decision
if d.Intent != IntentAct {
t.Errorf("Intent = %q, want %q", d.Intent, IntentAct)
}
if !d.CapabilitySelection.Resolved {
t.Error("CapabilitySelection.Resolved = false, want true")
}
if d.CapabilitySelection.Fn != "restart" {
t.Errorf("CapabilitySelection.Fn = %q, want %q", d.CapabilitySelection.Fn, "restart")
}
if d.CapabilitySelection.Method != ActionResolutionGrammarMatcher {
t.Errorf("CapabilitySelection.Method = %q, want %q", d.CapabilitySelection.Method, ActionResolutionGrammarMatcher)
}
}
func TestTryFastPathDoesNotReadMatchText(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
now := refNow()
// Pass an input where MatchText differs from Text. If any grammar
// consumed MatchText, the result would differ from using Text alone.
input := NormalizedInput{
Text: "который час",
MatchText: "totally different text that should not be used",
}
fast, err := r.TryFastPath(context.Background(), input, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if !fast.Matched {
t.Fatal("expected Matched=true for time query")
}
if fast.Decision.Intent != IntentSystem {
t.Errorf("Intent = %q, want %q (grammar should use Text, not MatchText)", fast.Decision.Intent, IntentSystem)
}
}
func TestRouteIdenticalBeforeAfter(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
r.grammars = append(r.grammars, ReminderGrammar())
now := refNow()
utterances := []struct {
text string
intent Intent
stage int
}{
{"напомни позвонить маме завтра", IntentReminder, 0},
{"который час", IntentSystem, 0},
{"как дела", IntentChat, 2}, // falls through to classifier (stage 2)
}
for _, u := range utterances {
d, err := r.Route(context.Background(), u.text, now)
if err != nil {
t.Errorf("%s: Route: %v", u.text, err)
continue
}
if d.Intent != u.intent {
t.Errorf("%s: Intent = %q, want %q", u.text, d.Intent, u.intent)
}
if d.Stage != u.stage {
t.Errorf("%s: Stage = %d, want %d", u.text, d.Stage, u.stage)
}
if u.stage == 0 && d.Producer != RouteProducerGrammar {
t.Errorf("%s: Producer = %q, want %q", u.text, d.Producer, RouteProducerGrammar)
}
}
}
func TestTryFastPathSourceAnchored(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, AgendaQueryGrammars()...)
now := refNow()
// "что в календаре на завтра" — calendar-query names SourceCalendar.
fast, err := r.TryFastPath(context.Background(), NormalizedInput{Text: "что в календаре на завтра"}, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if !fast.Matched {
t.Fatal("expected Matched=true")
}
d := fast.Decision
if d.Source != SourceCalendar {
t.Errorf("Source = %q, want %q", d.Source, SourceCalendar)
}
if !d.SourceAnchored {
t.Error("SourceAnchored = false, want true (grammar named the destination)")
}
}
func TestTryFastPathFillMatchedSlots(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, ReminderGrammar())
now := refNow()
// "напомни в 11:00 позвонить маме" — grammar captures text, extractor fills time.
fast, err := r.TryFastPath(context.Background(), NormalizedInput{Text: "напомни в 11:00 позвонить маме"}, now)
if err != nil {
t.Fatalf("TryFastPath: %v", err)
}
if !fast.Matched {
t.Fatal("expected Matched=true")
}
d := fast.Decision
if !d.Slots.HasTime {
t.Error("HasTime = false, want true (fillMatchedSlots should fill time)")
}
if got, want := d.Slots.Time.Format("15:04"), "11:00"; got != want {
t.Errorf("Time = %s, want %s", got, want)
}
}