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
173 lines
6.1 KiB
Go
173 lines
6.1 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/graph"
|
|
)
|
|
|
|
// newArchHierarchyTestServer builds a server over a two-package fixture
|
|
// graph with cached communities, so get_architecture can be exercised
|
|
// with a resolution tier.
|
|
//
|
|
// pkg/auth/login.go :: Login, Logout (community-0)
|
|
// pkg/store/db.go :: Save, Load (community-1)
|
|
//
|
|
// auth -> store carries 3 underlying call edges.
|
|
func newArchHierarchyTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
g := graph.New()
|
|
add := func(id, file string) {
|
|
g.AddNode(&graph.Node{ID: id, Name: id, Kind: graph.KindFunction, FilePath: file, Language: "go", RepoPrefix: "demo"})
|
|
}
|
|
add("Login", "pkg/auth/login.go")
|
|
add("Logout", "pkg/auth/login.go")
|
|
add("Save", "pkg/store/db.go")
|
|
add("Load", "pkg/store/db.go")
|
|
g.AddEdge(&graph.Edge{From: "Login", To: "Logout", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "Login", To: "Save", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "Login", To: "Load", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "Logout", To: "Save", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "Save", To: "Load", Kind: graph.EdgeCalls})
|
|
|
|
s := &Server{
|
|
graph: g,
|
|
session: newSessionState(),
|
|
tokenStats: &tokenStats{},
|
|
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
|
|
sessions: newSessionMap(),
|
|
toolScopes: newScopeRegistry(),
|
|
}
|
|
s.analysisMu.Lock()
|
|
s.communities = &analysis.CommunityResult{
|
|
Communities: []analysis.Community{
|
|
{ID: "community-0", Label: "auth", Members: []string{"Login", "Logout"}, Size: 2},
|
|
{ID: "community-1", Label: "storage", Members: []string{"Save", "Load"}, Size: 2},
|
|
},
|
|
NodeToComm: map[string]string{
|
|
"Login": "community-0", "Logout": "community-0",
|
|
"Save": "community-1", "Load": "community-1",
|
|
},
|
|
}
|
|
s.analysisMu.Unlock()
|
|
return s
|
|
}
|
|
|
|
func callGetArchitecture(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)
|
|
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_NoResolutionOmitsHierarchy(t *testing.T) {
|
|
s := newArchHierarchyTestServer(t)
|
|
out := callGetArchitecture(t, s, map[string]any{})
|
|
_, present := out["hierarchy"]
|
|
assert.False(t, present, "hierarchy block must be absent when no resolution is requested")
|
|
}
|
|
|
|
func TestArchitecture_PackageResolution(t *testing.T) {
|
|
s := newArchHierarchyTestServer(t)
|
|
out := callGetArchitecture(t, s, map[string]any{"resolution": "package"})
|
|
|
|
h, ok := out["hierarchy"].(map[string]any)
|
|
require.True(t, ok, "hierarchy block must be present for resolution=package")
|
|
assert.Equal(t, "package", h["level"])
|
|
assert.EqualValues(t, 4, h["leaf_count"])
|
|
assert.EqualValues(t, 2, h["node_count"])
|
|
|
|
nodes, _ := h["nodes"].([]any)
|
|
require.Len(t, nodes, 2)
|
|
ids := map[string]int{}
|
|
for _, n := range nodes {
|
|
row := n.(map[string]any)
|
|
ids[row["id"].(string)] = int(row["leaf_count"].(float64))
|
|
// No function-leaf node IDs in the rollup.
|
|
assert.Nil(t, s.graph.GetNode(row["id"].(string)),
|
|
"rollup node %q must not be a base-graph leaf", row["id"])
|
|
}
|
|
assert.Equal(t, 2, ids["package:pkg/auth"])
|
|
assert.Equal(t, 2, ids["package:pkg/store"])
|
|
|
|
edges, _ := h["edges"].([]any)
|
|
require.Len(t, edges, 1)
|
|
edge := edges[0].(map[string]any)
|
|
assert.Equal(t, "package:pkg/auth", edge["from"])
|
|
assert.Equal(t, "package:pkg/store", edge["to"])
|
|
// Weight = count of underlying function-level call edges crossing
|
|
// the two packages (Login->Save, Login->Load, Logout->Save).
|
|
assert.EqualValues(t, 3, edge["weight"])
|
|
|
|
selfLoops, _ := h["self_loops"].(map[string]any)
|
|
assert.EqualValues(t, 1, selfLoops["package:pkg/auth"])
|
|
assert.EqualValues(t, 1, selfLoops["package:pkg/store"])
|
|
}
|
|
|
|
func TestArchitecture_ServiceResolution(t *testing.T) {
|
|
s := newArchHierarchyTestServer(t)
|
|
out := callGetArchitecture(t, s, map[string]any{"resolution": "service"})
|
|
|
|
h, ok := out["hierarchy"].(map[string]any)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "service", h["level"])
|
|
|
|
nodes, _ := h["nodes"].([]any)
|
|
require.Len(t, nodes, 2, "service tier groups the two communities")
|
|
for _, n := range nodes {
|
|
row := n.(map[string]any)
|
|
assert.Contains(t, []any{"service:community-0", "service:community-1"}, row["id"])
|
|
}
|
|
|
|
edges, _ := h["edges"].([]any)
|
|
require.Len(t, edges, 1)
|
|
edge := edges[0].(map[string]any)
|
|
assert.EqualValues(t, 3, edge["weight"],
|
|
"a module->module edge weight equals the count of underlying function-level call edges")
|
|
}
|
|
|
|
func TestArchitecture_SystemResolution(t *testing.T) {
|
|
s := newArchHierarchyTestServer(t)
|
|
out := callGetArchitecture(t, s, map[string]any{"resolution": "system"})
|
|
h := out["hierarchy"].(map[string]any)
|
|
assert.Equal(t, "system", h["level"])
|
|
nodes, _ := h["nodes"].([]any)
|
|
require.Len(t, nodes, 1, "single-repo fixture rolls up to one system node")
|
|
assert.Equal(t, "system:demo", nodes[0].(map[string]any)["id"])
|
|
}
|
|
|
|
func TestArchitecture_UnknownResolutionIsError(t *testing.T) {
|
|
s := newArchHierarchyTestServer(t)
|
|
req := mcp.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"resolution": "galaxy"}
|
|
res, err := s.handleGetArchitecture(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.True(t, res.IsError, "an unknown resolution tier must return a clean error")
|
|
}
|
|
|
|
func TestArchitecture_HierarchyDeterministic(t *testing.T) {
|
|
s := newArchHierarchyTestServer(t)
|
|
first := callGetArchitecture(t, s, map[string]any{"resolution": "package"})
|
|
firstJSON, _ := json.Marshal(first["hierarchy"])
|
|
for i := 0; i < 5; i++ {
|
|
again := callGetArchitecture(t, s, map[string]any{"resolution": "package"})
|
|
againJSON, _ := json.Marshal(again["hierarchy"])
|
|
assert.JSONEq(t, string(firstJSON), string(againJSON),
|
|
"the hierarchy rollup must be stable across runs")
|
|
}
|
|
}
|