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
328 lines
9.5 KiB
Go
328 lines
9.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
mcplib "github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/indexer"
|
|
"github.com/zzet/gortex/internal/query"
|
|
)
|
|
|
|
// cfgTestServer indexes one Go file with a branchy function so the
|
|
// CFG tools can be exercised end-to-end against a real graph +
|
|
// on-disk source.
|
|
func cfgTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
src := `package main
|
|
|
|
func Classify(score int) string {
|
|
label := "low"
|
|
if score > 90 {
|
|
label = "high"
|
|
} else if score > 50 {
|
|
label = "mid"
|
|
}
|
|
for i := 0; i < score; i++ {
|
|
if i == 3 {
|
|
break
|
|
}
|
|
}
|
|
return label
|
|
}
|
|
|
|
var topLevel = 1
|
|
`
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644))
|
|
g := graph.New()
|
|
reg := testRegistry()
|
|
cfgConf := config.Default()
|
|
idx := indexer.New(g, reg, cfgConf.Index, zap.NewNop())
|
|
_, err := idx.Index(dir)
|
|
require.NoError(t, err)
|
|
idx.ResolveAll()
|
|
eng := query.NewEngine(g)
|
|
return NewServer(eng, g, idx, nil, zap.NewNop(), nil)
|
|
}
|
|
|
|
func cfgFindSymbol(t *testing.T, srv *Server, name string, kinds ...graph.NodeKind) string {
|
|
t.Helper()
|
|
kindOK := func(k graph.NodeKind) bool {
|
|
if len(kinds) == 0 {
|
|
return true
|
|
}
|
|
for _, want := range kinds {
|
|
if k == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for _, n := range srv.graph.AllNodes() {
|
|
if n.Name == name && kindOK(n.Kind) {
|
|
return n.ID
|
|
}
|
|
}
|
|
t.Fatalf("symbol %q not found", name)
|
|
return ""
|
|
}
|
|
|
|
func TestHandleGetCFG_BlocksEdgesChains(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
id := cfgFindSymbol(t, srv, "Classify", graph.KindFunction)
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"id": id}
|
|
res, err := srv.handleGetCFG(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError, "tool errored: %v", res)
|
|
|
|
var payload struct {
|
|
Name string `json:"name"`
|
|
Blocks []struct {
|
|
ID int `json:"id"`
|
|
Statements []struct {
|
|
Text string `json:"text"`
|
|
Defs []string `json:"defs"`
|
|
Uses []string `json:"uses"`
|
|
} `json:"statements"`
|
|
} `json:"blocks"`
|
|
Edges []struct {
|
|
From int `json:"from"`
|
|
To int `json:"to"`
|
|
Label string `json:"label"`
|
|
} `json:"edges"`
|
|
DefUse []struct {
|
|
Stmt int `json:"stmt"`
|
|
Var string `json:"var"`
|
|
Defs []int `json:"defs"`
|
|
} `json:"def_use"`
|
|
TotalBlocks int `json:"total_blocks"`
|
|
}
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
require.NoError(t, json.Unmarshal([]byte(text), &payload))
|
|
require.Equal(t, "Classify", payload.Name)
|
|
require.Greater(t, payload.TotalBlocks, 4, "branchy function needs several blocks")
|
|
|
|
labels := map[string]bool{}
|
|
for _, e := range payload.Edges {
|
|
labels[e.Label] = true
|
|
}
|
|
for _, want := range []string{"true", "false", "loop_back", "break", "return"} {
|
|
require.True(t, labels[want], "missing %s edge in %s", want, text)
|
|
}
|
|
|
|
// The return's use of label must chain to all three defs.
|
|
var labelChain []int
|
|
for _, ch := range payload.DefUse {
|
|
if ch.Var == "label" {
|
|
labelChain = ch.Defs
|
|
}
|
|
}
|
|
require.Len(t, labelChain, 3, "label has three reaching defs at the return: %s", text)
|
|
}
|
|
|
|
func TestHandleGetCFG_Mermaid(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
id := cfgFindSymbol(t, srv, "Classify", graph.KindFunction)
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"id": id, "mermaid": true}
|
|
res, err := srv.handleGetCFG(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError)
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
var payload struct {
|
|
Mermaid string `json:"mermaid"`
|
|
}
|
|
require.NoError(t, json.Unmarshal([]byte(text), &payload))
|
|
require.Contains(t, payload.Mermaid, "flowchart TD")
|
|
require.Contains(t, payload.Mermaid, "-->|true|")
|
|
}
|
|
|
|
func TestHandleGetCFG_GCXFormat(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
id := cfgFindSymbol(t, srv, "Classify", graph.KindFunction)
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Name = "get_cfg"
|
|
req.Params.Arguments = map[string]any{"id": id, "format": "gcx", "mermaid": true}
|
|
res, err := srv.handleGetCFG(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError)
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
for _, section := range []string{"get_cfg.summary", "get_cfg.stmts", "get_cfg.edges", "get_cfg.chains", "get_cfg.mermaid"} {
|
|
require.Contains(t, text, section)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetCFG_Errors(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
|
|
// Missing id.
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{}
|
|
res, err := srv.handleGetCFG(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.True(t, res.IsError)
|
|
|
|
// Unknown symbol.
|
|
req.Params.Arguments = map[string]any{"id": "nope.go::Missing"}
|
|
res, err = srv.handleGetCFG(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.True(t, res.IsError)
|
|
|
|
// Non-function symbol.
|
|
varID := cfgFindSymbol(t, srv, "topLevel")
|
|
req.Params.Arguments = map[string]any{"id": varID}
|
|
res, err = srv.handleGetCFG(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.True(t, res.IsError, "get_cfg on a variable must error")
|
|
}
|
|
|
|
func TestHandleAnalyzeDefUse_ThroughDispatcher(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
id := cfgFindSymbol(t, srv, "Classify", graph.KindFunction)
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"kind": "def_use", "ids": id}
|
|
res, err := srv.handleAnalyze(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError, "analyze def_use errored: %v", res)
|
|
|
|
var payload struct {
|
|
Total int `json:"total"`
|
|
Symbols []struct {
|
|
ID string `json:"id"`
|
|
Error string `json:"error"`
|
|
Chains []struct {
|
|
Var string `json:"var"`
|
|
StmtLine int `json:"stmt_line"`
|
|
DefLines []int `json:"def_lines"`
|
|
} `json:"chains"`
|
|
Variables []struct {
|
|
Var string `json:"var"`
|
|
Defs int `json:"defs"`
|
|
Uses int `json:"uses"`
|
|
} `json:"variables"`
|
|
} `json:"symbols"`
|
|
}
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
require.NoError(t, json.Unmarshal([]byte(text), &payload))
|
|
require.Equal(t, 1, payload.Total)
|
|
sym := payload.Symbols[0]
|
|
require.Empty(t, sym.Error)
|
|
require.NotEmpty(t, sym.Chains)
|
|
|
|
// label's per-variable summary: 3 defs (init + two arms).
|
|
foundLabel := false
|
|
for _, v := range sym.Variables {
|
|
if v.Var == "label" {
|
|
foundLabel = true
|
|
require.Equal(t, 3, v.Defs, "label is defined three times")
|
|
require.GreaterOrEqual(t, v.Uses, 1)
|
|
}
|
|
}
|
|
require.True(t, foundLabel, "variables rollup must include label: %s", text)
|
|
}
|
|
|
|
func TestHandleAnalyzeDefUse_DegradesPerSymbol(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
good := cfgFindSymbol(t, srv, "Classify", graph.KindFunction)
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"kind": "def_use", "ids": good + ",missing.go::Nope"}
|
|
res, err := srv.handleAnalyze(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError)
|
|
|
|
var payload struct {
|
|
Symbols []struct {
|
|
ID string `json:"id"`
|
|
Error string `json:"error"`
|
|
} `json:"symbols"`
|
|
}
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
require.NoError(t, json.Unmarshal([]byte(text), &payload))
|
|
require.Len(t, payload.Symbols, 2)
|
|
require.Empty(t, payload.Symbols[0].Error)
|
|
require.NotEmpty(t, payload.Symbols[1].Error, "missing symbol must degrade to a per-symbol error")
|
|
}
|
|
|
|
func TestHandleAnalyzeDefUse_GCX(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
id := cfgFindSymbol(t, srv, "Classify", graph.KindFunction)
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Name = "analyze"
|
|
req.Params.Arguments = map[string]any{"kind": "def_use", "ids": id, "format": "gcx"}
|
|
res, err := srv.handleAnalyze(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError)
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
require.Contains(t, text, "analyze.def_use")
|
|
}
|
|
|
|
func TestHandleAnalyzeDefUse_RequiresIDs(t *testing.T) {
|
|
srv := cfgTestServer(t)
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"kind": "def_use"}
|
|
res, err := srv.handleAnalyze(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.True(t, res.IsError)
|
|
}
|
|
|
|
// TestHandleFlowBetween_RefinementLive proves the reaching-
|
|
// definitions refinement runs on the real flow_between path: the
|
|
// indexed fixture's Mid function binds `v := s`, so the
|
|
// param-s → local-v hop must come back stamped
|
|
// confirmed_intraprocedural.
|
|
func TestHandleFlowBetween_RefinementLive(t *testing.T) {
|
|
srv := dataflowTestServer(t)
|
|
driverID := findFunctionID(t, srv, "Driver")
|
|
sinkID := findFunctionID(t, srv, "Sink")
|
|
|
|
req := mcplib.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{
|
|
"source_id": driverID + "#param:input",
|
|
"sink_id": sinkID + "#param:payload",
|
|
"max_depth": float64(10),
|
|
}
|
|
res, err := srv.handleFlowBetween(t.Context(), req)
|
|
require.NoError(t, err)
|
|
require.False(t, res.IsError, "tool errored: %v", res)
|
|
|
|
var payload struct {
|
|
Paths []struct {
|
|
Edges []struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Kind string `json:"kind"`
|
|
Refined string `json:"refined"`
|
|
} `json:"edges"`
|
|
} `json:"paths"`
|
|
}
|
|
text := res.Content[0].(mcplib.TextContent).Text
|
|
require.NoError(t, json.Unmarshal([]byte(text), &payload))
|
|
require.NotEmpty(t, payload.Paths)
|
|
|
|
confirmed := 0
|
|
for _, p := range payload.Paths {
|
|
for _, e := range p.Edges {
|
|
require.NotEqual(t, "pruned", e.Refined, "fixture has no stale flows: %s -> %s", e.From, e.To)
|
|
if e.Refined == "confirmed_intraprocedural" {
|
|
confirmed++
|
|
}
|
|
}
|
|
}
|
|
require.Greater(t, confirmed, 0, "at least one same-function value_flow hop must be confirmed: %s", text)
|
|
}
|