231 lines
6.5 KiB
Go
231 lines
6.5 KiB
Go
package builtin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCodeIndexSearchGoSymbols(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "service.go"), `package demo
|
|
|
|
type Service struct{}
|
|
|
|
func NewService() *Service { return &Service{} }
|
|
|
|
func (s *Service) Handle() {}
|
|
|
|
const DefaultName = "demo"
|
|
`)
|
|
|
|
out := runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "Service",
|
|
})
|
|
for _, want := range []string{
|
|
"service.go:3: struct Service",
|
|
"service.go:5: func NewService",
|
|
"service.go:7: method Service.Handle",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("code_index search missing %q; got:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexOutlineTypeScriptAndSkipsNoiseDirs(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "src", "app.ts"), `export interface User {
|
|
name: string
|
|
}
|
|
export class Client {}
|
|
export const loadUser = async () => {}
|
|
`)
|
|
mkfile(t, filepath.Join(root, "node_modules", "dep", "dep.ts"), `export class Dependency {}`)
|
|
|
|
out := runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "outline",
|
|
"path": ".",
|
|
})
|
|
for _, want := range []string{
|
|
"src/app.ts:1: interface User",
|
|
"src/app.ts:4: class Client",
|
|
"src/app.ts:5: func loadUser",
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("code_index outline missing %q; got:\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "Dependency") {
|
|
t.Fatalf("code_index should skip node_modules; got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexOutlineFiltersBeforeLimit(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "a.ts"), `export const first = () => {}
|
|
export const second = () => {}
|
|
`)
|
|
mkfile(t, filepath.Join(root, "z.ts"), `export interface Later {
|
|
id: string
|
|
}
|
|
`)
|
|
|
|
out := runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "outline",
|
|
"kind": "interface",
|
|
"limit": 1,
|
|
})
|
|
if !strings.Contains(out, "z.ts:1: interface Later") {
|
|
t.Fatalf("code_index should find filtered symbols after early unfiltered matches; got:\n%s", out)
|
|
}
|
|
if strings.Contains(out, "first") || strings.Contains(out, "second") {
|
|
t.Fatalf("code_index should filter before returning limited outline; got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexKindFiltersJavaTypes(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "src", "Example.java"), `package demo;
|
|
|
|
public interface Repository {}
|
|
public enum Mode { Fast }
|
|
public class Client {
|
|
public void run() {}
|
|
}
|
|
`)
|
|
|
|
out := runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "Repository",
|
|
"kind": "interface",
|
|
})
|
|
if !strings.Contains(out, "src/Example.java:3: interface Repository") {
|
|
t.Fatalf("code_index should return Java interface for kind=interface; got:\n%s", out)
|
|
}
|
|
|
|
out = runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "Mode",
|
|
"kind": "enum",
|
|
})
|
|
if !strings.Contains(out, "src/Example.java:4: enum Mode") {
|
|
t.Fatalf("code_index should return Java enum for kind=enum; got:\n%s", out)
|
|
}
|
|
|
|
out = runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "Client",
|
|
"kind": "class",
|
|
})
|
|
if !strings.Contains(out, "src/Example.java:5: class Client") {
|
|
t.Fatalf("code_index should return Java class for kind=class; got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexKindFiltersRustItems(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "src", "lib.rs"), `pub struct Store {}
|
|
pub trait Repository {}
|
|
pub enum Mode { Fast }
|
|
pub fn load_store() {}
|
|
`)
|
|
|
|
out := runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "Store",
|
|
"kind": "struct",
|
|
})
|
|
if !strings.Contains(out, "src/lib.rs:1: struct Store") {
|
|
t.Fatalf("code_index should return Rust struct for kind=struct; got:\n%s", out)
|
|
}
|
|
|
|
out = runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "Repository",
|
|
"kind": "trait",
|
|
})
|
|
if !strings.Contains(out, "src/lib.rs:2: trait Repository") {
|
|
t.Fatalf("code_index should return Rust trait for kind=trait; got:\n%s", out)
|
|
}
|
|
|
|
out = runTool(t, codeIndex{workDir: root}, map[string]any{
|
|
"action": "search",
|
|
"query": "load_store",
|
|
"kind": "fn",
|
|
})
|
|
if !strings.Contains(out, "src/lib.rs:4: fn load_store") {
|
|
t.Fatalf("code_index should return Rust fn for kind=fn; got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexRequiresQueryForSearch(t *testing.T) {
|
|
_, err := codeIndex{workDir: t.TempDir()}.Execute(context.Background(), json.RawMessage(`{"action":"search"}`))
|
|
if err == nil || !strings.Contains(err.Error(), "query is required") {
|
|
t.Fatalf("error = %v, want query required", err)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexWorkspaceBinding(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "main.py"), "class App:\n pass\n")
|
|
t.Chdir(t.TempDir())
|
|
|
|
tools := byName(Workspace{Dir: root}.Tools("code_index"))
|
|
tl := tools["code_index"]
|
|
if tl == nil {
|
|
t.Fatal("workspace did not expose code_index")
|
|
}
|
|
out, err := tl.Execute(context.Background(), json.RawMessage(`{"action":"outline","path":"."}`))
|
|
if err != nil {
|
|
t.Fatalf("code_index: %v", err)
|
|
}
|
|
if !strings.Contains(out, "main.py:1: class App") {
|
|
t.Fatalf("code_index should resolve relative paths against workspace; got:\n%s", out)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexForbidRead(t *testing.T) {
|
|
root := t.TempDir()
|
|
mkfile(t, filepath.Join(root, "allowed.go"), "package demo\nfunc AllowedNeedle() {}\n")
|
|
secretDir := filepath.Join(root, "secret")
|
|
mkfile(t, filepath.Join(secretDir, "secret.go"), "package secret\nfunc SecretNeedle() {}\n")
|
|
|
|
ci := codeIndex{workDir: root, forbidRoots: realRoots([]string{secretDir})}
|
|
out := runTool(t, ci, map[string]any{
|
|
"action": "search",
|
|
"query": "Needle",
|
|
"path": ".",
|
|
})
|
|
if !strings.Contains(out, "AllowedNeedle") {
|
|
t.Fatalf("code_index should still return allowed symbols, got:\n%s", out)
|
|
}
|
|
if strings.Contains(out, "SecretNeedle") || strings.Contains(out, "secret.go") {
|
|
t.Fatalf("code_index leaked forbidden symbols:\n%s", out)
|
|
}
|
|
|
|
out = runTool(t, ci, map[string]any{
|
|
"action": "search",
|
|
"query": "SecretNeedle",
|
|
"path": "secret",
|
|
})
|
|
if out != "(no symbols)" {
|
|
t.Fatalf("code_index forbidden root = %q, want (no symbols)", out)
|
|
}
|
|
}
|
|
|
|
func TestCodeIndexIgnoresLargeFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(root, "huge.ts"), []byte(strings.Repeat("x", codeIndexMaxFileSize+1)), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := runTool(t, codeIndex{workDir: root}, map[string]any{"action": "outline"})
|
|
if out != "(no symbols)" {
|
|
t.Fatalf("large files should be ignored, got:\n%s", out)
|
|
}
|
|
}
|