Files
zzet--gortex/internal/mcp/tools_analyze_framework_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

310 lines
12 KiB
Go

package mcp
import (
"context"
"encoding/json"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/zzet/gortex/internal/graph"
)
func callAnalyzeFramework(t *testing.T, srv *Server, kind string, args map[string]any) map[string]any {
t.Helper()
args["kind"] = kind
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 addContractNode(g graph.Store, id, ctype string, meta map[string]any) {
full := map[string]any{"type": ctype, "role": "provider"}
for k, v := range meta {
full[k] = v
}
g.AddNode(&graph.Node{
ID: id, Kind: graph.KindContract, Name: id, Language: "contract", Meta: full,
})
}
func addHandlesRouteEdge(g graph.Store, from, to, file string, line int) {
g.AddEdge(&graph.Edge{
From: from, To: to, Kind: graph.EdgeHandlesRoute,
FilePath: file, Line: line,
})
}
func TestAnalyzeRoutes_BasicHTTPListing(t *testing.T) {
srv, _ := setupTestServer(t)
addContractNode(srv.graph, "http::GET::/v1/users", "http", map[string]any{"method": "GET", "path": "/v1/users"})
addContractNode(srv.graph, "http::POST::/v1/users", "http", map[string]any{"method": "POST", "path": "/v1/users"})
addHandlesRouteEdge(srv.graph, "handlers/users.go::List", "http::GET::/v1/users", "handlers/users.go", 12)
addHandlesRouteEdge(srv.graph, "handlers/users.go::Create", "http::POST::/v1/users", "handlers/users.go", 24)
out := callAnalyzeFramework(t, srv, "routes", map[string]any{})
rows, _ := out["routes"].([]any)
if len(rows) != 2 {
t.Fatalf("expected 2 route rows, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["method"] != "GET" || first["path"] != "/v1/users" {
t.Errorf("expected GET /v1/users first, got %v %v", first["method"], first["path"])
}
}
func TestAnalyzeRoutes_FilterByMethod(t *testing.T) {
srv, _ := setupTestServer(t)
addContractNode(srv.graph, "http::GET::/x", "http", map[string]any{"method": "GET", "path": "/x"})
addContractNode(srv.graph, "http::POST::/y", "http", map[string]any{"method": "POST", "path": "/y"})
addHandlesRouteEdge(srv.graph, "h.go::A", "http::GET::/x", "h.go", 1)
addHandlesRouteEdge(srv.graph, "h.go::B", "http::POST::/y", "h.go", 5)
out := callAnalyzeFramework(t, srv, "routes", map[string]any{"method": "POST"})
rows, _ := out["routes"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 route after method=POST filter, got %d", len(rows))
}
}
func TestAnalyzeRoutes_FilterByKind(t *testing.T) {
srv, _ := setupTestServer(t)
addContractNode(srv.graph, "http::GET::/x", "http", map[string]any{"method": "GET", "path": "/x"})
addContractNode(srv.graph, "grpc::Foo/Bar", "grpc", map[string]any{"service": "Foo", "method": "Bar"})
addHandlesRouteEdge(srv.graph, "h.go::A", "http::GET::/x", "h.go", 1)
addHandlesRouteEdge(srv.graph, "g.go::B", "grpc::Foo/Bar", "g.go", 1)
out := callAnalyzeFramework(t, srv, "routes", map[string]any{"type": "grpc"})
rows, _ := out["routes"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 grpc route, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["kind"] != "grpc" {
t.Errorf("filter leaked: %v", first["kind"])
}
}
func addModelTableEdge(g graph.Store, from, to, orm, table, derivation string) {
g.AddNode(&graph.Node{ID: to, Kind: graph.KindTable, Name: table, Language: "go", Meta: map[string]any{"dialect": "orm"}})
g.AddEdge(&graph.Edge{
From: from, To: to, Kind: graph.EdgeModelsTable,
Meta: map[string]any{"orm": orm, "table_name": table, "derivation": derivation},
})
}
func TestAnalyzeModels_GroupsByORM(t *testing.T) {
srv, _ := setupTestServer(t)
srv.graph.AddNode(&graph.Node{ID: "model.go::User", Kind: graph.KindType, Name: "User", Language: "go"})
srv.graph.AddNode(&graph.Node{ID: "models/order.py::Order", Kind: graph.KindType, Name: "Order", Language: "python"})
addModelTableEdge(srv.graph, "model.go::User", "db::orm::users", "gorm", "users", "convention")
addModelTableEdge(srv.graph, "models/order.py::Order", "db::orm::orders", "sqlalchemy", "orders", "override")
out := callAnalyzeFramework(t, srv, "models", map[string]any{})
rows, _ := out["models"].([]any)
if len(rows) != 2 {
t.Fatalf("expected 2 models, got %d", len(rows))
}
}
func TestAnalyzeModels_FilterByORM(t *testing.T) {
srv, _ := setupTestServer(t)
srv.graph.AddNode(&graph.Node{ID: "a.go::A", Kind: graph.KindType, Name: "A", Language: "go"})
srv.graph.AddNode(&graph.Node{ID: "b.py::B", Kind: graph.KindType, Name: "B", Language: "python"})
addModelTableEdge(srv.graph, "a.go::A", "db::orm::as", "gorm", "as", "convention")
addModelTableEdge(srv.graph, "b.py::B", "db::orm::bs", "sqlalchemy", "bs", "convention")
out := callAnalyzeFramework(t, srv, "models", map[string]any{"orm": "sqlalchemy"})
rows, _ := out["models"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 sqlalchemy model, got %d", len(rows))
}
}
func TestAnalyzeModels_FilterByTableSubstring(t *testing.T) {
srv, _ := setupTestServer(t)
srv.graph.AddNode(&graph.Node{ID: "a.go::A", Kind: graph.KindType, Name: "A", Language: "go"})
srv.graph.AddNode(&graph.Node{ID: "b.go::B", Kind: graph.KindType, Name: "B", Language: "go"})
addModelTableEdge(srv.graph, "a.go::A", "db::orm::orders", "gorm", "orders", "convention")
addModelTableEdge(srv.graph, "b.go::B", "db::orm::users", "gorm", "users", "convention")
out := callAnalyzeFramework(t, srv, "models", map[string]any{"table": "ord"})
rows, _ := out["models"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 model after table=ord, got %d", len(rows))
}
first := rows[0].(map[string]any)
if first["table"] != "orders" {
t.Errorf("filter leaked: %v", first["table"])
}
}
func addRendersChildEdge(g graph.Store, from, to, name string, line int) {
g.AddEdge(&graph.Edge{
From: from, To: to, Kind: graph.EdgeRendersChild,
Line: line,
Meta: map[string]any{"child_name": name},
})
}
func TestAnalyzeComponents_RollupSortsByTotal(t *testing.T) {
srv, _ := setupTestServer(t)
srv.graph.AddNode(&graph.Node{ID: "src/Page.tsx::Page", Kind: graph.KindFunction, Name: "Page", Language: "typescript"})
srv.graph.AddNode(&graph.Node{ID: "src/Card.tsx::Card", Kind: graph.KindFunction, Name: "Card", Language: "typescript"})
srv.graph.AddNode(&graph.Node{ID: "src/Button.tsx::Button", Kind: graph.KindFunction, Name: "Button", Language: "typescript"})
addRendersChildEdge(srv.graph, "src/Page.tsx::Page", "src/Card.tsx::Card", "Card", 5)
addRendersChildEdge(srv.graph, "src/Page.tsx::Page", "src/Button.tsx::Button", "Button", 6)
addRendersChildEdge(srv.graph, "src/Card.tsx::Card", "src/Button.tsx::Button", "Button", 4)
out := callAnalyzeFramework(t, srv, "components", map[string]any{})
rows, _ := out["components"].([]any)
if len(rows) < 3 {
t.Fatalf("expected ≥3 component rows, got %d", len(rows))
}
// Button has fan-in 2 + fan-out 0 = 2; Card has 1 + 1 = 2; Page 0 + 2 = 2.
// Each row contributes its own sum, sort ties break by name. Just
// verify Button is present with correct fan_in.
for _, r := range rows {
row := r.(map[string]any)
if row["name"] == "Button" {
if int(row["fan_in"].(float64)) != 2 {
t.Errorf("Button fan_in=%v, expected 2", row["fan_in"])
}
return
}
}
t.Fatalf("expected Button in rollup")
}
func TestAnalyzeComponents_PerComponentChildren(t *testing.T) {
srv, _ := setupTestServer(t)
srv.graph.AddNode(&graph.Node{ID: "src/Page.tsx::Page", Kind: graph.KindFunction, Name: "Page", Language: "typescript"})
addRendersChildEdge(srv.graph, "src/Page.tsx::Page", "src/Card.tsx::Card", "Card", 5)
addRendersChildEdge(srv.graph, "src/Page.tsx::Page", "unresolved::Header", "Header", 7)
out := callAnalyzeFramework(t, srv, "components", map[string]any{
"id": "src/Page.tsx::Page",
})
if out["parent"] != "src/Page.tsx::Page" {
t.Errorf("expected parent=Page, got %v", out["parent"])
}
rows, _ := out["children"].([]any)
if len(rows) != 2 {
t.Fatalf("expected 2 children, got %d", len(rows))
}
for _, r := range rows {
row := r.(map[string]any)
if row["name"] == "Header" && row["resolved"] != false {
t.Errorf("Header is unresolved::, expected resolved=false, got %v", row["resolved"])
}
if row["name"] == "Card" && row["resolved"] != true {
t.Errorf("Card has real target, expected resolved=true")
}
}
}
func TestAnalyzeComponents_EmptyOnNoEdges(t *testing.T) {
srv, _ := setupTestServer(t)
out := callAnalyzeFramework(t, srv, "components", map[string]any{})
rows, _ := out["components"].([]any)
if len(rows) != 0 {
t.Fatalf("expected 0 components, got %d", len(rows))
}
}
func addDbtModelNode(g graph.Store, id, name, framework, resourceType, materialized string) {
g.AddNode(&graph.Node{
ID: id, Kind: graph.KindTable, Name: name, Language: "sql",
FilePath: name + ".sql", StartLine: 1,
Meta: map[string]any{
"framework": framework, "resource_type": resourceType,
"materialized": materialized,
},
})
}
func addDbtColumn(g graph.Store, modelID, col string) {
colID := modelID + "::" + col
g.AddNode(&graph.Node{ID: colID, Kind: graph.KindColumn, Name: col, Language: "sql"})
g.AddEdge(&graph.Edge{From: colID, To: modelID, Kind: graph.EdgeMemberOf})
}
func TestAnalyzeDbtModels_ListingAndCounts(t *testing.T) {
srv, _ := setupTestServer(t)
g := srv.graph
addDbtModelNode(g, "dbt::model::stg_orders", "stg_orders", "dbt", "model", "view")
addDbtModelNode(g, "dbt::model::dim_orders", "dim_orders", "dbt", "model", "table")
addDbtModelNode(g, "dbt::source::raw.orders", "raw.orders", "dbt", "source", "")
addDbtModelNode(g, "sqlmesh::model::sushi.customers", "customers", "sqlmesh", "model", "full")
addDbtColumn(g, "dbt::model::stg_orders", "order_id")
addDbtColumn(g, "dbt::model::stg_orders", "total")
addDbtColumn(g, "dbt::model::dim_orders", "order_id")
// Lineage: dim_orders depends on stg_orders depends on raw.orders.
g.AddEdge(&graph.Edge{From: "dbt::model::dim_orders", To: "dbt::model::stg_orders", Kind: graph.EdgeDependsOn})
g.AddEdge(&graph.Edge{From: "dbt::model::stg_orders", To: "dbt::source::raw.orders", Kind: graph.EdgeDependsOn})
out := callAnalyzeFramework(t, srv, "dbt_models", map[string]any{})
rows, _ := out["dbt_models"].([]any)
if len(rows) != 4 {
t.Fatalf("expected 4 dbt model rows, got %d", len(rows))
}
byName := map[string]map[string]any{}
for _, r := range rows {
m := r.(map[string]any)
byName[m["name"].(string)] = m
}
stg := byName["stg_orders"]
if stg["columns"].(float64) != 2 {
t.Errorf("stg_orders columns = %v, want 2", stg["columns"])
}
if stg["upstream"].(float64) != 1 {
t.Errorf("stg_orders upstream = %v, want 1", stg["upstream"])
}
if stg["downstream"].(float64) != 1 {
t.Errorf("stg_orders downstream = %v, want 1", stg["downstream"])
}
}
func TestAnalyzeDbtModels_FilterByFramework(t *testing.T) {
srv, _ := setupTestServer(t)
g := srv.graph
addDbtModelNode(g, "dbt::model::a", "a", "dbt", "model", "view")
addDbtModelNode(g, "sqlmesh::model::b", "b", "sqlmesh", "model", "full")
out := callAnalyzeFramework(t, srv, "dbt_models", map[string]any{"framework": "sqlmesh"})
rows, _ := out["dbt_models"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 row after framework=sqlmesh, got %d", len(rows))
}
if rows[0].(map[string]any)["name"] != "b" {
t.Errorf("expected sqlmesh model b, got %v", rows[0])
}
}
func TestAnalyzeDbtModels_FilterByType(t *testing.T) {
srv, _ := setupTestServer(t)
g := srv.graph
addDbtModelNode(g, "dbt::model::a", "a", "dbt", "model", "view")
addDbtModelNode(g, "dbt::source::raw.b", "raw.b", "dbt", "source", "")
out := callAnalyzeFramework(t, srv, "dbt_models", map[string]any{"type": "source"})
rows, _ := out["dbt_models"].([]any)
if len(rows) != 1 {
t.Fatalf("expected 1 source row, got %d", len(rows))
}
}