package router import ( "testing" ) // TestResolveActionCandidate_RouteSource pins that an act with HasFn=true // produces a candidate from the route, not the matcher. func TestResolveActionCandidate_RouteSource(t *testing.T) { dec := Decision{ Intent: IntentAct, Slots: Slots{Fn: "restart", Args: []string{"nginx"}, HasFn: true}, } 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 len(c.Args) != 1 || c.Args[0] != "nginx" { t.Errorf("Args = %v, want [nginx]", c.Args) } if c.Source != ActionSourceRoute { t.Errorf("Source = %q, want route", c.Source) } } // TestResolveActionCandidate_MatcherSource pins that an act without Fn // invokes the matcher and produces a candidate from it. func TestResolveActionCandidate_MatcherSource(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart", "stop"}} dec := Decision{ Intent: IntentAct, Slots: Slots{Text: "restart nginx"}, } c := ResolveActionCandidate(dec, m) if !c.ActionResolved() { t.Fatal("expected resolved candidate") } if c.Fn != "restart" { t.Errorf("Fn = %q, want restart", c.Fn) } if c.Source != ActionSourceMatcher { t.Errorf("Source = %q, want matcher", c.Source) } } // TestResolveActionCandidate_MatcherMiss pins that a matcher miss produces // an unresolved candidate. func TestResolveActionCandidate_MatcherMiss(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart", "stop"}} dec := Decision{ Intent: IntentAct, Slots: Slots{Text: "deploy the thing"}, } c := ResolveActionCandidate(dec, m) if c.ActionResolved() { t.Fatal("expected unresolved candidate") } if c.Fn != "" { t.Errorf("Fn = %q, want empty", c.Fn) } if c.Source != "" { t.Errorf("Source = %q, want empty", c.Source) } } // TestResolveActionCandidate_NonAct pins that a non-act decision produces // an empty candidate. func TestResolveActionCandidate_NonAct(t *testing.T) { dec := Decision{ Intent: IntentFact, Slots: Slots{Key: "water", Value: "drank", HasKey: true}, } c := ResolveActionCandidate(dec, nil) if c.ActionResolved() { t.Fatal("expected unresolved candidate for non-act") } } // TestResolveActionCandidate_Stage0Match pins that a stage-0 act (which // sets HasFn=true) produces a route-sourced candidate. func TestResolveActionCandidate_Stage0Match(t *testing.T) { dec := Decision{ Intent: IntentAct, Stage: 0, Confidence: 1.0, Slots: Slots{Fn: "restart", Args: []string{"nginx"}, HasFn: true}, Producer: RouteProducerGrammar, } c := ResolveActionCandidate(dec, nil) if !c.ActionResolved() { t.Fatal("expected resolved candidate") } if c.Source != ActionSourceRoute { t.Errorf("Source = %q, want route", c.Source) } if c.Producer != RouteProducerGrammar { t.Errorf("Producer = %q, want grammar", c.Producer) } if c.Confidence != 1.0 { t.Errorf("Confidence = %f, want 1.0", c.Confidence) } } // TestResolveActionCandidate_LearnedRouterNoFn pins that a learned-router // act without Fn falls through to the matcher. func TestResolveActionCandidate_LearnedRouterNoFn(t *testing.T) { m := DefaultActMatcher{Fns: []string{"restart", "stop"}} dec := Decision{ Intent: IntentAct, Stage: 1, Confidence: 0.85, Slots: Slots{Text: "restart the server"}, Producer: RouteProducerLLM, } c := ResolveActionCandidate(dec, m) if !c.ActionResolved() { t.Fatal("expected resolved candidate from matcher fallback") } if c.Fn != "restart" { t.Errorf("Fn = %q, want restart", c.Fn) } if c.Source != ActionSourceMatcher { t.Errorf("Source = %q, want matcher", c.Source) } } // TestResolveActionCandidate_AliasMatch pins that aliases resolve through // the matcher path. func TestResolveActionCandidate_AliasMatch(t *testing.T) { m := DefaultActMatcher{ Fns: []string{"restart"}, Aliases: map[string][]string{"restart": {"перезагрузи"}}, } dec := Decision{ Intent: IntentAct, Slots: Slots{Text: "перезагрузи роутер"}, } c := ResolveActionCandidate(dec, m) if !c.ActionResolved() { t.Fatal("expected resolved candidate from alias match") } if c.Fn != "restart" { t.Errorf("Fn = %q, want restart", c.Fn) } if len(c.Args) != 1 || c.Args[0] != "роутер" { t.Errorf("Args = %v, want [роутер]", c.Args) } } // --- ValidateActionCandidate tests --- // TestValidateActionCandidate_UnresolvedEmptyFn pins that an empty Fn // produces an unresolved result (not invalid). func TestValidateActionCandidate_UnresolvedEmptyFn(t *testing.T) { c := ActionCandidate{} v := ValidateActionCandidate(c) if !v.Unresolved { t.Error("expected unresolved for empty Fn") } if v.Valid { t.Error("unresolved must not be valid") } } // TestValidateActionCandidate_UnresolvedMatcherMiss pins that a matcher-miss // candidate (Fn empty, source empty) is unresolved. func TestValidateActionCandidate_UnresolvedMatcherMiss(t *testing.T) { c := ActionCandidate{ Producer: RouteProducerLLM, Confidence: 0.5, } v := ValidateActionCandidate(c) if !v.Unresolved { t.Error("expected unresolved for matcher miss") } } // TestValidateActionCandidate_ValidRoute pins that a route-resolved candidate // with a non-empty Fn is valid. func TestValidateActionCandidate_ValidRoute(t *testing.T) { c := ActionCandidate{ Fn: "restart", Args: []string{"nginx"}, Source: ActionSourceRoute, } v := ValidateActionCandidate(c) if v.Unresolved { t.Error("expected resolved, not unresolved") } if !v.Valid { t.Errorf("expected valid, got issues: %v", v.Issues) } } // TestValidateActionCandidate_ValidMatcher pins that a matcher-resolved // candidate with a non-empty Fn is valid. func TestValidateActionCandidate_ValidMatcher(t *testing.T) { c := ActionCandidate{ Fn: "status", Source: ActionSourceMatcher, } v := ValidateActionCandidate(c) if v.Unresolved { t.Error("expected resolved, not unresolved") } if !v.Valid { t.Errorf("expected valid, got issues: %v", v.Issues) } } // TestValidateActionCandidate_ValidNoArgs pins that a zero-arg tool is valid. func TestValidateActionCandidate_ValidNoArgs(t *testing.T) { c := ActionCandidate{ Fn: "status", Source: ActionSourceRoute, } v := ValidateActionCandidate(c) if v.Unresolved { t.Error("expected resolved, not unresolved") } if !v.Valid { t.Errorf("expected valid, got issues: %v", v.Issues) } } // TestValidateActionCandidate_BlankFn pins that a whitespace-only Fn is // structurally invalid (not unresolved). func TestValidateActionCandidate_BlankFn(t *testing.T) { c := ActionCandidate{ Fn: " ", Source: ActionSourceRoute, } v := ValidateActionCandidate(c) if v.Unresolved { t.Error("blank Fn should be invalid, not unresolved") } if v.Valid { t.Error("blank Fn should be invalid") } if len(v.Issues) != 1 { t.Fatalf("expected 1 issue, got %d", len(v.Issues)) } if v.Issues[0].Field != FieldFn { t.Errorf("issue field = %q, want fn", v.Issues[0].Field) } }