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
310 lines
12 KiB
Go
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))
|
|
}
|
|
}
|