Files
zzet--gortex/internal/mcp/tools_analyze_external_calls_test.go
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

278 lines
10 KiB
Go

package mcp
import (
"context"
"encoding/json"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/zzet/gortex/internal/graph"
)
func callAnalyzeExternalCalls(t *testing.T, srv *Server, args map[string]any) map[string]any {
t.Helper()
args["kind"] = "external_calls"
req := mcplib.CallToolRequest{}
req.Params.Name = "analyze"
req.Params.Arguments = args
res, err := srv.handleAnalyze(context.Background(), req)
if err != nil {
t.Fatalf("handleAnalyze: %v", err)
}
if res.IsError {
t.Fatalf("error: %+v", res.Content)
}
textBlock := res.Content[0].(mcplib.TextContent)
var out map[string]any
if err := json.Unmarshal([]byte(textBlock.Text), &out); err != nil {
t.Fatalf("json: %v\n%s", err, textBlock.Text)
}
return out
}
func addExternalModuleNode(g graph.Store, id, path, version, kind string) {
g.AddNode(&graph.Node{
ID: id,
Kind: graph.KindModule,
Name: path,
Language: "go",
Meta: map[string]any{
"path": path,
"version": version,
"module_kind": kind,
},
})
}
func addExternalSymbolNode(g graph.Store, id, name, importPath, moduleID string, kind graph.NodeKind) {
g.AddNode(&graph.Node{
ID: id,
Kind: kind,
Name: name,
Language: "go",
Meta: map[string]any{
"external": true,
"import_path": importPath,
"module_id": moduleID,
},
})
g.AddEdge(&graph.Edge{
From: id,
To: moduleID,
Kind: graph.EdgeDependsOnModule,
})
}
func addExternalCall(g graph.Store, from, to string) {
g.AddEdge(&graph.Edge{
From: from,
To: to,
Kind: graph.EdgeCalls,
})
}
func TestAnalyzeExternalCalls_GroupsByModule(t *testing.T) {
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalModuleNode(srv.graph, "module::go:github.com/foo/bar@v1.2.3", "github.com/foo/bar", "v1.2.3", "module_cache")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt",
"module::go:stdlib", graph.KindFunction)
addExternalSymbolNode(srv.graph, "ext::go:fmt::Sprintf", "Sprintf", "fmt",
"module::go:stdlib", graph.KindFunction)
addExternalSymbolNode(srv.graph, "ext::go:github.com/foo/bar::Do", "Do", "github.com/foo/bar",
"module::go:github.com/foo/bar@v1.2.3", graph.KindFunction)
addExternalCall(srv.graph, "main.go::A", "ext::go:fmt::Println")
addExternalCall(srv.graph, "main.go::B", "ext::go:fmt::Println")
addExternalCall(srv.graph, "main.go::C", "ext::go:fmt::Sprintf")
addExternalCall(srv.graph, "main.go::D", "ext::go:github.com/foo/bar::Do")
out := callAnalyzeExternalCalls(t, srv, map[string]any{})
rows, _ := out["modules"].([]any)
if len(rows) != 2 {
t.Fatalf("expected 2 module rollups, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["id"] != "module::go:stdlib" {
t.Errorf("expected stdlib first (3 calls), got %v", first["id"])
}
if int(first["calls"].(float64)) != 3 {
t.Errorf("expected 3 calls on stdlib, got %v", first["calls"])
}
if int(first["symbols"].(float64)) != 2 {
t.Errorf("expected 2 symbols on stdlib, got %v", first["symbols"])
}
if first["module_kind"] != "stdlib" {
t.Errorf("expected module_kind=stdlib, got %v", first["module_kind"])
}
}
func TestAnalyzeExternalCalls_ModuleKindFilter(t *testing.T) {
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalModuleNode(srv.graph, "module::go:github.com/foo/bar@v1", "github.com/foo/bar", "v1", "module_cache")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalSymbolNode(srv.graph, "ext::go:github.com/foo/bar::X", "X", "github.com/foo/bar", "module::go:github.com/foo/bar@v1", graph.KindFunction)
addExternalCall(srv.graph, "main.go::A", "ext::go:fmt::Println")
addExternalCall(srv.graph, "main.go::B", "ext::go:github.com/foo/bar::X")
out := callAnalyzeExternalCalls(t, srv, map[string]any{
"module_kind": "module_cache",
})
rows, _ := out["modules"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 module after module_kind=module_cache, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["module_kind"] != "module_cache" {
t.Errorf("filter leaked: %v", first["module_kind"])
}
}
func TestAnalyzeExternalCalls_ModulePathFilter(t *testing.T) {
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalModuleNode(srv.graph, "module::go:github.com/foo/bar@v1", "github.com/foo/bar", "v1", "module_cache")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalSymbolNode(srv.graph, "ext::go:github.com/foo/bar::X", "X", "github.com/foo/bar", "module::go:github.com/foo/bar@v1", graph.KindFunction)
addExternalCall(srv.graph, "main.go::A", "ext::go:fmt::Println")
addExternalCall(srv.graph, "main.go::B", "ext::go:github.com/foo/bar::X")
out := callAnalyzeExternalCalls(t, srv, map[string]any{
"module_path": "github.com",
})
rows, _ := out["modules"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 module after module_path=github.com, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["path"] != "github.com/foo/bar" {
t.Errorf("filter leaked: %v", first["path"])
}
}
// TestAnalyzeExternalCalls_SkipsSyntheticNodes confirms the rollup
// composes with the resolver's external-call synthesis pass: a
// synthetic KindModule placeholder (it carries Meta["synthetic"] but no
// goanalysis attribution) must not surface as an empty 0/0 row.
func TestAnalyzeExternalCalls_SkipsSyntheticNodes(t *testing.T) {
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalCall(srv.graph, "main.go::A", "ext::go:fmt::Println")
// A synthetic external-call node — the shape the resolver's
// SynthesizeExternalCalls pass materialises for an un-indexed
// package. It must be ignored by this rollup.
srv.graph.AddNode(&graph.Node{
ID: "external-call::dep::github.com/acme/widget",
Kind: graph.KindModule,
Name: "github.com/acme/widget",
Language: "go",
Meta: map[string]any{
"synthetic": true,
"external_call": true,
"import_path": "github.com/acme/widget",
"ecosystem": "dep",
},
})
addExternalCall(srv.graph, "main.go::B", "external-call::dep::github.com/acme/widget")
out := callAnalyzeExternalCalls(t, srv, map[string]any{})
rows, _ := out["modules"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 module (synthetic node skipped), got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["id"] != "module::go:stdlib" {
t.Errorf("synthetic node leaked into the rollup: %v", first["id"])
}
}
func TestAnalyzeExternalCalls_PerModuleSymbolDetail(t *testing.T) {
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalSymbolNode(srv.graph, "ext::go:fmt::Sprintf", "Sprintf", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalCall(srv.graph, "main.go::A", "ext::go:fmt::Println")
addExternalCall(srv.graph, "main.go::B", "ext::go:fmt::Println") // 2 callers
addExternalCall(srv.graph, "main.go::C", "ext::go:fmt::Sprintf")
out := callAnalyzeExternalCalls(t, srv, map[string]any{
"id": "module::go:stdlib",
})
if out["module"] != "module::go:stdlib" {
t.Errorf("expected module=stdlib, got %v", out["module"])
}
rows, _ := out["symbols"].([]any)
if len(rows) != 2 {
t.Fatalf("expected 2 symbols, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["name"] != "Println" {
t.Errorf("expected Println first (2 calls), got %v", first["name"])
}
if int(first["calls"].(float64)) != 2 {
t.Errorf("expected 2 calls, got %v", first["calls"])
}
if int(first["callers"].(float64)) != 2 {
t.Errorf("expected 2 distinct callers, got %v", first["callers"])
}
}
func TestAnalyzeExternalCalls_PerModuleNameFilter(t *testing.T) {
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalSymbolNode(srv.graph, "ext::go:fmt::Sprintf", "Sprintf", "fmt", "module::go:stdlib", graph.KindFunction)
addExternalCall(srv.graph, "main.go::A", "ext::go:fmt::Println")
addExternalCall(srv.graph, "main.go::B", "ext::go:fmt::Sprintf")
out := callAnalyzeExternalCalls(t, srv, map[string]any{
"id": "module::go:stdlib",
"name": "println",
})
rows, _ := out["symbols"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 symbol after name=println filter, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["name"] != "Println" {
t.Errorf("expected Println, got %v", first["name"])
}
}
func TestAnalyzeExternalCalls_DependsOnModuleEdgesIgnoredInCallCount(t *testing.T) {
// EdgeDependsOnModule from external symbol → KindModule must NOT be
// counted as a "call". Only true call/reference edges count.
srv, _ := setupTestServer(t)
addExternalModuleNode(srv.graph, "module::go:stdlib", "stdlib", "", "stdlib")
addExternalSymbolNode(srv.graph, "ext::go:fmt::Println", "Println", "fmt", "module::go:stdlib", graph.KindFunction)
// No real callers, just the structural EdgeDependsOnModule.
out := callAnalyzeExternalCalls(t, srv, map[string]any{})
rows, _ := out["modules"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 module, got %d", len(rows))
}
first := rows[0].(map[string]any)
if int(first["calls"].(float64)) != 0 {
t.Errorf("EdgeDependsOnModule must not count as a call; got calls=%v", first["calls"])
}
if int(first["symbols"].(float64)) != 1 {
t.Errorf("expected 1 symbol, got %v", first["symbols"])
}
}
func TestAnalyzeExternalCalls_EmptyGraphReturnsZero(t *testing.T) {
srv, _ := setupTestServer(t)
out := callAnalyzeExternalCalls(t, srv, map[string]any{})
rows, _ := out["modules"].([]any)
if len(rows) != 0 {
t.Fatalf("expected 0 modules on empty graph, got %d", len(rows))
}
if int(out["total"].(float64)) != 0 {
t.Errorf("expected total=0, got %v", out["total"])
}
}