0a2e194e76
Migrate the two remaining action-routing consumers from compatibility Decision.Slings fields to authoritative Decision.CapabilitySelection: - refusesCommand: reads CapabilitySelection.Fn instead of Slots.Fn - ActHasEntityTarget: reads CapabilitySelection.Resolved and CapabilitySelection.Args instead of Slots.HasFn and Slots.Args Slots.Text remains the source for entity text when positional args do not contain the target (unchanged). Regression tests prove: - prohibited sentinel preserved byte-for-byte through SelectCapability - blanked Slots.Fn/Args/HasFn do not affect migrated consumers - Praxis/Hexis entity-target routing unchanged - stage-0 deterministic act unchanged - classifier/extractor act unchanged do not remove the compatibility mirrors yet.
222 lines
6.9 KiB
Go
222 lines
6.9 KiB
Go
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")
|
||
}
|
||
}
|