package intent import ( "context" "testing" ) // buildCommandSnap 组装命令测试用快照:一个带必填槽位的意图与一个无槽位意图。 func buildCommandSnap(t *testing.T) *Snapshot { t.Helper() b := NewRegistryBuilder(Options{}) if err := b.Register(IntentSpec{ ID: "report.generate", Domain: "report", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"生成报告"}}, Slots: []SlotSpec{ {Key: "topic", Required: true, Pattern: `主题是(.+)`, Question: "要查询哪个主题?"}, }, Handler: "reportHandler", }); err != nil { t.Fatal(err) } if err := b.Register(IntentSpec{ ID: "report.chat", Domain: "report", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"闲聊一下"}}, }); err != nil { t.Fatal(err) } reg, err := b.Build() if err != nil { t.Fatal(err) } return reg.Snapshot() } func recognize(t *testing.T, snap *Snapshot, input string) Result { t.Helper() res, err := NewRecognizer(Options{}, nil, nil).Recognize(context.Background(), input, snap) if err != nil { t.Fatal(err) } return res } // TestCommandsMatchedFullSlots:槽位齐全 → 单条 Start,ActionRef 取 Handler。 func TestCommandsMatchedFullSlots(t *testing.T) { snap := buildCommandSnap(t) res := recognize(t, snap, "帮我生成报告,主题是电机") if res.Branch != BranchMatched { t.Fatalf("branch = %s", res.Branch) } if len(res.Commands) != 1 { t.Fatalf("commands = %+v", res.Commands) } cmd := res.Commands[0] if cmd.Kind != CommandStart || cmd.Action.Kind != ActionHandler || cmd.Action.ID != "reportHandler" { t.Fatalf("start = %+v", cmd) } if cmd.Slots["topic"] != "电机" { t.Fatalf("slots = %v", cmd.Slots) } } // TestCommandsMatchedMissingSlot:必填缺失 → Start(带已抽槽)+ Clarify(含建议提问)。 func TestCommandsMatchedMissingSlot(t *testing.T) { snap := buildCommandSnap(t) res := recognize(t, snap, "帮我生成报告") if len(res.Commands) != 2 { t.Fatalf("commands = %+v", res.Commands) } start, clarify := res.Commands[0], res.Commands[1] if start.Kind != CommandStart { t.Fatalf("first = %+v", start) } if clarify.Kind != CommandClarify || len(clarify.Missing) != 1 || clarify.Missing[0] != "topic" { t.Fatalf("clarify = %+v", clarify) } if clarify.Question != "要查询哪个主题?" { t.Fatalf("question = %q", clarify.Question) } } // TestCommandsExpectationSetSlotPlusStart:预期命中 → SetSlot + Start(验收形态)。 func TestCommandsExpectationSetSlotPlusStart(t *testing.T) { snap := buildCommandSnap(t) exp := listExpectation{ids: []string{"report.generate"}} res, err := NewRecognizer(Options{}, exp, nil).Recognize(context.Background(), "生成报告,主题是电机", snap) if err != nil { t.Fatal(err) } if res.Branch != BranchExpectationHit { t.Fatalf("branch = %s", res.Branch) } var setSlot, start *Command for i := range res.Commands { switch res.Commands[i].Kind { case CommandSetSlot: setSlot = &res.Commands[i] case CommandStart: start = &res.Commands[i] } } if setSlot == nil || start == nil { t.Fatalf("commands = %+v", res.Commands) } if setSlot.Name != "topic" || setSlot.Value != "电机" { t.Fatalf("set_slot = %+v", *setSlot) } if start.Action.Kind != ActionHandler || start.Slots["topic"] != "电机" { t.Fatalf("start = %+v", *start) } } // TestCommandsAmbiguousClarify:标歧 → 单条 Clarify 携带候选 ID。 func TestCommandsAmbiguousClarify(t *testing.T) { b := NewRegistryBuilder(Options{}) if err := b.Register(IntentSpec{ID: "demo.alpha", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"模糊词"}}}); err != nil { t.Fatal(err) } if err := b.Register(IntentSpec{ID: "demo.beta", Domain: "demo", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"模糊词"}}}); err != nil { t.Fatal(err) } reg, err := b.Build() if err != nil { t.Fatal(err) } res := recognize(t, reg.Snapshot(), "模糊词") if res.Branch != BranchAmbiguous { t.Fatalf("branch = %s", res.Branch) } if len(res.Commands) != 1 || res.Commands[0].Kind != CommandClarify { t.Fatalf("commands = %+v", res.Commands) } if len(res.Commands[0].Candidates) != 2 { t.Fatalf("candidates = %v", res.Commands[0].Candidates) } } // TestCommandsFallbackEmpty:fallback 不产出命令。 func TestCommandsFallbackEmpty(t *testing.T) { snap := buildCommandSnap(t) res := recognize(t, snap, "完全不相关的一句话") if res.Branch != BranchFallback || len(res.Commands) != 0 { t.Fatalf("fallback commands = %+v", res.Commands) } } // TestActionRefDerivation:blueprint 键优先于 Handler;都没有回落 auto。 func TestActionRefDerivation(t *testing.T) { if got := actionOf(Candidate{ID: "x", Metadata: map[string]string{"blueprint": "bp-1"}, Handler: "h"}); got.Kind != ActionBlueprint || got.ID != "bp-1" { t.Fatalf("blueprint = %+v", got) } if got := actionOf(Candidate{ID: "x", Handler: "h"}); got.Kind != ActionHandler || got.ID != "h" { t.Fatalf("handler = %+v", got) } if got := actionOf(Candidate{ID: "x"}); got.Kind != ActionAuto || got.ID != "x" { t.Fatalf("auto = %+v", got) } } type listExpectation struct{ ids []string } func (s listExpectation) Pending(ctx context.Context) *PendingExpectation { return &PendingExpectation{IntentIDs: s.ids} } // TestSpecActionDeclaredWins:spec.action 显式声明在 ActionRef 推导中胜出(WP2)。 func TestSpecActionDeclaredWins(t *testing.T) { b := NewRegistryBuilder(Options{}) if err := b.Register(IntentSpec{ ID: "report.explicit", Domain: "report", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"显式动作"}}, Action: &ActionRef{Kind: ActionWorkflow, ID: "wf-report"}, }); err != nil { t.Fatal(err) } reg, err := b.Build() if err != nil { t.Fatal(err) } res := recognize(t, reg.Snapshot(), "显式动作") if len(res.Commands) != 1 || res.Commands[0].Kind != CommandStart { t.Fatalf("commands = %+v", res.Commands) } if got := res.Commands[0].Action; got.Kind != ActionWorkflow || got.ID != "wf-report" { t.Fatalf("action = %+v", got) } } // TestSpecActionValidation:action.kind 非法与 id 为空都被拒绝。 func TestSpecActionValidation(t *testing.T) { bad := IntentSpec{ ID: "report.bad", Domain: "report", Version: 1, Matcher: MatchSpec{IncludesAny: []string{"x"}}, Action: &ActionRef{Kind: "bogus", ID: "y"}, } b := NewRegistryBuilder(Options{}) if err := b.Register(bad); err == nil { t.Fatal("bogus action kind accepted") } bad.Action = &ActionRef{Kind: ActionCommand, ID: ""} b2 := NewRegistryBuilder(Options{}) if err := b2.Register(bad); err == nil { t.Fatal("empty action id accepted") } }