router: add provenance tests for ActionResolutionMethod
Pin each path: grammar_fixed, grammar_matcher, extractor_raw, extractor_llm_text, fallback_matcher. Verify unresolved has empty ResolvedBy. Verify fn/args remain byte-for-byte identical.
This commit is contained in:
@@ -352,3 +352,181 @@ func TestValidateActionCandidate_IssuesPopulatedOnInvalid(t *testing.T) {
|
||||
t.Error("invalid result has no issues")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ResolutionMethod provenance tests ---
|
||||
|
||||
// TestResolveActionCandidate_GrammarFixed pins that a stage-0 grammar that
|
||||
// hardcodes fn (praxis/task-status) carries grammar_fixed provenance.
|
||||
func TestResolveActionCandidate_GrammarFixed(t *testing.T) {
|
||||
dec := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{
|
||||
Fn: "resolve_item", HasFn: true,
|
||||
ResolvedBy: ActionResolutionGrammarFixed,
|
||||
},
|
||||
}
|
||||
c := ResolveActionCandidate(dec, nil)
|
||||
if !c.ActionResolved() {
|
||||
t.Fatal("expected resolved candidate")
|
||||
}
|
||||
if c.ResolvedBy != ActionResolutionGrammarFixed {
|
||||
t.Errorf("ResolvedBy = %q, want grammar_fixed", c.ResolvedBy)
|
||||
}
|
||||
if c.Fn != "resolve_item" {
|
||||
t.Errorf("Fn = %q, want resolve_item", c.Fn)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_GrammarMatcher pins that a stage-0 grammar that
|
||||
// invokes the ActMatcher (wakeword-act) carries grammar_matcher provenance.
|
||||
func TestResolveActionCandidate_GrammarMatcher(t *testing.T) {
|
||||
dec := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{
|
||||
Fn: "restart", Args: []string{"nginx"}, HasFn: true,
|
||||
ResolvedBy: ActionResolutionGrammarMatcher,
|
||||
},
|
||||
}
|
||||
c := ResolveActionCandidate(dec, nil)
|
||||
if !c.ActionResolved() {
|
||||
t.Fatal("expected resolved candidate")
|
||||
}
|
||||
if c.ResolvedBy != ActionResolutionGrammarMatcher {
|
||||
t.Errorf("ResolvedBy = %q, want grammar_matcher", c.ResolvedBy)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_ExtractorRaw pins that a classifier/heads-routed
|
||||
// act whose extractor matched carries extractor_raw provenance.
|
||||
func TestResolveActionCandidate_ExtractorRaw(t *testing.T) {
|
||||
dec := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{
|
||||
Fn: "restart", HasFn: true,
|
||||
ResolvedBy: ActionResolutionExtractorRaw,
|
||||
},
|
||||
Producer: RouteProducerClassifier,
|
||||
}
|
||||
c := ResolveActionCandidate(dec, nil)
|
||||
if !c.ActionResolved() {
|
||||
t.Fatal("expected resolved candidate")
|
||||
}
|
||||
if c.ResolvedBy != ActionResolutionExtractorRaw {
|
||||
t.Errorf("ResolvedBy = %q, want extractor_raw", c.ResolvedBy)
|
||||
}
|
||||
if c.Producer != RouteProducerClassifier {
|
||||
t.Errorf("Producer = %q, want classifier", c.Producer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_ExtractorLLMText pins that an LLM-routed act
|
||||
// whose function was resolved from the LLM's cleaned text carries
|
||||
// extractor_llm_text provenance.
|
||||
func TestResolveActionCandidate_ExtractorLLMText(t *testing.T) {
|
||||
dec := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{
|
||||
Fn: "restart", HasFn: true,
|
||||
ResolvedBy: ActionResolutionExtractorLLMText,
|
||||
},
|
||||
Producer: RouteProducerLLM,
|
||||
}
|
||||
c := ResolveActionCandidate(dec, nil)
|
||||
if !c.ActionResolved() {
|
||||
t.Fatal("expected resolved candidate")
|
||||
}
|
||||
if c.ResolvedBy != ActionResolutionExtractorLLMText {
|
||||
t.Errorf("ResolvedBy = %q, want extractor_llm_text", c.ResolvedBy)
|
||||
}
|
||||
if c.Producer != RouteProducerLLM {
|
||||
t.Errorf("Producer = %q, want llm", c.Producer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_FallbackMatcher pins that a matcher fallback
|
||||
// carry fallback_matcher provenance.
|
||||
func TestResolveActionCandidate_FallbackMatcher(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.ResolvedBy != ActionResolutionFallbackMatcher {
|
||||
t.Errorf("ResolvedBy = %q, want fallback_matcher", c.ResolvedBy)
|
||||
}
|
||||
if c.Source != ActionSourceMatcher {
|
||||
t.Errorf("Source = %q, want matcher", c.Source)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_UnresolvedNoFalseMethod pins that an unresolved
|
||||
// candidate (matcher miss) has empty ResolvedBy.
|
||||
func TestResolveActionCandidate_UnresolvedNoFalseMethod(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.ResolvedBy != "" {
|
||||
t.Errorf("ResolvedBy = %q, want empty for unresolved", c.ResolvedBy)
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_PropagatesResolvedBy pins that ResolvedBy
|
||||
// travels from Slots through to ActionCandidate for every route-sourced case.
|
||||
func TestResolveActionCandidate_PropagatesResolvedBy(t *testing.T) {
|
||||
methods := []ActionResolutionMethod{
|
||||
ActionResolutionGrammarFixed,
|
||||
ActionResolutionGrammarMatcher,
|
||||
ActionResolutionExtractorRaw,
|
||||
ActionResolutionExtractorLLMText,
|
||||
}
|
||||
for _, m := range methods {
|
||||
t.Run(string(m), func(t *testing.T) {
|
||||
dec := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{Fn: "restart", HasFn: true, ResolvedBy: m},
|
||||
}
|
||||
c := ResolveActionCandidate(dec, nil)
|
||||
if c.ResolvedBy != m {
|
||||
t.Errorf("ResolvedBy = %q, want %q", c.ResolvedBy, m)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveActionCandidate_FnArgsIdentical pins that adding ResolvedBy
|
||||
// does not change the selected fn or args for any path.
|
||||
func TestResolveActionCandidate_FnArgsIdentical(t *testing.T) {
|
||||
// Route-sourced.
|
||||
dec := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{
|
||||
Fn: "restart", Args: []string{"nginx"}, HasFn: true,
|
||||
ResolvedBy: ActionResolutionGrammarMatcher,
|
||||
},
|
||||
}
|
||||
c := ResolveActionCandidate(dec, nil)
|
||||
if c.Fn != "restart" || len(c.Args) != 1 || c.Args[0] != "nginx" {
|
||||
t.Errorf("route fn/args changed: Fn=%q Args=%v", c.Fn, c.Args)
|
||||
}
|
||||
|
||||
// Matcher-sourced.
|
||||
m := DefaultActMatcher{Fns: []string{"restart"}}
|
||||
dec2 := Decision{
|
||||
Intent: IntentAct,
|
||||
Slots: Slots{Text: "restart nginx"},
|
||||
}
|
||||
c2 := ResolveActionCandidate(dec2, m)
|
||||
if c2.Fn != "restart" || len(c2.Args) != 1 || c2.Args[0] != "nginx" {
|
||||
t.Errorf("matcher fn/args changed: Fn=%q Args=%v", c2.Fn, c2.Args)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user