返回 DeepSeek-Reasonix
codeindex_test.go
根目录 / internal / tool / builtin / codeindex_test.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 )
11
12 func TestCodeIndexSearchGoSymbols(t *testing.T) {
13 root := t.TempDir()
14 mkfile(t, filepath.Join(root, "service.go"), `package demo
15
16 type Service struct{}
17
18 func NewService() *Service { return &Service{} }
19
20 func (s *Service) Handle() {}
21
22 const DefaultName = "demo"
23 `)
24
25 out := runTool(t, codeIndex{workDir: root}, map[string]any{
26 "action": "search",
27 "query": "Service",
28 })
29 for _, want := range []string{
30 "service.go:3: struct Service",
31 "service.go:5: func NewService",
32 "service.go:7: method Service.Handle",
33 } {
34 if !strings.Contains(out, want) {
35 t.Fatalf("code_index search missing %q; got:\n%s", want, out)
36 }
37 }
38 }
39
40 func TestCodeIndexOutlineTypeScriptAndSkipsNoiseDirs(t *testing.T) {
41 root := t.TempDir()
42 mkfile(t, filepath.Join(root, "src", "app.ts"), `export interface User {
43 name: string
44 }
45 export class Client {}
46 export const loadUser = async () => {}
47 `)
48 mkfile(t, filepath.Join(root, "node_modules", "dep", "dep.ts"), `export class Dependency {}`)
49
50 out := runTool(t, codeIndex{workDir: root}, map[string]any{
51 "action": "outline",
52 "path": ".",
53 })
54 for _, want := range []string{
55 "src/app.ts:1: interface User",
56 "src/app.ts:4: class Client",
57 "src/app.ts:5: func loadUser",
58 } {
59 if !strings.Contains(out, want) {
60 t.Fatalf("code_index outline missing %q; got:\n%s", want, out)
61 }
62 }
63 if strings.Contains(out, "Dependency") {
64 t.Fatalf("code_index should skip node_modules; got:\n%s", out)
65 }
66 }
67
68 func TestCodeIndexOutlineFiltersBeforeLimit(t *testing.T) {
69 root := t.TempDir()
70 mkfile(t, filepath.Join(root, "a.ts"), `export const first = () => {}
71 export const second = () => {}
72 `)
73 mkfile(t, filepath.Join(root, "z.ts"), `export interface Later {
74 id: string
75 }
76 `)
77
78 out := runTool(t, codeIndex{workDir: root}, map[string]any{
79 "action": "outline",
80 "kind": "interface",
81 "limit": 1,
82 })
83 if !strings.Contains(out, "z.ts:1: interface Later") {
84 t.Fatalf("code_index should find filtered symbols after early unfiltered matches; got:\n%s", out)
85 }
86 if strings.Contains(out, "first") || strings.Contains(out, "second") {
87 t.Fatalf("code_index should filter before returning limited outline; got:\n%s", out)
88 }
89 }
90
91 func TestCodeIndexKindFiltersJavaTypes(t *testing.T) {
92 root := t.TempDir()
93 mkfile(t, filepath.Join(root, "src", "Example.java"), `package demo;
94
95 public interface Repository {}
96 public enum Mode { Fast }
97 public class Client {
98 public void run() {}
99 }
100 `)
101
102 out := runTool(t, codeIndex{workDir: root}, map[string]any{
103 "action": "search",
104 "query": "Repository",
105 "kind": "interface",
106 })
107 if !strings.Contains(out, "src/Example.java:3: interface Repository") {
108 t.Fatalf("code_index should return Java interface for kind=interface; got:\n%s", out)
109 }
110
111 out = runTool(t, codeIndex{workDir: root}, map[string]any{
112 "action": "search",
113 "query": "Mode",
114 "kind": "enum",
115 })
116 if !strings.Contains(out, "src/Example.java:4: enum Mode") {
117 t.Fatalf("code_index should return Java enum for kind=enum; got:\n%s", out)
118 }
119
120 out = runTool(t, codeIndex{workDir: root}, map[string]any{
121 "action": "search",
122 "query": "Client",
123 "kind": "class",
124 })
125 if !strings.Contains(out, "src/Example.java:5: class Client") {
126 t.Fatalf("code_index should return Java class for kind=class; got:\n%s", out)
127 }
128 }
129
130 func TestCodeIndexKindFiltersRustItems(t *testing.T) {
131 root := t.TempDir()
132 mkfile(t, filepath.Join(root, "src", "lib.rs"), `pub struct Store {}
133 pub trait Repository {}
134 pub enum Mode { Fast }
135 pub fn load_store() {}
136 `)
137
138 out := runTool(t, codeIndex{workDir: root}, map[string]any{
139 "action": "search",
140 "query": "Store",
141 "kind": "struct",
142 })
143 if !strings.Contains(out, "src/lib.rs:1: struct Store") {
144 t.Fatalf("code_index should return Rust struct for kind=struct; got:\n%s", out)
145 }
146
147 out = runTool(t, codeIndex{workDir: root}, map[string]any{
148 "action": "search",
149 "query": "Repository",
150 "kind": "trait",
151 })
152 if !strings.Contains(out, "src/lib.rs:2: trait Repository") {
153 t.Fatalf("code_index should return Rust trait for kind=trait; got:\n%s", out)
154 }
155
156 out = runTool(t, codeIndex{workDir: root}, map[string]any{
157 "action": "search",
158 "query": "load_store",
159 "kind": "fn",
160 })
161 if !strings.Contains(out, "src/lib.rs:4: fn load_store") {
162 t.Fatalf("code_index should return Rust fn for kind=fn; got:\n%s", out)
163 }
164 }
165
166 func TestCodeIndexRequiresQueryForSearch(t *testing.T) {
167 _, err := codeIndex{workDir: t.TempDir()}.Execute(context.Background(), json.RawMessage(`{"action":"search"}`))
168 if err == nil || !strings.Contains(err.Error(), "query is required") {
169 t.Fatalf("error = %v, want query required", err)
170 }
171 }
172
173 func TestCodeIndexWorkspaceBinding(t *testing.T) {
174 root := t.TempDir()
175 mkfile(t, filepath.Join(root, "main.py"), "class App:\n pass\n")
176 t.Chdir(t.TempDir())
177
178 tools := byName(Workspace{Dir: root}.Tools("code_index"))
179 tl := tools["code_index"]
180 if tl == nil {
181 t.Fatal("workspace did not expose code_index")
182 }
183 out, err := tl.Execute(context.Background(), json.RawMessage(`{"action":"outline","path":"."}`))
184 if err != nil {
185 t.Fatalf("code_index: %v", err)
186 }
187 if !strings.Contains(out, "main.py:1: class App") {
188 t.Fatalf("code_index should resolve relative paths against workspace; got:\n%s", out)
189 }
190 }
191
192 func TestCodeIndexForbidRead(t *testing.T) {
193 root := t.TempDir()
194 mkfile(t, filepath.Join(root, "allowed.go"), "package demo\nfunc AllowedNeedle() {}\n")
195 secretDir := filepath.Join(root, "secret")
196 mkfile(t, filepath.Join(secretDir, "secret.go"), "package secret\nfunc SecretNeedle() {}\n")
197
198 ci := codeIndex{workDir: root, forbidRoots: realRoots([]string{secretDir})}
199 out := runTool(t, ci, map[string]any{
200 "action": "search",
201 "query": "Needle",
202 "path": ".",
203 })
204 if !strings.Contains(out, "AllowedNeedle") {
205 t.Fatalf("code_index should still return allowed symbols, got:\n%s", out)
206 }
207 if strings.Contains(out, "SecretNeedle") || strings.Contains(out, "secret.go") {
208 t.Fatalf("code_index leaked forbidden symbols:\n%s", out)
209 }
210
211 out = runTool(t, ci, map[string]any{
212 "action": "search",
213 "query": "SecretNeedle",
214 "path": "secret",
215 })
216 if out != "(no symbols)" {
217 t.Fatalf("code_index forbidden root = %q, want (no symbols)", out)
218 }
219 }
220
221 func TestCodeIndexIgnoresLargeFiles(t *testing.T) {
222 root := t.TempDir()
223 if err := os.WriteFile(filepath.Join(root, "huge.ts"), []byte(strings.Repeat("x", codeIndexMaxFileSize+1)), 0o644); err != nil {
224 t.Fatal(err)
225 }
226 out := runTool(t, codeIndex{workDir: root}, map[string]any{"action": "outline"})
227 if out != "(no symbols)" {
228 t.Fatalf("large files should be ignored, got:\n%s", out)
229 }
230 }
231
231 lines GO