返回 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 TestRouteDoesNotTreatTextMentionAsInvocation(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 for _, text := range []string{"/audit 检查一下", "then run /audit", "请使用 audit skill 检查一下"} {
37 decision := Route(text, entries)
38 if len(decision.Candidates) != 0 {
39 t.Errorf("%q must not create a structural skill invocation: %+v", text, decision.Candidates)
40 }
41 }
42 }
43
44 func TestRouteRespectsSkillAutoUseMetadata(t *testing.T) {
45 entries := SkillEntries([]skill.Skill{
46 {
47 Name: "quiet",
48 Description: "quiet skill",
49 Scope: skill.ScopeProject,
50 Triggers: []string{"inspect"},
51 AutoUse: "off",
52 },
53 {
54 Name: "gentle",
55 Description: "gentle skill",
56 Scope: skill.ScopeProject,
57 Triggers: []string{"inspect"},
58 AutoUse: "suggest",
59 },
60 }, []tool.ContractEntry{{Name: "run_skill"}})
61
62 decision := Route("please inspect this", entries)
63 if len(decision.Candidates) != 1 {
64 t.Fatalf("candidates = %+v, want exactly the suggest skill", decision.Candidates)
65 }
66 if got := decision.Candidates[0]; got.Entry.ID != "skill:gentle" || got.Policy != AutoUseSuggest {
67 t.Fatalf("candidate = %+v, want gentle/suggest", got)
68 }
69 }
70
71 func TestRouteKeepsAllStrongCandidatesBeforeSuggestBudget(t *testing.T) {
72 entries := make([]Entry, 0, 8)
73 for i := range 6 {
74 entries = append(entries, Entry{ID: fmt.Sprintf("skill:required-%d", i), Kind: KindSkill, Name: fmt.Sprintf("required-%d", i), AutoUse: AutoUsePrefer, Triggers: []string{"ship"}})
75 }
76 entries = append(entries,
77 Entry{ID: "skill:suggest-a", Kind: KindSkill, Name: "suggest-a", AutoUse: AutoUseSuggest, Triggers: []string{"ship"}},
78 Entry{ID: "skill:suggest-b", Kind: KindSkill, Name: "suggest-b", AutoUse: AutoUseSuggest, Triggers: []string{"ship"}},
79 )
80
81 decision := Route("ship this", entries)
82 if len(decision.Candidates) != 6 {
83 t.Fatalf("candidates = %d, want all 6 strong candidates", len(decision.Candidates))
84 }
85 for _, candidate := range decision.Candidates {
86 if candidate.Policy != AutoUsePrefer {
87 t.Fatalf("suggest candidate displaced a strong candidate: %+v", candidate)
88 }
89 }
90 }
91
92 func TestRouteClosedLoopPromotesMatchedBuiltinSkills(t *testing.T) {
93 entries := []Entry{
94 {ID: "skill:explore", Kind: KindSkill, Name: "explore", Source: string(skill.ScopeBuiltin), AutoUse: AutoUseSuggest, Triggers: []string{"调用链"}},
95 {ID: "skill:custom", Kind: KindSkill, Name: "custom", Source: string(skill.ScopeProject), AutoUse: AutoUseSuggest, Triggers: []string{"调用链"}},
96 }
97 decision := RouteClosedLoop("分析调用链", entries)
98 if !decision.ClosedLoop || len(decision.Candidates) != 2 {
99 t.Fatalf("delivery decision = %+v", decision)
100 }
101 if decision.Candidates[0].Entry.ID != "skill:explore" || decision.Candidates[0].Policy != AutoUsePrefer {
102 t.Fatalf("built-in candidate was not promoted: %+v", decision.Candidates)
103 }
104 if decision.Candidates[1].Entry.ID != "skill:custom" || decision.Candidates[1].Policy != AutoUseSuggest {
105 t.Fatalf("custom authored policy changed: %+v", decision.Candidates)
106 }
107 }
108
109 // Mentioning a vendor or asking for fresh data names no tool: a prefer tells
110 // the model to use it unless clearly unnecessary, so a server exposing forty
111 // tools must not put forty of them in front of every GitHub question.
112 func TestRouteDoesNotFloodOneMCPServer(t *testing.T) {
113 var contracts []tool.ContractEntry
114 for i := range 40 {
115 contracts = append(contracts, tool.ContractEntry{Name: fmt.Sprintf("mcp__github__tool_%d", i), ReadOnly: true})
116 }
117 contracts = append(contracts,
118 tool.ContractEntry{Name: "mcp__docs__search_pages", ReadOnly: true},
119 tool.ContractEntry{Name: "mcp__web__fetch_url", ReadOnly: true},
120 tool.ContractEntry{Name: "mcp__notes__read_note", ReadOnly: true},
121 )
122 entries := ToolEntries(contracts)
123
124 for _, input := range []string{
125 "can you look at this github issue and the latest pr comments",
126 "查一下 GitHub issue 里有没有相关反馈",
127 "use the github mcp to check the recent issues",
128 } {
129 if decision := Route(input, entries); len(decision.Candidates) != 0 {
130 t.Fatalf("Route(%q) nominated %d tools nobody named, first %s", input, len(decision.Candidates), decision.Candidates[0].Entry.ID)
131 }
132 }
133
134 decision := Route("call mcp__github__tool_7 on this issue", entries)
135 if len(decision.Candidates) != 1 {
136 t.Fatalf("named tool routed %d candidates, want 1: %+v", len(decision.Candidates), decision.Candidates)
137 }
138 if got := decision.Candidates[0]; got.Entry.ID != "mcp-tool:github/tool_7" || got.Policy != AutoUsePrefer {
139 t.Fatalf("candidate = %+v, want github/tool_7 prefer", got)
140 }
141 }
142
143 func TestRouteDoesNotPreferFailedCachedMCPTool(t *testing.T) {
144 entries := []Entry{{
145 ID: "mcp-tool:github/search_issues",
146 Kind: KindMCPTool,
147 Name: "github/search_issues",
148 Source: "github",
149 ToolName: "mcp__github__search_issues",
150 Status: StatusFailed,
151 ConnectSource: "mcp",
152 ConnectName: "github",
153 }}
154
155 decision := Route("用 mcp__github__search_issues 查一下相关反馈", entries)
156 if len(decision.Candidates) != 0 {
157 t.Fatalf("failed cached MCP tool was still routed: %+v", decision.Candidates)
158 }
159 }
160
161 func TestRenderTransientBlockMentionsConnectSource(t *testing.T) {
162 decision := RouteDecision{Candidates: []RouteCandidate{{
163 Entry: Entry{
164 ID: "skill:review",
165 Kind: KindSkill,
166 Name: "review",
167 Status: StatusConfigured,
168 ConnectSource: "skills",
169 },
170 Policy: AutoUsePrefer,
171 Reason: "matched",
172 }}}
173
174 block := RenderTransientBlock(decision)
175 for _, want := range []string{`<capability-route version="1">`, `source:skills`, `connect_tool_source`} {
176 if !strings.Contains(block, want) {
177 t.Fatalf("block missing %q:\n%s", want, block)
178 }
179 }
180 }
181
181 lines GO