router: update all Route callers for NormalizedInput + add invariant tests (slice 11)

Migrate 54 test call sites to construct NormalizedInput{Text: ...}.
Add invariant tests:
- TestNormalizedInputReachesRouteIntact: ingress NormalizedInput reaches Route
- TestTryFastPathReceivesMatchText: TryFastPath gets the same input
- TestMatchTextDoesNotChangeRouting: same Text + different MatchText → same Decision
- TestDecisionUtteranceEqualsInputText: Decision.Utterance == input.Text
This commit is contained in:
2026-09-07 01:32:49 +04:00
parent dad3cd0738
commit 494c719a5d
12 changed files with 195 additions and 61 deletions
+7 -7
View File
@@ -28,7 +28,7 @@ func TestAgendaQuestionsRouteToQuery(t *testing.T) {
"какие у меня встречи завтра",
"покажи расписание на среду",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -48,7 +48,7 @@ func TestAgendaGrammarsLeaveTheClockAlone(t *testing.T) {
"который час",
"сколько сейчас времени",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -66,7 +66,7 @@ func TestAgendaGrammarSparesStatements(t *testing.T) {
"у меня кончилась вода",
"напомни мне завтра позвонить маме",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -89,7 +89,7 @@ func TestNarrativeGrammarsRouteToQuery(t *testing.T) {
"объясни как работает дизель",
"опиши Ватерлоо",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("%q: %v", u, err)
}
@@ -114,7 +114,7 @@ func TestAgendaCoversOtherDaysAndNamedEvents(t *testing.T) {
"во сколько созвон",
"когда будет совещание",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -129,7 +129,7 @@ func TestAgendaCoversOtherDaysAndNamedEvents(t *testing.T) {
func TestNarrativeGrammarLeavesCapturesAlone(t *testing.T) {
r := agendaRouter(t)
r.grammars = append(r.grammars, NarrativeQueryGrammars()...)
d, err := r.Route(context.Background(), "расскажи и запиши что я пил воду", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "расскажи и запиши что я пил воду"}, refNow())
if err != nil {
t.Fatal(err)
}
@@ -147,7 +147,7 @@ func TestAgendaGrammarsLeaveTheWorldAlone(t *testing.T) {
"когда была битва при ватерлоо",
"когда изобрели телефон",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
+4 -4
View File
@@ -58,7 +58,7 @@ func TestStage0SetsGrammarProducer(t *testing.T) {
r := buildTestRouter(t)
now := time.Now()
// "напомни позвонить маме завтра" — a reminder grammar match.
d, err := r.Route(context.Background(), "напомни позвонить маме завтра", now)
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни позвонить маме завтра"}, now)
if err != nil {
t.Fatalf("Route: %v", err)
}
@@ -76,7 +76,7 @@ func TestClassifierSetsProducer(t *testing.T) {
r := buildTestRouterNoModel(t)
now := time.Now()
// "как дела" — a free-form chat utterance that no grammar matches.
d, err := r.Route(context.Background(), "как дела", now)
d, err := r.Route(context.Background(), NormalizedInput{Text: "как дела"}, now)
if err != nil {
t.Fatalf("Route: %v", err)
}
@@ -93,7 +93,7 @@ func TestClarifyProducerIsClassifier(t *testing.T) {
now := time.Now()
// "привет как дела что нового" — a long ambiguous utterance that no
// grammar matches and the classifier scores below the clarify threshold.
d, err := r.Route(context.Background(), "привет как дела что нового", now)
d, err := r.Route(context.Background(), NormalizedInput{Text: "привет как дела что нового"}, now)
if err != nil {
t.Fatalf("Route: %v", err)
}
@@ -119,7 +119,7 @@ func TestStage0ProducerOnEveryGrammar(t *testing.T) {
{"который час", "system-time"},
}
for _, u := range utterances {
d, err := r.Route(context.Background(), u.text, now)
d, err := r.Route(context.Background(), NormalizedInput{Text: u.text}, now)
if err != nil {
t.Errorf("%s: Route: %v", u.name, err)
continue
+3 -3
View File
@@ -321,7 +321,7 @@ func TestRouterRoute_CapabilitySelectionPopulated(t *testing.T) {
r := newTestRouter(t, 0.3)
// Stage-0 grammar path: wakeword-act.
d, err := r.Route(t.Context(), "maven restart nginx", refNow())
d, err := r.Route(t.Context(), NormalizedInput{Text: "maven restart nginx"}, refNow())
if err != nil {
t.Fatal(err)
}
@@ -336,7 +336,7 @@ func TestRouterRoute_CapabilitySelectionPopulated(t *testing.T) {
}
// Classifier path: raw utterance matches allowlist.
d, err = r.Route(t.Context(), "restart nginx", refNow())
d, err = r.Route(t.Context(), NormalizedInput{Text: "restart nginx"}, refNow())
if err != nil {
t.Fatal(err)
}
@@ -365,7 +365,7 @@ func TestRouterRoute_ClassifierUnresolvedAct(t *testing.T) {
Threshold: 0.3,
})
d, err := r.Route(t.Context(), "deploy the thing", refNow())
d, err := r.Route(t.Context(), NormalizedInput{Text: "deploy the thing"}, refNow())
if err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -145,7 +145,7 @@ func TestClaimOfLeavesTheDecisionAlone(t *testing.T) {
Extractor: Extractor{},
Threshold: 0.55,
})
before, err := r.Route(context.Background(), "напомни полить цветы", time.Now())
before, err := r.Route(context.Background(), NormalizedInput{Text: "напомни полить цветы"}, time.Now())
if err != nil {
t.Fatalf("Route: %v", err)
}
+1 -1
View File
@@ -123,7 +123,7 @@ func reportConfidences(t *testing.T, name string, emb router.Embedder) {
byMargin := map[string]*bucket{}
var cosines, margins []float64
for _, c := range f.Cases {
d, err := r.Route(context.Background(), c.Utterance, now)
d, err := r.Route(context.Background(), router.NormalizedInput{Text: c.Utterance}, now)
if err != nil {
t.Fatalf("%s: %v", c.ID, err)
}
+3 -3
View File
@@ -78,7 +78,7 @@ func TestResolutionMethodMatrix(t *testing.T) {
resolved := 0
counts := [5]int{} // fixed, matcher, raw, llm, fallback
for _, c := range rf.Cases {
d, err := r.Route(ctx, c.Utterance, rfNow)
d, err := r.Route(ctx, router.NormalizedInput{Text: c.Utterance}, rfNow)
if err != nil || d.Intent != router.IntentAct {
continue
}
@@ -132,7 +132,7 @@ func TestResolutionMethodMatrix(t *testing.T) {
resolved := 0
counts := [5]int{}
for _, c := range ef.Cases {
d, err := r2.Route(ctx, c.Utterance, efNow)
d, err := r2.Route(ctx, router.NormalizedInput{Text: c.Utterance}, efNow)
if err != nil || d.Intent != router.IntentAct {
continue
}
@@ -189,7 +189,7 @@ func TestShadowMatcherComparison(t *testing.T) {
same, different, routeOnly, matcherOnly, missBoth, total := 0, 0, 0, 0, 0, 0
for _, c := range f.Cases {
d, err := r.Route(ctx, c.Utterance, now)
d, err := r.Route(ctx, router.NormalizedInput{Text: c.Utterance}, now)
if err != nil || d.Intent != router.IntentAct {
continue
}
+135 -1
View File
@@ -154,7 +154,7 @@ func TestRouteIdenticalBeforeAfter(t *testing.T) {
}
for _, u := range utterances {
d, err := r.Route(context.Background(), u.text, now)
d, err := r.Route(context.Background(), NormalizedInput{Text: u.text}, now)
if err != nil {
t.Errorf("%s: Route: %v", u.text, err)
continue
@@ -214,3 +214,137 @@ func TestTryFastPathFillMatchedSlots(t *testing.T) {
t.Errorf("Time = %s, want %s", got, want)
}
}
// TestNormalizedInputReachesRouteIntact pins that the NormalizedInput
// constructed at ingress arrives at Router.Route without reconstruction.
func TestNormalizedInputReachesRouteIntact(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
now := refNow()
input := NormalizedInput{
Text: "который час",
MatchText: "который час",
Source: InputSourceText,
}
d, err := r.Route(context.Background(), input, now)
if err != nil {
t.Fatalf("Route: %v", err)
}
if d.Intent != IntentSystem {
t.Errorf("Intent = %q, want %q", d.Intent, IntentSystem)
}
if d.Utterance != input.Text {
t.Errorf("Utterance = %q, want %q (Decision.Utterance must equal input.Text)", d.Utterance, input.Text)
}
}
// TestTryFastPathReceivesMatchText pins that TryFastPath receives the
// same NormalizedInput that Route was given (including MatchText).
// Today no grammar reads MatchText, so the result must be identical
// whether MatchText is set or empty — this freezes the dark-data contract.
func TestTryFastPathReceivesMatchText(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
now := refNow()
without := NormalizedInput{Text: "который час"}
with := NormalizedInput{Text: "который час", MatchText: "который час"}
fastWithout, err := r.TryFastPath(context.Background(), without, now)
if err != nil {
t.Fatalf("TryFastPath (without): %v", err)
}
fastWith, err := r.TryFastPath(context.Background(), with, now)
if err != nil {
t.Fatalf("TryFastPath (with): %v", err)
}
if fastWithout.Matched != fastWith.Matched {
t.Errorf("Matched: without=%v, with=%v", fastWithout.Matched, fastWith.Matched)
}
if fastWithout.Matched && fastWith.Matched {
if fastWithout.Decision.Intent != fastWith.Decision.Intent {
t.Errorf("Intent: without=%q, with=%q", fastWithout.Decision.Intent, fastWith.Decision.Intent)
}
if fastWithout.Decision.Confidence != fastWith.Decision.Confidence {
t.Errorf("Confidence: without=%f, with=%f", fastWithout.Decision.Confidence, fastWith.Decision.Confidence)
}
}
}
// TestMatchTextDoesNotChangeRouting pins the dark-data invariant:
// same Text, different MatchText → same Decision. This must hold until
// an explicit later slice opts a consumer into MatchText.
func TestMatchTextDoesNotChangeRouting(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
want Intent
}{
{"который час", IntentSystem},
{"напомни позвонить маме завтра", IntentReminder},
}
for _, u := range utterances {
without := NormalizedInput{Text: u.text}
with := NormalizedInput{Text: u.text, MatchText: NormalizeMatchText(u.text)}
dWithout, err := r.Route(context.Background(), without, now)
if err != nil {
t.Errorf("%s (without MatchText): Route: %v", u.text, err)
continue
}
dWith, err := r.Route(context.Background(), with, now)
if err != nil {
t.Errorf("%s (with MatchText): Route: %v", u.text, err)
continue
}
if dWithout.Intent != dWith.Intent {
t.Errorf("%s: Intent changed: without=%q, with=%q", u.text, dWithout.Intent, dWith.Intent)
}
if dWithout.Confidence != dWith.Confidence {
t.Errorf("%s: Confidence changed: without=%f, with=%f", u.text, dWithout.Confidence, dWith.Confidence)
}
if dWithout.Stage != dWith.Stage {
t.Errorf("%s: Stage changed: without=%d, with=%d", u.text, dWithout.Stage, dWith.Stage)
}
}
}
// TestDecisionUtteranceEqualsInputText pins that Decision.Utterance is
// always input.Text, regardless of which cascade path was taken.
func TestDecisionUtteranceEqualsInputText(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
want Intent
}{
{"который час", IntentSystem},
{"напомни позвонить маме завтра", IntentReminder},
{"как дела", IntentChat},
}
for _, u := range utterances {
input := NormalizedInput{Text: u.text}
d, err := r.Route(context.Background(), input, now)
if err != nil {
t.Errorf("%s: Route: %v", u.text, err)
continue
}
if d.Intent != u.want {
t.Errorf("%s: Intent = %q, want %q", u.text, d.Intent, u.want)
continue
}
if d.Utterance != u.text {
t.Errorf("%s: Decision.Utterance = %q, want %q", u.text, d.Utterance, u.text)
}
}
}
+2 -2
View File
@@ -25,7 +25,7 @@ func TestFeedQuestionsRouteToQuery(t *testing.T) {
"расскажи что в новостных лентах",
"покажи ленту",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -44,7 +44,7 @@ func TestFeedGrammarLeavesTheGreetingAlone(t *testing.T) {
"что нового?",
"у меня новая лента в инстаграме",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
+17 -17
View File
@@ -168,7 +168,7 @@ func TestRouterFallsBackWhenLLMRefuses(t *testing.T) {
Threshold: 0.4,
LLM: NewLLMRouter(mockLLM{out: `{"intent":"unknown"}`}),
})
d, err := r.Route(context.Background(), "напомни позвонить маме", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни позвонить маме"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -206,7 +206,7 @@ func newLLMTestRouter(t *testing.T, out string) *Router {
// reminder was dropped as "no time".
func TestLLMDecisionGetsReminderTime(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"reminder","text":"позвонить маме"}`)
d, err := r.Route(context.Background(), "напомни позвонить маме через 2 часа", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни позвонить маме через 2 часа"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -225,7 +225,7 @@ func TestLLMDecisionGetsReminderTime(t *testing.T) {
// daemon says it could not read the time.
func TestLLMReminderWithoutTimeStaysEmpty(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"reminder","text":"позвонить маме"}`)
d, err := r.Route(context.Background(), "напомни позвонить маме", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни позвонить маме"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -237,7 +237,7 @@ func TestLLMReminderWithoutTimeStaysEmpty(t *testing.T) {
// An act decision arrived with no Fn, so the tool never ran.
func TestLLMDecisionGetsActFn(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"act","verb":"restart nginx"}`)
d, err := r.Route(context.Background(), "слушай, restart nginx пожалуйста", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "слушай, restart nginx пожалуйста"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -249,7 +249,7 @@ func TestLLMDecisionGetsActFn(t *testing.T) {
// The model's own slots win; extraction only fills gaps.
func TestLLMSlotsWinOverExtraction(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"fact","key":"hydration","value":"выпил"}`)
d, err := r.Route(context.Background(), "я выпил воду", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "я выпил воду"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -261,7 +261,7 @@ func TestLLMSlotsWinOverExtraction(t *testing.T) {
// A fact the model left keyless still gets one from the parser.
func TestLLMFactGetsKeyFromParser(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"fact","text":"я выпил воду"}`)
d, err := r.Route(context.Background(), "я выпил воду", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "я выпил воду"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -279,7 +279,7 @@ func TestLLMFactGetsKeyFromParser(t *testing.T) {
// fact/query coin flip. The gate must ask rather than guess confidently.
func TestLLMRouterSingleTokenTripsClarify(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"query","text":"вода"}`)
d, err := r.Route(context.Background(), "вода", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "вода"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -292,7 +292,7 @@ func TestLLMRouterSingleTokenTripsClarify(t *testing.T) {
// whole point is not trading the confident cases away for clarify coverage.
func TestLLMRouterMultiWordStaysConfident(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"reminder","text":"позвонить маме"}`)
d, err := r.Route(context.Background(), "напомни позвонить маме", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни позвонить маме"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -308,7 +308,7 @@ func TestLLMRouterMultiWordStaysConfident(t *testing.T) {
// "бэкап" as a verb — that must not fire a tool blind.
func TestLLMRouterActWithoutFnTripsClarify(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"act","verb":"бэкап"}`)
d, err := r.Route(context.Background(), "бэкап сделай пожалуйста расписание", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "бэкап сделай пожалуйста расписание"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -325,7 +325,7 @@ func TestLLMRouterActWithoutFnTripsClarify(t *testing.T) {
// check firing on the happy path.
func TestLLMRouterActWithFnStaysConfident(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"act","verb":"restart nginx"}`)
d, err := r.Route(context.Background(), "слушай, restart nginx пожалуйста", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "слушай, restart nginx пожалуйста"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -341,7 +341,7 @@ func TestLLMRouterActWithFnStaysConfident(t *testing.T) {
// must clarify instead of silently writing under an empty/guessed key.
func TestLLMRouterFactWithoutKeyTripsClarify(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"fact","value":"что-то"}`)
d, err := r.Route(context.Background(), "у меня какая-то фигня случилась вот прямо только что", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "у меня какая-то фигня случилась вот прямо только что"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -358,7 +358,7 @@ func TestLLMRouterFactWithoutKeyTripsClarify(t *testing.T) {
// through the full Router.Route path, not just the raw LLMRouter.
func TestRouterLLMFactWithResolvedKeyStaysConfident(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"fact","text":"я выпил воду"}`)
d, err := r.Route(context.Background(), "я выпил воду", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "я выпил воду"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -374,7 +374,7 @@ func TestRouterLLMFactWithResolvedKeyStaysConfident(t *testing.T) {
// (Vikunja #383).
func TestLLMReminderWithoutSubjectAsksInsteadOfGuessing(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"reminder"}`)
d, err := r.Route(context.Background(), "напомни в 11", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни в 11"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -390,7 +390,7 @@ func TestLLMReminderWithoutSubjectAsksInsteadOfGuessing(t *testing.T) {
// both halves still runs without a question.
func TestLLMReminderWithSubjectIsNotGated(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"reminder","text":"позвонить маме"}`)
d, err := r.Route(context.Background(), "напомни в 11 позвонить маме", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни в 11 позвонить маме"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -431,7 +431,7 @@ func TestRouteGrammarCoversSources(t *testing.T) {
// would take real query sources off the turn for a name nothing answers.
func TestLLMUnknownSourceFallsToTheFloor(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"query","text":"что там с бэкапами","source":"praxis"}`)
d, err := r.Route(context.Background(), "что там с бэкапами", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "что там с бэкапами"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -443,7 +443,7 @@ func TestLLMUnknownSourceFallsToTheFloor(t *testing.T) {
// And a known one survives, or the read-back is just a filter.
func TestLLMNamedSourceSurvives(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"query","text":"кто такой Линус Торвальдс","source":"world"}`)
d, err := r.Route(context.Background(), "кто такой Линус Торвальдс?", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "кто такой Линус Торвальдс?"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -457,7 +457,7 @@ func TestLLMNamedSourceSurvives(t *testing.T) {
// checks.
func TestLLMSourceIsQueryOnly(t *testing.T) {
r := newLLMTestRouter(t, `{"intent":"note","text":"кофе кончился","source":"recall"}`)
d, err := r.Route(context.Background(), "запиши что кофе кончился", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "запиши что кофе кончился"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
+3 -3
View File
@@ -37,7 +37,7 @@ func TestNarrativeAndRestOfDayRouteToQueryAtStageZero(t *testing.T) {
"что дальше",
"what's next?",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -61,7 +61,7 @@ func TestNarrativeGrammarLeavesOtherTurnsAlone(t *testing.T) {
"расскажи шутку",
"расскажи",
} {
d, err := r.Route(context.Background(), u, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: u}, refNow())
if err != nil {
t.Fatalf("route(%q): %v", u, err)
}
@@ -75,7 +75,7 @@ func TestNarrativeGrammarLeavesOtherTurnsAlone(t *testing.T) {
// search leg wants "битву при Ватерлоо", not "расскажи про битву при Ватерлоо".
func TestNarrativeGrammarKeepsTheTopic(t *testing.T) {
r := narrativeRouter(t)
d, err := r.Route(context.Background(), "расскажи про битву при Ватерлоо", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "расскажи про битву при Ватерлоо"}, refNow())
if err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -56,7 +56,7 @@ func TestPossessionStatementBeatsStatisticalQueryGuess(t *testing.T) {
Classifier: classifier,
Threshold: 0,
})
d, err := r.Route(context.Background(), "у меня новый ноутбук", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "у меня новый ноутбук"}, refNow())
if err != nil {
t.Fatal(err)
}
+18 -18
View File
@@ -78,7 +78,7 @@ func newTestRouter(t *testing.T, threshold float64) *Router {
func TestStage0WakeWordAct(t *testing.T) {
r := newTestRouter(t, 0.0)
d, err := r.Route(context.Background(), "maven, restart nginx", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "maven, restart nginx"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -94,7 +94,7 @@ func TestStage0WakeWordFallsThroughOnUnknownAct(t *testing.T) {
// wakeword prefix alone doesn't guarantee a known command. "maven, i'm tired"
// is a fact-ish utterance → falls through to the classifier.
r := newTestRouter(t, 0.0)
d, err := r.Route(context.Background(), "maven, i drank water", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "maven, i drank water"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -112,7 +112,7 @@ func TestStage0GrammarFiresThroughCyrillicWakeWord(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
d, err := r.Route(context.Background(), "Мэйвен который час", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "Мэйвен который час"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -130,7 +130,7 @@ func TestStage0ReminderCarriesTheHourHeSaid(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, ReminderGrammar())
d, err := r.Route(context.Background(), "напомни в 11:00 позвонить маме", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "напомни в 11:00 позвонить маме"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -156,7 +156,7 @@ func TestStage0ReminderCarriesTheHourHeSaid(t *testing.T) {
// be able to replace it.
func TestStage0MatchedSlotBeatsTheExtractor(t *testing.T) {
r := newTestRouter(t, 0.0)
d, err := r.Route(context.Background(), "maven, restart nginx", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "maven, restart nginx"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -176,7 +176,7 @@ func TestStage0QueryKeepsAnEmptyText(t *testing.T) {
r := newTestRouter(t, 0.0)
r.grammars = append(r.grammars, AgendaQueryGrammars()...)
d, err := r.Route(context.Background(), "что у меня сегодня", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "что у меня сегодня"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -192,7 +192,7 @@ func TestStage0QueryKeepsAnEmptyText(t *testing.T) {
func TestStage1ClassifiesAct(t *testing.T) {
r := newTestRouter(t, 0.0)
d, err := r.Route(context.Background(), "restart the backup now", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "restart the backup now"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -206,7 +206,7 @@ func TestStage1ClassifiesAct(t *testing.T) {
func TestStage1ClassifiesFact(t *testing.T) {
r := newTestRouter(t, 0.0)
d, err := r.Route(context.Background(), "i drank water", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "i drank water"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -230,7 +230,7 @@ func TestStage1ClassifiesNoteAndQuery(t *testing.T) {
{"when did i last eat", IntentQuery},
}
for _, c := range cases {
d, err := r.Route(context.Background(), c.in, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: c.in}, refNow())
if err != nil {
t.Fatalf("route %q: %v", c.in, err)
}
@@ -245,7 +245,7 @@ func TestStage1ClassifiesNoteAndQuery(t *testing.T) {
func TestStage2ReminderSlotExtraction(t *testing.T) {
r := newTestRouter(t, 0.0)
now := refNow()
d, err := r.Route(context.Background(), "remind me in four hours", now)
d, err := r.Route(context.Background(), NormalizedInput{Text: "remind me in four hours"}, now)
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -265,7 +265,7 @@ func TestStage2ReminderAtClockRollsToTomorrow(t *testing.T) {
// "wake me at 7" said at 12:00 → fires tomorrow 07:00 (already past today).
r := newTestRouter(t, 0.0)
now := refNow()
d, err := r.Route(context.Background(), "wake me at 7", now)
d, err := r.Route(context.Background(), NormalizedInput{Text: "wake me at 7"}, now)
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -280,7 +280,7 @@ func TestStage2ReminderAtClockRollsToTomorrow(t *testing.T) {
func TestStage2FactSleptDuration(t *testing.T) {
r := newTestRouter(t, 0.0)
d, err := r.Route(context.Background(), "slept 6h", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "slept 6h"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -298,7 +298,7 @@ func TestStage3ClarifyBelowThreshold(t *testing.T) {
// high threshold ⇒ even a well-classified utterance is gated to clarify.
// "shuts up when uncertain": a misrouted fact is a confident wrong write.
r := newTestRouter(t, 0.99)
d, err := r.Route(context.Background(), "i drank water", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "i drank water"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -314,7 +314,7 @@ func TestStage3ClarifyBelowThreshold(t *testing.T) {
func TestStage3PassesAboveThreshold(t *testing.T) {
r := newTestRouter(t, 0.0) // threshold 0 ⇒ nothing gated
d, err := r.Route(context.Background(), "i drank water", refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: "i drank water"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -331,7 +331,7 @@ func TestColdBootNoIntents(t *testing.T) {
emb := NewHashEmbedder(128)
c := NewClassifier(emb)
r := New(Config{Classifier: c, Threshold: 0})
if _, err := r.Route(context.Background(), "something freeform", refNow()); err != ErrNoIntents {
if _, err := r.Route(context.Background(), NormalizedInput{Text: "something freeform"}, refNow()); err != ErrNoIntents {
t.Fatalf("cold boot: want ErrNoIntents, got %v", err)
}
}
@@ -342,7 +342,7 @@ func TestCorrectMisrouteGrowsClassifier(t *testing.T) {
r := newTestRouter(t, 0.4)
// "note the backup is broken" looks note-ish but the user meant a fact
// (loop should know the backup is down). Without correction it routes note.
before, err := r.Route(context.Background(), "backup is broken", refNow())
before, err := r.Route(context.Background(), NormalizedInput{Text: "backup is broken"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -359,7 +359,7 @@ func TestCorrectMisrouteGrowsClassifier(t *testing.T) {
t.Fatalf("correct: %v", err)
}
}
after, err := r.Route(context.Background(), "backup is broken", refNow())
after, err := r.Route(context.Background(), NormalizedInput{Text: "backup is broken"}, refNow())
if err != nil {
t.Fatalf("route: %v", err)
}
@@ -403,7 +403,7 @@ func TestStage1ClassifiesChat(t *testing.T) {
{"расскажи что-нибудь", IntentChat},
}
for _, c := range cases {
d, err := r.Route(context.Background(), c.in, refNow())
d, err := r.Route(context.Background(), NormalizedInput{Text: c.in}, refNow())
if err != nil {
t.Fatalf("route %q: %v", c.in, err)
}