package router import "testing" func TestActHasEntityTargetRequiresNamedTargetEvidence(t *testing.T) { cases := []struct { name string dec Decision want bool }{ { name: "matched function and argument", dec: Decision{Intent: IntentAct, CapabilitySelection: CapabilitySelection{Fn: "restart", Resolved: true, Args: []string{"nginx"}}, Slots: Slots{Text: "restart nginx"}, }, want: true, }, { name: "model function and target text", dec: Decision{Intent: IntentAct, CapabilitySelection: CapabilitySelection{Fn: "restart", Resolved: true}, Slots: Slots{Text: "перезапусти гитею"}, }, want: true, }, { name: "matched function alone", dec: Decision{Intent: IntentAct, CapabilitySelection: CapabilitySelection{Fn: "выключи", Resolved: true}, Slots: Slots{Text: "выключи"}, }, want: false, }, { name: "matched function with politeness only", dec: Decision{Intent: IntentAct, CapabilitySelection: CapabilitySelection{Fn: "выключи", Resolved: true, Args: []string{"пожалуйста"}}, Slots: Slots{Text: "выключи пожалуйста"}, }, want: false, }, { name: "matched function with anaphora only", dec: Decision{Intent: IntentAct, CapabilitySelection: CapabilitySelection{Fn: "выключи", Resolved: true, Args: []string{"его"}}, Slots: Slots{Text: "выключи его"}, }, want: false, }, { name: "unmatched entity act", dec: Decision{Intent: IntentAct, Slots: Slots{Text: "перезапусти muzick indexer"}}, want: true, }, { name: "unmatched verb alone", dec: Decision{Intent: IntentAct, Slots: Slots{Text: "перезапусти"}}, want: false, }, { name: "unresolved demonstrative", dec: Decision{Intent: IntentAct, Slots: Slots{Text: "сделай это"}}, want: false, }, } for _, testCase := range cases { t.Run(testCase.name, func(t *testing.T) { if got := ActHasEntityTarget(testCase.dec); got != testCase.want { t.Errorf("ActHasEntityTarget(%+v) = %v, want %v", testCase.dec, got, testCase.want) } }) } } // TestActHasEntityTarget_BlankedSlotsStillWorks proves that ActHasEntityTarget // reads CapabilitySelection.Resolved and CapabilitySelection.Args, not the // compatibility Slots.Fn/Args/HasFn. When Slots fields are blank but // CapabilitySelection is populated, behavior must remain correct. func TestActHasEntityTarget_BlankedSlotsStillWorks(t *testing.T) { // Resolved capability with entity in Args, but Slots.Fn/Args/HasFn are blank. dec := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: "restart", Resolved: true, Args: []string{"nginx"}, }, Slots: Slots{Text: "restart nginx"}, } if !ActHasEntityTarget(dec) { t.Fatal("should detect entity in CapabilitySelection.Args even with blank Slots") } // Resolved capability with entity in Text (no Args), Slots.Fn/HasFn blank. dec2 := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: "restart", Resolved: true, }, Slots: Slots{Text: "перезапусти гитею"}, } if !ActHasEntityTarget(dec2) { t.Fatal("should detect entity in Slots.Text via CapabilitySelection.Resolved branch") } // Unresolved capability with Slots.Fn set (simulating stale compat state). // Must NOT enter the resolved branch. dec3 := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Resolved: false, }, Slots: Slots{Fn: "restart", HasFn: true, Args: []string{"nginx"}, Text: "restart nginx"}, } if !ActHasEntityTarget(dec3) { t.Fatal("unresolved capability with stale Slots should still find entity via text fallback") } } // TestActHasEntityTarget_PraxisHexisRoutingUnchanged proves that the Praxis // and Hexis entity-target routing semantics are unchanged by the migration. func TestActHasEntityTarget_PraxisHexisRoutingUnchanged(t *testing.T) { // Praxis act: resolved capability with entity target. praxis := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: "entity_attention", Resolved: true, }, Slots: Slots{Text: "что с muzick"}, } if !ActHasEntityTarget(praxis) { t.Fatal("Praxis entity act should have entity target") } // Hexis act: unresolved capability, entity in text. hexis := Decision{ Intent: IntentAct, Slots: Slots{Text: "перезапусти muzick indexer"}, } if !ActHasEntityTarget(hexis) { t.Fatal("Hexis entity act should have entity target") } } // TestActHasEntityTarget_Stage0DeterministicActUnchanged proves that a // stage-0 grammar-fixed act (like command prohibition) is handled correctly. func TestActHasEntityTarget_Stage0DeterministicActUnchanged(t *testing.T) { // Prohibited act: resolved capability, no entity in Args or Text. // "вот это" — filler particle + demonstrative, no named entity. prohibited := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: ProhibitedActFn, Resolved: true, }, Slots: Slots{Text: "вот это"}, } if ActHasEntityTarget(prohibited) { t.Fatal("prohibited act with demonstrative-only tail should not have entity target") } // Prohibited act WITH entity text still reports the entity — the function // checks entity presence, not prohibition status. prohibitedWithEntity := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: ProhibitedActFn, Resolved: true, }, Slots: Slots{Text: "don't restart nginx"}, } if !ActHasEntityTarget(prohibitedWithEntity) { t.Fatal("prohibited act with entity text should still report entity target") } // Task status act: resolved capability, no entity. taskStatus := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: TaskStatusFn, Resolved: true, }, Slots: Slots{Text: "задачи"}, } if ActHasEntityTarget(taskStatus) { t.Fatal("task status act should not have entity target") } } // TestActHasEntityTarget_ClassifierExtractorActUnchanged proves that the // classifier/extractor act path is unchanged. func TestActHasEntityTarget_ClassifierExtractorActUnchanged(t *testing.T) { // Classifier resolved act with entity in Args. dec := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Fn: "restart", Resolved: true, Args: []string{"nginx"}, }, Slots: Slots{Text: "restart nginx"}, } if !ActHasEntityTarget(dec) { t.Fatal("classifier resolved act with entity in Args should have entity target") } // Classifier unresolved act: entity in text. dec2 := Decision{ Intent: IntentAct, CapabilitySelection: CapabilitySelection{ Resolved: false, }, Slots: Slots{Text: "перезапусти muzick"}, } if !ActHasEntityTarget(dec2) { t.Fatal("classifier unresolved act with entity in text should have entity target") } }