Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

302 lines
9.7 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/query"
)
// setupNavServer indexes a Go source with a deeper call graph and a type
// carrying several methods, so the nav tool's into / up / sibling moves
// have real candidates to choose between.
func setupNavServer(t *testing.T) (*Server, graph.Store) {
t.Helper()
dir := t.TempDir()
src := `package svc
type Service struct {
name string
}
func (s *Service) Start() {
s.boot()
s.warm()
}
func (s *Service) Stop() {}
func (s *Service) boot() {}
func (s *Service) warm() {}
func run() {
svc := &Service{}
svc.Start()
}
`
require.NoError(t, os.WriteFile(filepath.Join(dir, "svc.go"), []byte(src), 0o644))
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, g
}
// navResult unmarshals a nav response payload.
func navResult(t *testing.T, result *mcplib.CallToolResult) map[string]any {
t.Helper()
require.False(t, result.IsError, "nav errored: %s", toolResultText(result))
var payload map[string]any
require.NoError(t, json.Unmarshal([]byte(result.Content[0].(mcplib.TextContent).Text), &payload))
return payload
}
// navFindMethod returns the graph ID of a method named `name`.
func navFindMethod(t *testing.T, g graph.Store, name string) string {
t.Helper()
for _, n := range g.AllNodes() {
if n.Name == name && (n.Kind == graph.KindMethod || n.Kind == graph.KindFunction) {
return n.ID
}
}
t.Fatalf("no method/function named %q in the test graph", name)
return ""
}
func navCursorCurrent(payload map[string]any) string {
cursor, _ := payload["cursor"].(map[string]any)
if cursor == nil {
return ""
}
s, _ := cursor["current"].(string)
return s
}
func TestNav_RequiresAction(t *testing.T) {
srv, _ := setupNavServer(t)
result := callTool(t, srv, "nav", map[string]any{})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "action is required")
}
func TestNav_GotoSetsCursor(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
payload := navResult(t, callTool(t, srv, "nav", map[string]any{
"action": "goto", "id": start,
}))
assert.Equal(t, start, navCursorCurrent(payload))
assert.Equal(t, "goto", payload["action"])
}
func TestNav_GotoMissingID(t *testing.T) {
srv, _ := setupNavServer(t)
result := callTool(t, srv, "nav", map[string]any{"action": "goto"})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "id is required")
}
func TestNav_GotoUnknownSymbol(t *testing.T) {
srv, _ := setupNavServer(t)
result := callTool(t, srv, "nav", map[string]any{
"action": "goto", "id": "no/such::Symbol",
})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "symbol not found")
}
func TestNav_IntoMovesToCallee(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{"action": "into"}))
// Start calls boot and warm; into with no select picks the first.
cur := navCursorCurrent(payload)
assert.NotEqual(t, start, cur, "into must advance the cursor")
node := g.GetNode(cur)
require.NotNil(t, node)
assert.Contains(t, []string{"boot", "warm"}, node.Name)
}
func TestNav_IntoSelectByName(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{
"action": "into", "select": "warm",
}))
node := g.GetNode(navCursorCurrent(payload))
require.NotNil(t, node)
assert.Equal(t, "warm", node.Name, "select by name substring must target warm")
}
func TestNav_IntoSelectByIndex(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{
"action": "into", "select": "1",
}))
node := g.GetNode(navCursorCurrent(payload))
require.NotNil(t, node)
assert.Contains(t, []string{"boot", "warm"}, node.Name)
}
func TestNav_IntoSelectOutOfRange(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
result := callTool(t, srv, "nav", map[string]any{
"action": "into", "select": "99",
})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "out of range")
}
func TestNav_UpMovesToCaller(t *testing.T) {
srv, g := setupNavServer(t)
boot := navFindMethod(t, g, "boot")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": boot})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{"action": "up"}))
node := g.GetNode(navCursorCurrent(payload))
require.NotNil(t, node)
assert.Equal(t, "Start", node.Name, "up from boot must reach its caller Start")
}
func TestNav_SiblingMovesAcrossParent(t *testing.T) {
srv, g := setupNavServer(t)
boot := navFindMethod(t, g, "boot")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": boot})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{"action": "sibling"}))
node := g.GetNode(navCursorCurrent(payload))
require.NotNil(t, node)
// boot is a method of Service; its siblings are the other members
// of Service — the Start / Stop / warm methods and the name field.
assert.Contains(t, []string{"Start", "Stop", "warm", "name"}, node.Name)
assert.NotEqual(t, "boot", node.Name)
}
// TestNav_SiblingSelectByName targets a specific sibling by name.
func TestNav_SiblingSelectByName(t *testing.T) {
srv, g := setupNavServer(t)
boot := navFindMethod(t, g, "boot")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": boot})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{
"action": "sibling", "select": "Stop",
}))
node := g.GetNode(navCursorCurrent(payload))
require.NotNil(t, node)
assert.Equal(t, "Stop", node.Name, "sibling select by name must target Stop")
}
func TestNav_BackPopsHistory(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
callTool(t, srv, "nav", map[string]any{"action": "into"}) // moved to a callee
payload := navResult(t, callTool(t, srv, "nav", map[string]any{"action": "back"}))
assert.Equal(t, start, navCursorCurrent(payload), "back must return to Start")
}
func TestNav_BackEmptyHistory(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
result := callTool(t, srv, "nav", map[string]any{"action": "back"})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "history is empty")
}
func TestNav_WhereReportsCursor(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{"action": "where"}))
assert.Equal(t, start, navCursorCurrent(payload))
assert.Equal(t, "where", payload["action"])
adj, ok := payload["adjacency"].(map[string]any)
require.True(t, ok, "where must carry an adjacency preview")
// Start calls boot and warm.
assert.Equal(t, float64(2), adj["callees"])
}
func TestNav_WhereUnsetCursor(t *testing.T) {
srv, _ := setupNavServer(t)
result := callTool(t, srv, "nav", map[string]any{"action": "where"})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "cursor is unset")
}
func TestNav_ReadReturnsSource(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
payload := navResult(t, callTool(t, srv, "nav", map[string]any{"action": "read"}))
src, _ := payload["source"].(string)
assert.Contains(t, src, "func (s *Service) Start()", "read must return the symbol source")
}
func TestNav_ReadUnsetCursor(t *testing.T) {
srv, _ := setupNavServer(t)
result := callTool(t, srv, "nav", map[string]any{"action": "read"})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "cursor is unset")
}
func TestNav_UnknownAction(t *testing.T) {
srv, _ := setupNavServer(t)
result := callTool(t, srv, "nav", map[string]any{"action": "teleport"})
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "unknown action")
}
// TestNav_StaleCursorReset verifies that when a re-index removes the
// node the cursor points at, the next nav call resets gracefully.
func TestNav_StaleCursorReset(t *testing.T) {
srv, g := setupNavServer(t)
start := navFindMethod(t, g, "Start")
// Position the cursor, then evict the file out from under it so
// the cursor node no longer exists in the graph.
callTool(t, srv, "nav", map[string]any{"action": "goto", "id": start})
node := g.GetNode(start)
require.NotNil(t, node)
g.EvictFile(node.FilePath)
result := callTool(t, srv, "nav", map[string]any{"action": "where"})
// where on a now-empty cursor is an error, but it must report the
// reset rather than operate on the dangling ID.
require.True(t, result.IsError)
assert.Contains(t, toolResultText(result), "cursor is unset")
}