a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
mcplib "github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/indexer"
|
|
"github.com/zzet/gortex/internal/query"
|
|
)
|
|
|
|
type findFilesResult struct {
|
|
Files []struct {
|
|
Path string `json:"path"`
|
|
Language string `json:"language"`
|
|
ID string `json:"id"`
|
|
} `json:"files"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
func setupFindFilesServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
write := func(rel, body string) {
|
|
full := filepath.Join(dir, filepath.FromSlash(rel))
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
|
|
require.NoError(t, os.WriteFile(full, []byte(body), 0o644))
|
|
}
|
|
write("main.go", "package app\n\nfunc Main() {}\n")
|
|
write("internal/handler.go", "package internal\n\nfunc Handle() {}\n")
|
|
write("internal/sub/handler_test.go", "package sub\n\nfunc TestHandle() {}\n")
|
|
|
|
g := graph.New()
|
|
reg := testRegistry()
|
|
cfg := config.Default()
|
|
idx := indexer.New(g, reg, cfg.Index, zap.NewNop())
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
eng := query.NewEngine(g)
|
|
return NewServer(eng, g, idx, nil, zap.NewNop(), nil)
|
|
}
|
|
|
|
func decodeFindFiles(t *testing.T, res *mcplib.CallToolResult) findFilesResult {
|
|
t.Helper()
|
|
require.False(t, res.IsError)
|
|
var out findFilesResult
|
|
require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &out))
|
|
return out
|
|
}
|
|
|
|
func TestFindFiles_ByName(t *testing.T) {
|
|
srv := setupFindFilesServer(t)
|
|
|
|
// A basename substring matches both handler files; the shallower
|
|
// one ranks first (same score, fewer path segments).
|
|
resp := decodeFindFiles(t, callTool(t, srv, "find_files", map[string]any{"query": "handler"}))
|
|
require.GreaterOrEqual(t, resp.Count, 2)
|
|
require.Equal(t, "internal/handler.go", resp.Files[0].Path)
|
|
|
|
// An exact basename only matches the one file.
|
|
exact := decodeFindFiles(t, callTool(t, srv, "find_files", map[string]any{"query": "handler.go"}))
|
|
require.Equal(t, 1, exact.Count)
|
|
require.Equal(t, "internal/handler.go", exact.Files[0].Path)
|
|
|
|
// main.go is reachable by name.
|
|
main := decodeFindFiles(t, callTool(t, srv, "find_files", map[string]any{"query": "main"}))
|
|
require.Equal(t, 1, main.Count)
|
|
require.Equal(t, "main.go", main.Files[0].Path)
|
|
}
|
|
|
|
func TestFindFiles_Glob(t *testing.T) {
|
|
srv := setupFindFilesServer(t)
|
|
|
|
resp := decodeFindFiles(t, callTool(t, srv, "find_files", map[string]any{"glob": "*_test.go"}))
|
|
require.Equal(t, 1, resp.Count)
|
|
require.Equal(t, "internal/sub/handler_test.go", resp.Files[0].Path)
|
|
}
|
|
|
|
func TestFindFiles_Fuzzy(t *testing.T) {
|
|
srv := setupFindFilesServer(t)
|
|
|
|
// "hndlr" is a subsequence of "handler.go" but not a substring.
|
|
none := decodeFindFiles(t, callTool(t, srv, "find_files", map[string]any{"query": "hndlr"}))
|
|
require.Equal(t, 0, none.Count)
|
|
|
|
fuzzy := decodeFindFiles(t, callTool(t, srv, "find_files",
|
|
map[string]any{"query": "hndlr", "fuzzy": true}))
|
|
require.GreaterOrEqual(t, fuzzy.Count, 1)
|
|
require.Equal(t, "internal/handler.go", fuzzy.Files[0].Path)
|
|
}
|
|
|
|
func TestFindFiles_PathScoping(t *testing.T) {
|
|
srv := setupFindFilesServer(t)
|
|
|
|
scoped := decodeFindFiles(t, callTool(t, srv, "find_files",
|
|
map[string]any{"query": "handler", "path": "internal/sub"}))
|
|
require.Equal(t, 1, scoped.Count)
|
|
require.Equal(t, "internal/sub/handler_test.go", scoped.Files[0].Path)
|
|
}
|
|
|
|
func TestFindFiles_RequiresArg(t *testing.T) {
|
|
srv := setupFindFilesServer(t)
|
|
bad := callTool(t, srv, "find_files", map[string]any{})
|
|
require.True(t, bad.IsError)
|
|
}
|
|
|
|
func TestScoreFilenameMatch(t *testing.T) {
|
|
cases := []struct {
|
|
query, base, rel string
|
|
fuzzy bool
|
|
want int
|
|
ok bool
|
|
}{
|
|
{"handler.go", "handler.go", "internal/handler.go", false, 100, true},
|
|
{"hand", "handler.go", "internal/handler.go", false, 70, true},
|
|
{"ndl", "handler.go", "internal/handler.go", false, 50, true},
|
|
{"internal", "handler.go", "internal/handler.go", false, 30, true},
|
|
{"nomatch", "handler.go", "internal/handler.go", false, 0, false},
|
|
{"hndlr", "handler.go", "internal/handler.go", true, 10, true},
|
|
{"hndlr", "handler.go", "internal/handler.go", false, 0, false},
|
|
}
|
|
for _, c := range cases {
|
|
got, ok := scoreFilenameMatch(c.query, c.base, c.rel, c.fuzzy)
|
|
require.Equal(t, c.ok, ok, "match? query=%q", c.query)
|
|
require.Equal(t, c.want, got, "score query=%q", c.query)
|
|
}
|
|
}
|
|
|
|
func TestIsSubsequence(t *testing.T) {
|
|
require.True(t, isSubsequence("", "anything"))
|
|
require.True(t, isSubsequence("abc", "aXbYcZ"))
|
|
require.True(t, isSubsequence("hndlr", "handler.go"))
|
|
require.False(t, isSubsequence("abc", "acb"))
|
|
require.False(t, isSubsequence("xyz", "abc"))
|
|
}
|