返回 DeepSeek-Reasonix
capability_test.go
根目录 / internal / capability / capability_test.go
1 package capability
2
3 import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "reasonix/internal/skill"
9 "reasonix/internal/tool"
10 )
11
12 func TestRoutePrefersReviewSkillForReviewRequest(t *testing.T) {
13 entries := SkillEntries([]skill.Skill{{
14 Name: "review",
15 Description: "review code for bugs",
16 Scope: skill.ScopeBuiltin,
17 }}, []tool.ContractEntry{{Name: "run_skill"}})
18
19 decision := Route("帮我看看这段代码有没有问题", entries)
20 if len(decision.Candidates) == 0 {
21 t.Fatal("Route returned no candidates")
22 }
23 got := decision.Candidates[0]
24 if got.Entry.ID != "skill:review" || got.Policy != AutoUsePrefer {
25 t.Fatalf("candidate = %+v, want review/prefer", got)
26 }
27 }
28
29 func TestRouteRequiresExplicitSkill(t *testing.T) {
30 entries := SkillEntries([]skill.Skill{{
31 Name: "audit",
32 Description: "audit something",
33 Scope: skill.ScopeProject,
34 }}, []tool.ContractEntry{{Name: "run_skill"}})
35
36 decision := Route("请使用 audit skill 检查一下", entries)
37 if len(decision.Candidates) == 0 {
38 t.Fatal("Route returned no candidates")
39 }
40 if got := decision.Candidates[0].Policy; got != AutoUseRequire {
41 t.Fatalf("policy = %s, want require", got)
42 }
43 }
44
45 func TestRouteRespectsSkillAutoUseMetadata(t *testing.T) {
46 entries := SkillEntries([]skill.Skill{
47 {
48 Name: "quiet",
49 Description: "quiet skill",
50 Scope: skill.ScopeProject,
51 Triggers: []string{"inspect"},
52 AutoUse: "off",
53 },
54 {
55 Name: "gentle",
56 Description: "gentle skill",
57 Scope: skill.ScopeProject,
58 Triggers: []string{"inspect"},
59 AutoUse: "suggest",
60 },
61 }, []tool.ContractEntry{{Name: "run_skill"}})
62
63 decision := Route("please inspect this", entries)
64 if len(decision.Candidates) != 1 {
65 t.Fatalf("candidates = %+v, want exactly the suggest skill", decision.Candidates)
66 }
67 if got := decision.Candidates[0]; got.Entry.ID != "skill:gentle" || got.Policy != AutoUseSuggest {
68 t.Fatalf("candidate = %+v, want gentle/suggest", got)
69 }
70 }
71
72 func TestRouteKeepsAllStrongCandidatesBeforeSuggestBudget(t *testing.T) {
73 entries := make([]Entry, 0, 8)
74 for i := 0; i < 6; i++ {
75 entries = append(entries, Entry{ID: fmt.Sprintf("skill:required-%d", i), Kind: KindSkill, Name: fmt.Sprintf("required-%d", i), AutoUse: AutoUsePrefer, Triggers: []string{"ship"}})
76 }
77 entries = append(entries,
78 Entry{ID: "skill:suggest-a", Kind: KindSkill, Name: "suggest-a", AutoUse: AutoUseSuggest, Triggers: []string{"ship"}},
79 Entry{ID: "skill:suggest-b", Kind: KindSkill, Name: "suggest-b", AutoUse: AutoUseSuggest, Triggers: []string{"ship"}},
80 )
81
82 decision := Route("ship this", entries)
83 if len(decision.Candidates) != 6 {
84 t.Fatalf("candidates = %d, want all 6 strong candidates", len(decision.Candidates))
85 }
86 for _, candidate := range decision.Candidates {
87 if candidate.Policy != AutoUsePrefer {
88 t.Fatalf("suggest candidate displaced a strong candidate: %+v", candidate)
89 }
90 }
91 }
92
93 func TestRouteDeliveryPromotesMatchedBuiltinSkills(t *testing.T) {
94 entries := []Entry{
95 {ID: "skill:explore", Kind: KindSkill, Name: "explore", Source: string(skill.ScopeBuiltin), AutoUse: AutoUseSuggest, Triggers: []string{"调用链"}},
96 {ID: "skill:custom", Kind: KindSkill, Name: "custom", Source: string(skill.ScopeProject), AutoUse: AutoUseSuggest, Triggers: []string{"调用链"}},
97 }
98 decision := RouteDelivery("分析调用链", entries)
99 if !decision.Delivery || len(decision.Candidates) != 2 {
100 t.Fatalf("delivery decision = %+v", decision)
101 }
102 if decision.Candidates[0].Entry.ID != "skill:explore" || decision.Candidates[0].Policy != AutoUsePrefer {
103 t.Fatalf("built-in candidate was not promoted: %+v", decision.Candidates)
104 }
105 if decision.Candidates[1].Entry.ID != "skill:custom" || decision.Candidates[1].Policy != AutoUseSuggest {
106 t.Fatalf("custom authored policy changed: %+v", decision.Candidates)
107 }
108 }
109
110 func TestRoutePrefersGitHubMCPForIssueLookup(t *testing.T) {
111 entries := ToolEntries([]tool.ContractEntry{{
112 Name: "mcp__github__search_issues",
113 Description: "search GitHub issues",
114 ReadOnly: true,
115 }})
116
117 decision := Route("查一下 GitHub issue 里有没有相关反馈", entries)
118 if len(decision.Candidates) == 0 {
119 t.Fatal("Route returned no candidates")
120 }
121 got := decision.Candidates[0]
122 if got.Entry.ID != "mcp-tool:github/search_issues" || got.Policy != AutoUsePrefer {
123 t.Fatalf("candidate = %+v, want github mcp/prefer", got)
124 }
125 }
126
127 func TestRenderTransientBlockMentionsConnectSource(t *testing.T) {
128 decision := RouteDecision{Candidates: []RouteCandidate{{
129 Entry: Entry{
130 ID: "skill:review",
131 Kind: KindSkill,
132 Name: "review",
133 Status: StatusConfigured,
134 ConnectSource: "skills",
135 },
136 Policy: AutoUsePrefer,
137 Reason: "matched",
138 }}}
139
140 block := RenderTransientBlock(decision)
141 for _, want := range []string{`<capability-route version="1">`, `source:skills`, `connect_tool_source`} {
142 if !strings.Contains(block, want) {
143 t.Fatalf("block missing %q:\n%s", want, block)
144 }
145 }
146 }
147
147 lines GO