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