Files
Maven/internal/router/acttarget_test.go
T
claude 0a2e194e76 router: migrate refusesCommand and ActHasEntityTarget to CapabilitySelection (slice 6c)
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.
2026-09-06 21:43:14 +04:00

222 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}