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
218 lines
7.9 KiB
Go
218 lines
7.9 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/analysis"
|
|
"github.com/zzet/gortex/internal/contracts"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func newArchitectureTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
g := graph.New()
|
|
// A small architecture: handler.go calls service.go calls repo.go.
|
|
// Plus a Python file in a different language for the language mix.
|
|
g.AddNode(&graph.Node{ID: "p/handler.go::Handle", Name: "Handle", Kind: graph.KindFunction, FilePath: "p/handler.go", Language: "go", StartLine: 5})
|
|
g.AddNode(&graph.Node{ID: "p/service.go::Process", Name: "Process", Kind: graph.KindFunction, FilePath: "p/service.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "p/repo.go::Fetch", Name: "Fetch", Kind: graph.KindFunction, FilePath: "p/repo.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "p/script.py::main", Name: "main", Kind: graph.KindFunction, FilePath: "p/script.py", Language: "python"})
|
|
|
|
g.AddEdge(&graph.Edge{From: "p/handler.go::Handle", To: "p/service.go::Process", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "p/service.go::Process", To: "p/repo.go::Fetch", Kind: graph.EdgeCalls})
|
|
|
|
return &Server{
|
|
graph: g,
|
|
session: newSessionState(),
|
|
tokenStats: &tokenStats{},
|
|
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
|
|
sessions: newSessionMap(),
|
|
toolScopes: newScopeRegistry(),
|
|
}
|
|
}
|
|
|
|
func callArchitectureHandler(t *testing.T, s *Server, args map[string]any) map[string]any {
|
|
t.Helper()
|
|
req := mcp.CallToolRequest{}
|
|
req.Params.Arguments = args
|
|
res, err := s.handleGetArchitecture(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.False(t, res.IsError, "handler error: %+v", res.Content)
|
|
tc, ok := res.Content[0].(mcp.TextContent)
|
|
require.True(t, ok)
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
|
|
return m
|
|
}
|
|
|
|
func TestArchitecture_SummaryReportsLanguageMix(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
out := callArchitectureHandler(t, s, map[string]any{})
|
|
|
|
summary := out["summary"].(map[string]any)
|
|
assert.Equal(t, "go", summary["primary_language"], "go wins on node count")
|
|
languages, _ := summary["languages"].([]any)
|
|
require.NotEmpty(t, languages)
|
|
|
|
// Find python entry.
|
|
pythonSeen := false
|
|
for _, l := range languages {
|
|
m := l.(map[string]any)
|
|
if m["name"] == "python" {
|
|
pythonSeen = true
|
|
}
|
|
}
|
|
assert.True(t, pythonSeen, "python should appear in the language mix")
|
|
|
|
assert.EqualValues(t, 4, summary["total_nodes"].(float64))
|
|
assert.EqualValues(t, 2, summary["total_edges"].(float64))
|
|
}
|
|
|
|
func TestArchitecture_EntryPointsAreUncalledFunctions(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
out := callArchitectureHandler(t, s, map[string]any{})
|
|
|
|
entries, _ := out["entry_points"].([]any)
|
|
require.NotEmpty(t, entries, "Handle has zero in-edges and one out-edge — it must surface")
|
|
ids := map[string]bool{}
|
|
for _, e := range entries {
|
|
ids[e.(map[string]any)["id"].(string)] = true
|
|
}
|
|
assert.True(t, ids["p/handler.go::Handle"], "Handle is the obvious entry point")
|
|
assert.False(t, ids["p/service.go::Process"], "Process has a caller, not an entry point")
|
|
}
|
|
|
|
func TestArchitecture_CommunitiesIncludedWhenAnalysisCached(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
s.analysisMu.Lock()
|
|
s.communities = &analysis.CommunityResult{
|
|
Communities: []analysis.Community{
|
|
{
|
|
ID: "c-handler",
|
|
Label: "handler-service-repo",
|
|
Size: 3,
|
|
Files: []string{"p/handler.go", "p/service.go", "p/repo.go"},
|
|
Members: []string{
|
|
"p/handler.go::Handle", "p/service.go::Process", "p/repo.go::Fetch",
|
|
},
|
|
},
|
|
},
|
|
Modularity: 0.5,
|
|
}
|
|
s.analysisMu.Unlock()
|
|
|
|
out := callArchitectureHandler(t, s, map[string]any{})
|
|
communities := out["communities"].(map[string]any)
|
|
assert.EqualValues(t, 1, communities["count"].(float64))
|
|
top := communities["top"].([]any)
|
|
require.Len(t, top, 1)
|
|
assert.Equal(t, "c-handler", top[0].(map[string]any)["id"])
|
|
}
|
|
|
|
func TestArchitecture_CrossRepoRollup(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
// Add a second repo's node and a cross-repo call edge.
|
|
s.graph.AddNode(&graph.Node{ID: "repoB/lib.go::Util", Name: "Util", Kind: graph.KindFunction, FilePath: "repoB/lib.go", Language: "go", RepoPrefix: "repoB"})
|
|
for n := range []string{"p/handler.go::Handle", "p/service.go::Process", "p/repo.go::Fetch", "p/script.py::main"} {
|
|
_ = n
|
|
}
|
|
// Mark our existing handler node with a RepoPrefix so the
|
|
// cross-repo edge has named endpoints.
|
|
hn := s.graph.GetNode("p/handler.go::Handle")
|
|
hn.RepoPrefix = "repoA"
|
|
s.graph.AddEdge(&graph.Edge{
|
|
From: "p/handler.go::Handle",
|
|
To: "repoB/lib.go::Util",
|
|
Kind: graph.EdgeCrossRepoCalls,
|
|
})
|
|
|
|
out := callArchitectureHandler(t, s, map[string]any{})
|
|
cross, _ := out["cross_repo"].([]any)
|
|
require.Len(t, cross, 1)
|
|
row := cross[0].(map[string]any)
|
|
assert.Equal(t, "cross_repo_calls", row["kind"])
|
|
assert.Equal(t, "repoA", row["from_repo"])
|
|
assert.Equal(t, "repoB", row["to_repo"])
|
|
assert.EqualValues(t, 1, row["count"].(float64))
|
|
}
|
|
|
|
func TestArchitecture_ContractsRollup(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
reg := contracts.NewRegistry()
|
|
reg.Add(contracts.Contract{
|
|
ID: "http:GET:/api/users", Type: contracts.ContractType("http"), Role: contracts.RoleProvider,
|
|
SymbolID: "p/handler.go::Handle", FilePath: "p/handler.go", RepoPrefix: "repoA",
|
|
})
|
|
reg.Add(contracts.Contract{
|
|
ID: "http:GET:/api/users", Type: contracts.ContractType("http"), Role: contracts.RoleConsumer,
|
|
SymbolID: "p/client.go::Call", FilePath: "p/client.go", RepoPrefix: "repoB",
|
|
})
|
|
s.contractRegistry = reg
|
|
|
|
out := callArchitectureHandler(t, s, map[string]any{})
|
|
contractsSection := out["contracts"].(map[string]any)
|
|
assert.EqualValues(t, 2, contractsSection["total"].(float64))
|
|
byType := contractsSection["by_type"].(map[string]any)
|
|
assert.EqualValues(t, 2, byType["http"].(float64))
|
|
byRole := contractsSection["by_role"].(map[string]any)
|
|
assert.EqualValues(t, 1, byRole["provider"].(float64))
|
|
assert.EqualValues(t, 1, byRole["consumer"].(float64))
|
|
}
|
|
|
|
func TestArchitecture_PathPrefixFilter(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
// Add an extra file outside p/.
|
|
s.graph.AddNode(&graph.Node{ID: "vendor/x.go::X", Name: "X", Kind: graph.KindFunction, FilePath: "vendor/x.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "vendor/x.go::X", To: "p/repo.go::Fetch", Kind: graph.EdgeCalls})
|
|
|
|
out := callArchitectureHandler(t, s, map[string]any{"path_prefix": "p/"})
|
|
summary := out["summary"].(map[string]any)
|
|
// vendor/x.go is excluded; only the 4 original p/ nodes.
|
|
assert.EqualValues(t, 4, summary["total_nodes"].(float64))
|
|
}
|
|
|
|
func TestArchitecture_EmptyGraphIsSafe(t *testing.T) {
|
|
s := &Server{
|
|
graph: graph.New(),
|
|
session: newSessionState(),
|
|
tokenStats: &tokenStats{},
|
|
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
|
|
sessions: newSessionMap(),
|
|
toolScopes: newScopeRegistry(),
|
|
}
|
|
out := callArchitectureHandler(t, s, map[string]any{})
|
|
|
|
// Every section should be present and empty/zero.
|
|
for _, key := range []string{"summary", "communities", "hotspots", "entry_points", "processes", "cross_repo", "contracts"} {
|
|
_, ok := out[key]
|
|
assert.True(t, ok, "section %q must be present even on empty graph", key)
|
|
}
|
|
}
|
|
|
|
func TestArchitecture_TopCommunitiesCap(t *testing.T) {
|
|
s := newArchitectureTestServer(t)
|
|
comms := []analysis.Community{}
|
|
for i := range 10 {
|
|
comms = append(comms, analysis.Community{
|
|
ID: "c" + string(rune('A'+i)),
|
|
Label: "c" + string(rune('A'+i)),
|
|
Size: 10 - i,
|
|
Members: []string{"p/handler.go::Handle"},
|
|
Files: []string{"p/handler.go"},
|
|
})
|
|
}
|
|
s.analysisMu.Lock()
|
|
s.communities = &analysis.CommunityResult{Communities: comms}
|
|
s.analysisMu.Unlock()
|
|
|
|
out := callArchitectureHandler(t, s, map[string]any{"top_communities": 3})
|
|
top := out["communities"].(map[string]any)["top"].([]any)
|
|
assert.Len(t, top, 3, "cap honored")
|
|
}
|