Files
zzet--gortex/internal/mcp/tools_dataflow_test.go
wehub-resource-sync a06f331eb8
install-script / powershell-syntax (push) Waiting to run
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
install-script / install (macos-14) (push) Waiting to run
install-script / install (ubuntu-latest) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

280 lines
7.9 KiB
Go

package mcp
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"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"
)
// dataflowTestServer indexes a single Go file with a known
// source-mid-sink shape so we can exercise the MCP tools end-to-end
// without depending on the larger fixture used by setupTestServer.
func dataflowTestServer(t *testing.T) *Server {
t.Helper()
dir := t.TempDir()
src := `package main
func Source(seed string) string {
return seed
}
func Mid(s string) string {
v := s
return v
}
func Sink(payload string) {}
func Driver(input string) {
a := Source(input)
b := Mid(a)
Sink(b)
}
`
require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644))
g := graph.New()
reg := testRegistry()
cfg := config.Default()
idx := indexer.New(g, reg, cfg.Index, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll()
eng := query.NewEngine(g)
srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil)
return srv
}
// findFunctionID walks the graph for a function/method node by
// name and returns its ID (the prefix is repo-dependent in our
// indexer, so we discover it via the live graph).
func findFunctionID(t *testing.T, srv *Server, name string) string {
t.Helper()
for _, n := range srv.graph.AllNodes() {
if n.Kind != graph.KindFunction && n.Kind != graph.KindMethod {
continue
}
if n.Name == name {
return n.ID
}
}
t.Fatalf("function %q not found", name)
return ""
}
func TestHandleFlowBetween_DirectPath(t *testing.T) {
srv := dataflowTestServer(t)
driverID := findFunctionID(t, srv, "Driver")
req := mcplib.CallToolRequest{}
req.Params.Arguments = map[string]any{
"source_id": driverID + "#param:input",
"sink_id": findFunctionID(t, srv, "Sink") + "#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 {
Total int `json:"total"`
Paths []struct {
IDs []string `json:"ids"`
Edges []struct {
Kind string `json:"kind"`
} `json:"edges"`
} `json:"paths"`
}
text := res.Content[0].(mcplib.TextContent).Text
require.NoError(t, json.Unmarshal([]byte(text), &payload))
require.Greater(t, payload.Total, 0, "expected at least one path; got: %s", text)
// Path must traverse Source / Mid → Sink param.
first := payload.Paths[0]
if !contains(first.IDs, "Source") {
t.Logf("path does not reference Source by short name (this is fine: IDs are full): %v", first.IDs)
}
}
func TestHandleFlowBetween_MissingSource(t *testing.T) {
srv := dataflowTestServer(t)
req := mcplib.CallToolRequest{}
req.Params.Arguments = map[string]any{
"sink_id": "anything",
}
res, err := srv.handleFlowBetween(t.Context(), req)
require.NoError(t, err)
require.True(t, res.IsError, "expected error result for missing source_id")
}
func TestHandleFlowBetween_GCXFormat(t *testing.T) {
srv := dataflowTestServer(t)
driverID := findFunctionID(t, srv, "Driver")
req := mcplib.CallToolRequest{}
req.Params.Name = "flow_between"
req.Params.Arguments = map[string]any{
"source_id": driverID + "#param:input",
"sink_id": findFunctionID(t, srv, "Sink") + "#param:payload",
"format": "gcx",
}
res, err := srv.handleFlowBetween(t.Context(), req)
require.NoError(t, err)
require.False(t, res.IsError)
text := res.Content[0].(mcplib.TextContent).Text
require.Contains(t, text, "flow_between.summary")
require.Contains(t, text, "flow_between.paths")
}
func TestHandleTaintPaths_PatternMatch(t *testing.T) {
srv := dataflowTestServer(t)
req := mcplib.CallToolRequest{}
req.Params.Arguments = map[string]any{
"source_pattern": "name:Source",
"sink_pattern": "name:Sink",
"max_depth": float64(10),
}
res, err := srv.handleTaintPaths(t.Context(), req)
require.NoError(t, err)
require.False(t, res.IsError, "errored: %v", res)
text := res.Content[0].(mcplib.TextContent).Text
require.Contains(t, text, "findings")
var payload struct {
Total int `json:"total"`
Findings []map[string]any `json:"findings"`
}
require.NoError(t, json.Unmarshal([]byte(text), &payload))
require.Greater(t, payload.Total, 0, "expected at least one finding; got %s", text)
}
func TestHandleTaintPaths_EmptyPattern(t *testing.T) {
srv := dataflowTestServer(t)
req := mcplib.CallToolRequest{}
req.Params.Arguments = map[string]any{
"source_pattern": "",
"sink_pattern": "name:Sink",
}
res, err := srv.handleTaintPaths(t.Context(), req)
require.NoError(t, err)
require.True(t, res.IsError, "expected error for empty source_pattern")
}
// TestHandleTaintPaths_E2EMultiFile builds a more realistic
// multi-file scenario: `os/Getenv`-shaped source returning a
// secret, threaded through a couple of helpers, and ultimately
// passed to a sink simulating `db.Query`. Verifies that
// flow_between → returns_to → arg_of compose into a path the
// agent can recover via `name:` patterns.
func TestHandleTaintPaths_E2EMultiFile(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "secret.go"), []byte(`package main
func Getenv(key string) string {
return key
}
`), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "format.go"), []byte(`package main
func Format(secret string) string {
out := secret
return out
}
`), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "db.go"), []byte(`package main
func DBQuery(sql string) {}
`), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "driver.go"), []byte(`package main
func Driver(name string) {
secret := Getenv(name)
sql := Format(secret)
DBQuery(sql)
}
`), 0o644))
g := graph.New()
reg := testRegistry()
cfg := config.Default()
idx := indexer.New(g, reg, cfg.Index, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
idx.ResolveAll()
eng := query.NewEngine(g)
srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil)
req := mcplib.CallToolRequest{}
req.Params.Arguments = map[string]any{
"source_pattern": "name:Getenv",
"sink_pattern": "name:DBQuery",
"max_depth": float64(20),
}
res, err := srv.handleTaintPaths(t.Context(), req)
require.NoError(t, err)
require.False(t, res.IsError, "tool errored: %v", res)
text := res.Content[0].(mcplib.TextContent).Text
var payload struct {
Total int `json:"total"`
Findings []struct {
Source map[string]any `json:"source"`
Sink map[string]any `json:"sink"`
Paths []struct {
IDs []string `json:"ids"`
} `json:"paths"`
} `json:"findings"`
}
require.NoError(t, json.Unmarshal([]byte(text), &payload))
require.Greater(t, payload.Total, 0, "expected E2E taint finding; got %s", text)
bestPath := payload.Findings[0].Paths[0]
if len(bestPath.IDs) < 3 {
t.Errorf("expected multi-hop path; got %v", bestPath.IDs)
}
// Path must touch a Format-related node — confirms the BFS
// crossed the intermediate function.
found := false
for _, id := range bestPath.IDs {
if strings.Contains(id, "Format") {
found = true
break
}
}
if !found {
t.Errorf("expected Format on E2E path; got %v", bestPath.IDs)
}
}
func TestHandleTaintPaths_GCXFormat(t *testing.T) {
srv := dataflowTestServer(t)
req := mcplib.CallToolRequest{}
req.Params.Name = "taint_paths"
req.Params.Arguments = map[string]any{
"source_pattern": "name:Source",
"sink_pattern": "name:Sink",
"format": "gcx",
}
res, err := srv.handleTaintPaths(t.Context(), req)
require.NoError(t, err)
require.False(t, res.IsError)
text := res.Content[0].(mcplib.TextContent).Text
require.Contains(t, text, "taint_paths.summary")
require.Contains(t, text, "taint_paths.findings")
}
func contains(haystack []string, needle string) bool {
for _, s := range haystack {
if strings.Contains(s, needle) {
return true
}
}
return false
}