返回 DeepSeek-Reasonix
builtins_test.go
根目录 / internal / skill / builtins_test.go
1 package skill
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8
9 "reasonix/internal/tool"
10 )
11
12 type builtinTestTool struct {
13 name string
14 readOnly bool
15 }
16
17 func (t builtinTestTool) Name() string { return t.name }
18 func (t builtinTestTool) Description() string { return t.name }
19 func (t builtinTestTool) Schema() json.RawMessage {
20 return json.RawMessage(`{"type":"object"}`)
21 }
22 func (t builtinTestTool) Execute(context.Context, json.RawMessage) (string, error) {
23 return "", nil
24 }
25 func (t builtinTestTool) ReadOnly() bool { return t.readOnly }
26
27 // TestBuiltinReviewSkillsDeclareReadOnly pins the tool-boundary contract behind
28 // the review/security-review "Read-only" promise: runners select the read-only
29 // subagent registry from this flag, so losing it silently re-opens writer bash.
30 func TestBuiltinReviewSkillsDeclareReadOnly(t *testing.T) {
31 want := map[string]bool{
32 "explore": false,
33 "research": false,
34 "review": true,
35 "security-review": true,
36 }
37 for _, sk := range builtinSkills() {
38 expected, tracked := want[sk.Name]
39 if !tracked {
40 continue
41 }
42 if sk.ReadOnly != expected {
43 t.Errorf("builtin %q ReadOnly = %v, want %v", sk.Name, sk.ReadOnly, expected)
44 }
45 }
46 }
47
48 func TestCodeGraphReadToolsRequireKnownNameAndReadOnly(t *testing.T) {
49 reg := tool.NewRegistry()
50 reg.Add(builtinTestTool{name: "mcp__codegraph__symbols", readOnly: true})
51 reg.Add(builtinTestTool{name: "codegraph_search", readOnly: true})
52 reg.Add(builtinTestTool{name: "mcp__codegraph__write_index", readOnly: false})
53 reg.Add(builtinTestTool{name: "mcp__other__codegraph_search", readOnly: true})
54
55 got := CodeGraphReadTools(reg)
56 want := []string{"codegraph_search", "mcp__codegraph__symbols"}
57 if len(got) != len(want) {
58 t.Fatalf("CodeGraphReadTools = %v, want %v", got, want)
59 }
60 for i := range want {
61 if got[i] != want[i] {
62 t.Fatalf("CodeGraphReadTools = %v, want %v", got, want)
63 }
64 }
65 }
66
67 func TestBuiltinSkillsIncludeCodeGraphHintAndToolsWhenDiscovered(t *testing.T) {
68 reg := tool.NewRegistry()
69 reg.Add(builtinTestTool{name: "mcp__codegraph__symbols", readOnly: true})
70
71 var explore Skill
72 for _, sk := range builtinSkills() {
73 if sk.Name == "explore" {
74 explore = sk
75 break
76 }
77 }
78 if explore.Name == "" {
79 t.Fatal("explore skill not found")
80 }
81 if strings.Contains(explore.Body, "Optional installed code graph MCP tools") {
82 t.Fatalf("base explore body should not include session-specific codegraph hint:\n%s", explore.Body)
83 }
84 for _, name := range explore.AllowedTools {
85 if name == "mcp__codegraph__symbols" {
86 t.Fatalf("base explore allowed tools = %v, should not include session-specific codegraph tool", explore.AllowedTools)
87 }
88 }
89
90 explore = WithCodeGraphTools(explore, CodeGraphReadTools(reg))
91 if !strings.Contains(explore.Body, "Optional installed code graph MCP tools") {
92 t.Fatalf("explore body missing optional codegraph hint:\n%s", explore.Body)
93 }
94 for _, want := range []string{
95 "use LSP for language semantics",
96 "use code graph tools first for call graph, impact analysis, and architecture relationships",
97 "use code_index only as the built-in outline/definition-candidate fallback",
98 } {
99 if !strings.Contains(explore.Body, want) {
100 t.Fatalf("explore body missing priority hint %q:\n%s", want, explore.Body)
101 }
102 }
103 found := false
104 for _, name := range explore.AllowedTools {
105 if name == "mcp__codegraph__symbols" {
106 found = true
107 break
108 }
109 }
110 if !found {
111 t.Fatalf("explore allowed tools = %v, want codegraph tool", explore.AllowedTools)
112 }
113 }
114
115 func TestWithCodeGraphToolsOnlyTouchesCodeReadingBuiltins(t *testing.T) {
116 initSkill := Skill{Name: "init", Scope: ScopeBuiltin, Body: "body", AllowedTools: []string{"read_file"}}
117 got := WithCodeGraphTools(initSkill, []string{"mcp__codegraph__symbols"})
118 if strings.Contains(got.Body, "Optional installed code graph MCP tools") {
119 t.Fatalf("init skill should not receive codegraph hint:\n%s", got.Body)
120 }
121 if len(got.AllowedTools) != 1 || got.AllowedTools[0] != "read_file" {
122 t.Fatalf("init allowed tools = %v, want unchanged", got.AllowedTools)
123 }
124 }
125
126 func TestWithCodeGraphToolsSkipsUserSkillOverrides(t *testing.T) {
127 sk := Skill{Name: "explore", Scope: ScopeProject, Body: "user body", AllowedTools: []string{"read_file"}}
128 got := WithCodeGraphTools(sk, []string{"mcp__codegraph__symbols"})
129 if strings.Contains(got.Body, "Optional installed code graph MCP tools") {
130 t.Fatalf("project skill override should not receive codegraph hint:\n%s", got.Body)
131 }
132 if len(got.AllowedTools) != 1 || got.AllowedTools[0] != "read_file" {
133 t.Fatalf("project skill override allowed tools = %v, want unchanged", got.AllowedTools)
134 }
135 }
136
137 func TestWithCodeGraphToolsIsIdempotent(t *testing.T) {
138 sk := Skill{Name: "explore", Scope: ScopeBuiltin, Body: "body", AllowedTools: []string{"read_file"}}
139 sk = WithCodeGraphTools(sk, []string{"mcp__codegraph__symbols"})
140 sk = WithCodeGraphTools(sk, []string{"mcp__codegraph__symbols"})
141 if got := strings.Count(sk.Body, optionalCodeGraphHint); got != 1 {
142 t.Fatalf("codegraph hint count = %d, want 1; body:\n%s", got, sk.Body)
143 }
144 count := 0
145 for _, name := range sk.AllowedTools {
146 if name == "mcp__codegraph__symbols" {
147 count++
148 }
149 }
150 if count != 1 {
151 t.Fatalf("codegraph tool count = %d, want 1; allowed=%v", count, sk.AllowedTools)
152 }
153 }
154
154 lines GO