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
274 lines
8.5 KiB
Go
274 lines
8.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
mcplib "github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"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/parser"
|
|
"github.com/zzet/gortex/internal/parser/languages"
|
|
"github.com/zzet/gortex/internal/query"
|
|
"github.com/zzet/gortex/internal/search"
|
|
)
|
|
|
|
// setupDeclServer indexes a multi-file Go project where one function is
|
|
// called from several sites, so the use-site → declaration join and its
|
|
// grouping can be exercised.
|
|
func setupDeclServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
|
|
writeDecl := func(rel, content string) {
|
|
p := filepath.Join(dir, filepath.FromSlash(rel))
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
|
|
require.NoError(t, os.WriteFile(p, []byte(content), 0o644))
|
|
}
|
|
|
|
writeDecl("app.go", `package app
|
|
|
|
func compute() int {
|
|
return 42
|
|
}
|
|
|
|
func runOnce() int {
|
|
return compute()
|
|
}
|
|
|
|
func runTwice() int {
|
|
return compute() + compute()
|
|
}
|
|
`)
|
|
writeDecl("svc/handler.go", `package svc
|
|
|
|
import "fmt"
|
|
|
|
func helperFn() string {
|
|
return "ok"
|
|
}
|
|
|
|
func Handle() {
|
|
fmt.Println(helperFn())
|
|
}
|
|
`)
|
|
|
|
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)
|
|
srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil)
|
|
srv.RunAnalysis()
|
|
return srv
|
|
}
|
|
|
|
// declResult unmarshals a find_declaration response.
|
|
func declResult(t *testing.T, result *mcplib.CallToolResult) map[string]any {
|
|
t.Helper()
|
|
require.False(t, result.IsError, "find_declaration errored: %s", toolResultText(result))
|
|
var payload map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(result.Content[0].(mcplib.TextContent).Text), &payload))
|
|
return payload
|
|
}
|
|
|
|
// declNames returns the set of resolved declaration names in a response.
|
|
func declNames(payload map[string]any) map[string]int {
|
|
out := map[string]int{}
|
|
decls, _ := payload["declarations"].([]any)
|
|
for _, d := range decls {
|
|
dm, _ := d.(map[string]any)
|
|
decl, _ := dm["declaration"].(map[string]any)
|
|
name, _ := decl["name"].(string)
|
|
uses, _ := dm["use_sites"].([]any)
|
|
out[name] = len(uses)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestFindDeclaration_RequiresUseSite(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
result := callTool(t, srv, "find_declaration", map[string]any{})
|
|
require.True(t, result.IsError)
|
|
assert.Contains(t, toolResultText(result), "use_site is required")
|
|
}
|
|
|
|
func TestFindDeclaration_LiteralResolvesDeclaration(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "helperFn(",
|
|
}))
|
|
names := declNames(payload)
|
|
assert.Contains(t, names, "helperFn", "literal use site must resolve to the helperFn declaration")
|
|
}
|
|
|
|
func TestFindDeclaration_GroupsMultipleUseSites(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
// compute() is called from runOnce (once) and runTwice (twice) — and
|
|
// also appears in its own declaration line "func compute() int".
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "compute()",
|
|
}))
|
|
names := declNames(payload)
|
|
require.Contains(t, names, "compute", "compute must be resolved")
|
|
// The three call sites (runOnce x1, runTwice x2) all group under one
|
|
// declaration entry.
|
|
assert.GreaterOrEqual(t, names["compute"], 3,
|
|
"all three compute() call sites must group under one declaration")
|
|
assert.Equal(t, 1, len(declNames(payload)),
|
|
"compute() use sites must collapse to a single declaration group")
|
|
}
|
|
|
|
func TestFindDeclaration_RegexResolvesDeclaration(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
// Regex matching the compute call sites.
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": `compute\(\)`,
|
|
"regex": true,
|
|
}))
|
|
names := declNames(payload)
|
|
assert.Contains(t, names, "compute", "regex use site must resolve to the compute declaration")
|
|
}
|
|
|
|
func TestFindDeclaration_KindFilter(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
// helperFn is a function; filtering to type only must drop it.
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "helperFn(",
|
|
"kind": "type",
|
|
}))
|
|
names := declNames(payload)
|
|
assert.NotContains(t, names, "helperFn", "kind=type filter must drop the function declaration")
|
|
|
|
// Filtering to function keeps it.
|
|
payload = declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "helperFn(",
|
|
"kind": "function",
|
|
}))
|
|
assert.Contains(t, declNames(payload), "helperFn", "kind=function filter must keep the function")
|
|
}
|
|
|
|
func TestFindDeclaration_PathPrefixScoping(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
// helperFn is only used in svc/ — a path_prefix of app/ finds nothing.
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "helperFn(",
|
|
"path_prefix": "app",
|
|
}))
|
|
assert.Equal(t, float64(0), payload["count"], "path_prefix=app must exclude svc/handler.go matches")
|
|
|
|
// Scoped to svc/ it resolves.
|
|
payload = declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "helperFn(",
|
|
"path_prefix": "svc/",
|
|
}))
|
|
assert.Contains(t, declNames(payload), "helperFn")
|
|
}
|
|
|
|
func TestFindDeclaration_RegexPathPrefix(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": `helperFn\(`,
|
|
"regex": true,
|
|
"path_prefix": "svc/",
|
|
}))
|
|
assert.Contains(t, declNames(payload), "helperFn")
|
|
}
|
|
|
|
func TestFindDeclaration_NoMatch(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "nonexistent_zzz_call(",
|
|
}))
|
|
assert.Equal(t, float64(0), payload["count"])
|
|
}
|
|
|
|
func TestFindDeclaration_BadRegex(t *testing.T) {
|
|
srv := setupDeclServer(t)
|
|
|
|
result := callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "[unclosed",
|
|
"regex": true,
|
|
})
|
|
require.True(t, result.IsError)
|
|
assert.Contains(t, toolResultText(result), "invalid regex")
|
|
}
|
|
|
|
// TestFindDeclaration_MultiRepoFanout pins the regression: in
|
|
// multi-repo mode the daemon owns a MultiIndexer and s.indexer has
|
|
// no rootPath, so the legacy s.indexer.GrepText path returned no
|
|
// matches and find_declaration silently reported `count: 0` for
|
|
// use sites that genuinely existed across the workspace. The fix
|
|
// routes Stage 1 through the MultiIndexer fan-out so use-site hits
|
|
// surface with repo-prefixed paths.
|
|
func TestFindDeclaration_MultiRepoFanout(t *testing.T) {
|
|
dir := t.TempDir()
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "alpha"), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "alpha", "main.go"), []byte(`package alpha
|
|
|
|
func uniqueDecl() int { return 1 }
|
|
|
|
func consumer() int { return uniqueDecl() + uniqueDecl() }
|
|
`), 0o644))
|
|
// Second repo so IsMultiRepo() is true; the use_site only lives
|
|
// in alpha but multi-repo dispatch should still find it.
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "beta"), 0o755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "beta", "main.go"), []byte("package beta\n\nfunc placeholder() {}\n"), 0o644))
|
|
|
|
repoA := filepath.Join(dir, "alpha")
|
|
repoB := filepath.Join(dir, "beta")
|
|
|
|
tmpCfg := filepath.Join(t.TempDir(), "config.yaml")
|
|
gc := &config.GlobalConfig{
|
|
Repos: []config.RepoEntry{
|
|
{Path: repoA, Name: "alpha"},
|
|
{Path: repoB, Name: "beta"},
|
|
},
|
|
}
|
|
gc.SetConfigPath(tmpCfg)
|
|
require.NoError(t, gc.Save())
|
|
cm, err := config.NewConfigManager(tmpCfg)
|
|
require.NoError(t, err)
|
|
|
|
reg := parser.NewRegistry()
|
|
reg.Register(languages.NewGoExtractor())
|
|
g := graph.New()
|
|
mi := indexer.NewMultiIndexer(g, reg, search.NewBM25(), cm, zap.NewNop())
|
|
_, err = mi.IndexAll()
|
|
require.NoError(t, err)
|
|
require.True(t, mi.IsMultiRepo())
|
|
|
|
eng := query.NewEngine(g)
|
|
singleton := indexer.New(g, reg, config.IndexConfig{}, zap.NewNop())
|
|
srv := NewServer(eng, g, singleton, nil, zap.NewNop(), nil, MultiRepoOptions{
|
|
ConfigManager: cm,
|
|
MultiIndexer: mi,
|
|
})
|
|
|
|
payload := declResult(t, callTool(t, srv, "find_declaration", map[string]any{
|
|
"use_site": "uniqueDecl(",
|
|
}))
|
|
require.Equal(t, float64(1), payload["count"],
|
|
"expected one declaration with two grouped use sites; got %v", payload)
|
|
names := declNames(payload)
|
|
require.Contains(t, names, "uniqueDecl")
|
|
require.Equal(t, 2, names["uniqueDecl"], "two use sites must group under one declaration")
|
|
}
|