package router import ( "testing" ) // --- SelectCapability tests --- // TestSelectCapability_DeterministicBypass pins that a grammar-fixed act // bypasses the general selector and records the existing result. func TestSelectCapability_DeterministicBypass(t *testing.T) { dec := Decision{ Intent: IntentAct, Slots: Slots{ Fn: "resolve_item", HasFn: true, ResolvedBy: ActionResolutionGrammarFixed, }, Producer: RouteProducerGrammar, Confidence: 1.0, } sel := SelectCapability(dec, nil) if !sel.Resolved { t.Fatal("expected resolved") } if sel.Fn != "resolve_item" { t.Errorf("Fn = %q, want resolve_item", sel.Fn) } if sel.Method != ActionResolutionGrammarFixed { t.Errorf("Method = %q, want grammar_fixed", sel.Method) } if sel.InputKind != SelectionDeterministic { t.Errorf("InputKind = %q, want deterministic", sel.InputKind) } if sel.Producer != RouteProducerGrammar { t.Errorf("Producer = %q, want grammar", sel.Producer) } if sel.Confidence != 1.0 { t.Errorf("Confidence = %f, want 1.0", sel.Confidence) } } // TestSelectCapability_ExtractorRawBypass pins that an extractor-raw act // bypasses the general selector. func TestSelectCapability_ExtractorRawBypass(t *testing.T) { dec := Decision{ Intent: IntentAct, Slots: Slots{ Fn: "restart", Args: []string{"nginx"}, HasFn: true, ResolvedBy: ActionResolutionExtractorRaw, }, Producer: RouteProducerClassifier, } sel := SelectCapability(dec, nil) if !sel.Resolved { t.Fatal("expected resolved") } if sel.Fn != "restart" { t.Errorf("Fn = %q, want restart", sel.Fn) } if sel.Method != ActionResolutionExtractorRaw { t.Errorf("Method = %q, want extractor_raw", sel.Method) } if sel.InputKind != SelectionDeterministic { t.Errorf("InputKind = %q, want deterministic", sel.InputKind) } } // TestSelectCapability_GrammarMatcherBypass pins that a grammar-matcher act // (wakeword-act) bypasses the general selector. func TestSelectCapability_GrammarMatcherBypass(t *testing.T) { dec := Decision{ Intent: IntentAct, Slots: Slots{ Fn: "restart", Args: []string{"nginx"}, HasFn: true, ResolvedBy: ActionResolutionGrammarMatcher, }, Producer: RouteProducerGrammar, } sel := SelectCapability(dec, nil) if !sel.Resolved { t.Fatal("expected resolved") } if sel.Method != ActionResolutionGrammarMatcher { t.Errorf("Method = %q, want grammar_matcher", sel.Method) } if sel.InputKind != SelectionDeterministic { t.Errorf("InputKind = %q, want deterministic", sel.InputKind) } } // TestSelectCapability_LLMTextMatch pins that when the raw utterance did not // match but LLM cleaned text does, the selector resolves from LLM text. func TestSelectCapability_LLMTextMatch(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart", "stop"}} dec := Decision{ Intent: IntentAct, Utterance: "maven could you restart nginx", Slots: Slots{ Text: "restart nginx", }, Producer: RouteProducerLLM, } sel := SelectCapability(dec, m) if !sel.Resolved { t.Fatal("expected resolved from LLM text") } if sel.Fn != "restart" { t.Errorf("Fn = %q, want restart", sel.Fn) } if len(sel.Args) != 1 || sel.Args[0] != "nginx" { t.Errorf("Args = %v, want [nginx]", sel.Args) } if sel.Method != ActionResolutionExtractorLLMText { t.Errorf("Method = %q, want extractor_llm_text", sel.Method) } if sel.InputKind != SelectionLLMText { t.Errorf("InputKind = %q, want llm_text", sel.InputKind) } } // TestSelectCapability_LLMTextSameAsUtterance pins that when Slots.Text equals // the utterance, the selector does NOT try LLM text (no second attempt). func TestSelectCapability_LLMTextSameAsUtterance(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart"}} dec := Decision{ Intent: IntentAct, Utterance: "restart nginx", Slots: Slots{Text: "restart nginx"}, Producer: RouteProducerClassifier, } sel := SelectCapability(dec, m) // Text == Utterance means no LLM cleaned text; raw match should have // been done by the extractor. Since HasFn is false, selector sees no // LLM text to try. if sel.Resolved { t.Fatal("expected unresolved when Text == Utterance and no HasFn") } } // TestSelectCapability_Unresolved pins that when neither the grammar/extractor // nor the LLM text matches, the selection is unresolved. func TestSelectCapability_Unresolved(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart", "stop"}} dec := Decision{ Intent: IntentAct, Utterance: "deploy the thing", Slots: Slots{Text: "deploy the thing"}, Producer: RouteProducerClassifier, } sel := SelectCapability(dec, m) if sel.Resolved { t.Fatal("expected unresolved") } if sel.Fn != "" { t.Errorf("Fn = %q, want empty", sel.Fn) } } // TestSelectCapability_NonActIntent pins that a non-act intent returns an // empty selection. func TestSelectCapability_NonActIntent(t *testing.T) { dec := Decision{ Intent: IntentFact, Slots: Slots{Key: "water", HasKey: true}, } sel := SelectCapability(dec, nil) if sel.Resolved { t.Fatal("expected unresolved for non-act") } } // TestSelectCapability_NilMatcher pins that a nil matcher does not panic // and produces an unresolved selection when no grammar matched. func TestSelectCapability_NilMatcher(t *testing.T) { dec := Decision{ Intent: IntentAct, Utterance: "restart nginx", Slots: Slots{Text: "restart nginx"}, Producer: RouteProducerClassifier, } sel := SelectCapability(dec, nil) if sel.Resolved { t.Fatal("expected unresolved with nil matcher") } } // --- applyCapabilityToSlots tests --- // TestApplyCapabilityToSlots_PopulatesCompatibilityFields pins that the // compatibility fields on Decision.Slots are populated from the selection. func TestApplyCapabilityToSlots_PopulatesCompatibilityFields(t *testing.T) { dec := Decision{} sel := CapabilitySelection{ Fn: "restart", Args: []string{"nginx"}, Resolved: true, Method: ActionResolutionExtractorRaw, InputKind: SelectionDeterministic, Producer: RouteProducerClassifier, Confidence: 0.85, } applyCapabilityToSlots(&dec, sel) if dec.CapabilitySelection.Fn != "restart" { t.Errorf("CapabilitySelection.Fn = %q, want restart", dec.CapabilitySelection.Fn) } if !dec.Slots.HasFn { t.Error("Slots.HasFn should be true") } if dec.Slots.Fn != "restart" { t.Errorf("Slots.Fn = %q, want restart", dec.Slots.Fn) } if len(dec.Slots.Args) != 1 || dec.Slots.Args[0] != "nginx" { t.Errorf("Slots.Args = %v, want [nginx]", dec.Slots.Args) } if dec.Slots.ResolvedBy != ActionResolutionExtractorRaw { t.Errorf("Slots.ResolvedBy = %q, want extractor_raw", dec.Slots.ResolvedBy) } } // TestApplyCapabilityToSlots_UnresolvedDoesNotSetSlots pins that an unresolved // selection does not populate the compatibility fields. func TestApplyCapabilityToSlots_UnresolvedDoesNotSetSlots(t *testing.T) { dec := Decision{Slots: Slots{Fn: "old", HasFn: true}} sel := CapabilitySelection{ Resolved: false, Producer: RouteProducerClassifier, } applyCapabilityToSlots(&dec, sel) if dec.CapabilitySelection.Resolved { t.Error("CapabilitySelection.Resolved should be false") } // Compatibility fields should remain unchanged. if dec.Slots.Fn != "old" { t.Errorf("Slots.Fn = %q, want old (unchanged)", dec.Slots.Fn) } if !dec.Slots.HasFn { t.Error("Slots.HasFn should still be true") } } // --- ResolveActionCandidate from CapabilitySelection tests --- // TestResolveActionCandidate_CapabilitySelectionSource pins that a resolved // CapabilitySelection produces a route-sourced candidate. func TestResolveActionCandidate_CapabilitySelectionSource(t *testing.T) { dec := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: "restart", Args: []string{"nginx"}, Resolved: true, Method: ActionResolutionExtractorRaw, }, Producer: RouteProducerClassifier, } c := ResolveActionCandidate(dec, nil) if !c.ActionResolved() { t.Fatal("expected resolved candidate") } if c.Fn != "restart" { t.Errorf("Fn = %q, want restart", c.Fn) } if c.Source != ActionSourceRoute { t.Errorf("Source = %q, want route", c.Source) } if c.ResolvedBy != ActionResolutionExtractorRaw { t.Errorf("ResolvedBy = %q, want extractor_raw", c.ResolvedBy) } } // TestResolveActionCandidate_CapabilitySelectionOverSlots pins that // CapabilitySelection takes precedence over Slots.HasFn when both are set. func TestResolveActionCandidate_CapabilitySelectionOverSlots(t *testing.T) { dec := Decision{ Intent: IntentAct, Slots: Slots{ Fn: "old_fn", HasFn: true, ResolvedBy: ActionResolutionExtractorRaw, }, CapabilitySelection: CapabilitySelection{ Fn: "new_fn", Resolved: true, Method: ActionResolutionExtractorLLMText, }, } c := ResolveActionCandidate(dec, nil) if c.Fn != "new_fn" { t.Errorf("Fn = %q, want new_fn (CapabilitySelection wins)", c.Fn) } if c.ResolvedBy != ActionResolutionExtractorLLMText { t.Errorf("ResolvedBy = %q, want extractor_llm_text", c.ResolvedBy) } } // TestResolveActionCandidate_BackwardCompatSlotsHasFn pins that decisions // with Slots.HasFn but no CapabilitySelection still work (backward compat). func TestResolveActionCandidate_BackwardCompatSlotsHasFn(t *testing.T) { dec := Decision{ Intent: IntentAct, Slots: Slots{ Fn: "restart", Args: []string{"nginx"}, HasFn: true, ResolvedBy: ActionResolutionGrammarFixed, }, } c := ResolveActionCandidate(dec, nil) if !c.ActionResolved() { t.Fatal("expected resolved candidate from backward compat") } if c.Fn != "restart" { t.Errorf("Fn = %q, want restart", c.Fn) } if c.ResolvedBy != ActionResolutionGrammarFixed { t.Errorf("ResolvedBy = %q, want grammar_fixed", c.ResolvedBy) } } // --- Router integration: CapabilitySelection populated by Route --- // TestRouterRoute_CapabilitySelectionPopulated pins that Router.Route sets // CapabilitySelection on the returned Decision for each cascade path. func TestRouterRoute_CapabilitySelectionPopulated(t *testing.T) { r := newTestRouter(t, 0.3) // Stage-0 grammar path: wakeword-act. d, err := r.Route(t.Context(), NormalizedInput{Text: "maven restart nginx"}, refNow()) if err != nil { t.Fatal(err) } if !d.CapabilitySelection.Resolved { t.Error("stage-0: CapabilitySelection not resolved") } if d.CapabilitySelection.Fn != "restart" { t.Errorf("stage-0: Fn = %q, want restart", d.CapabilitySelection.Fn) } if d.CapabilitySelection.InputKind != SelectionDeterministic { t.Errorf("stage-0: InputKind = %q, want deterministic", d.CapabilitySelection.InputKind) } // Classifier path: raw utterance matches allowlist. d, err = r.Route(t.Context(), NormalizedInput{Text: "restart nginx"}, refNow()) if err != nil { t.Fatal(err) } if d.Intent != IntentAct { t.Skipf("classifier routed to %q, not act", d.Intent) } if !d.CapabilitySelection.Resolved { t.Error("classifier: CapabilitySelection not resolved") } if d.CapabilitySelection.Fn != "restart" { t.Errorf("classifier: Fn = %q, want restart", d.CapabilitySelection.Fn) } } // TestRouterRoute_ClassifierUnresolvedAct pins that an act intent where the // raw utterance does not match the allowlist has an unresolved selection. func TestRouterRoute_ClassifierUnresolvedAct(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart", "stop"}} emb := NewHashEmbedder(1024) c := NewClassifier(emb) seedClassifier(t, c) ex := Extractor{Acts: m} r := New(Config{ Classifier: c, Extractor: ex, Threshold: 0.3, }) d, err := r.Route(t.Context(), NormalizedInput{Text: "deploy the thing"}, refNow()) if err != nil { t.Fatal(err) } if d.Intent != IntentAct { t.Skipf("classifier routed to %q, not act", d.Intent) } if d.CapabilitySelection.Resolved { t.Error("expected unresolved selection for non-matching utterance") } } // --- SelectionInputKind constants --- // TestSelectionInputKindConstants pins that the three input kind constants // are distinct and non-empty. func TestSelectionInputKindConstants(t *testing.T) { kinds := []SelectionInputKind{SelectionRaw, SelectionLLMText, SelectionDeterministic} seen := make(map[SelectionInputKind]bool) for _, k := range kinds { if k == "" { t.Error("SelectionInputKind constant is empty") } if seen[k] { t.Errorf("SelectionInputKind %q appears twice", k) } seen[k] = true } }