Files
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

182 lines
6.4 KiB
Go

package mcp
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strconv"
"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"
)
// prRiskTestServer builds a server over a synthetic graph: a security-sensitive
// hub function with many inbound callers and no covering test.
func prRiskTestServer(t *testing.T) (*Server, string) {
t.Helper()
g := graph.New()
hubID := "internal/auth/login.go::ValidateToken"
g.AddNode(&graph.Node{ID: hubID, Kind: graph.KindFunction, Name: "ValidateToken", FilePath: "internal/auth/login.go", StartLine: 1, EndLine: 10})
for i := 0; i < 12; i++ {
cid := "pkg/c.go::caller" + strconv.Itoa(i)
g.AddNode(&graph.Node{ID: cid, Kind: graph.KindFunction, Name: "caller" + strconv.Itoa(i), FilePath: "pkg/c.go"})
g.AddEdge(&graph.Edge{From: cid, To: hubID, Kind: graph.EdgeCalls})
}
srv := NewServer(query.NewEngine(g), g, nil, nil, zap.NewNop(), nil)
return srv, hubID
}
func callPRRisk(t *testing.T, srv *Server, args map[string]any) *mcplib.CallToolResult {
t.Helper()
req := mcplib.CallToolRequest{}
req.Params.Name = "pr_risk"
req.Params.Arguments = args
res, err := srv.handlePRRisk(t.Context(), req)
require.NoError(t, err)
return res
}
func TestPRRisk_IDsPath(t *testing.T) {
srv, hubID := prRiskTestServer(t)
res := callPRRisk(t, srv, map[string]any{"ids": hubID})
require.False(t, res.IsError, "errored: %v", res)
var out struct {
Score float64 `json:"score"`
Risk string `json:"risk"`
ReviewPriorities []struct {
Axis string `json:"axis"`
Score float64 `json:"score"`
Reason string `json:"reason"`
} `json:"review_priorities"`
TotalAffected int `json:"total_affected"`
UncoveredSymbols int `json:"uncovered_symbols"`
CommunitySpan int `json:"community_span"`
SecurityHits []string `json:"security_hits"`
ChangedSymbols int `json:"changed_symbols"`
}
require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &out))
// Score is a 0-100 composite.
require.GreaterOrEqual(t, out.Score, 0.0)
require.LessOrEqual(t, out.Score, 100.0)
// Risk label matches the impactRisk thresholds for the score.
require.Equal(t, impactRisk(out.Score), out.Risk)
// review_priorities non-empty and sorted descending by score.
require.NotEmpty(t, out.ReviewPriorities)
for i := 1; i < len(out.ReviewPriorities); i++ {
require.GreaterOrEqual(t, out.ReviewPriorities[i-1].Score, out.ReviewPriorities[i].Score,
"review_priorities must be sorted descending")
}
// A security path + 12-caller untested hub is at least HIGH.
require.Contains(t, []string{"HIGH", "CRITICAL"}, out.Risk)
require.Contains(t, out.SecurityHits, "auth")
require.Equal(t, 1, out.ChangedSymbols)
}
func TestPRRisk_RequiresInput(t *testing.T) {
srv, _ := prRiskTestServer(t)
res := callPRRisk(t, srv, map[string]any{})
require.True(t, res.IsError, "expected error when neither ids nor base is given")
}
func TestPRRisk_GCXAndTOONAndBudget(t *testing.T) {
srv, hubID := prRiskTestServer(t)
// GCX round-trip: the section headers must appear.
gcx := callPRRisk(t, srv, map[string]any{"ids": hubID, "format": "gcx"})
require.False(t, gcx.IsError)
gtext := gcx.Content[0].(mcplib.TextContent).Text
require.Contains(t, gtext, "pr_risk.summary")
require.Contains(t, gtext, "pr_risk.priorities")
// max_bytes budget is honoured (response stays bounded).
budgeted := callPRRisk(t, srv, map[string]any{"ids": hubID, "format": "gcx", "max_bytes": float64(120)})
require.False(t, budgeted.IsError)
require.LessOrEqual(t, len(budgeted.Content[0].(mcplib.TextContent).Text), 400)
// TOON round-trip: still carries the score key.
toon := callPRRisk(t, srv, map[string]any{"ids": hubID, "format": "toon"})
require.False(t, toon.IsError)
require.Contains(t, toon.Content[0].(mcplib.TextContent).Text, "score")
}
// prRiskGitRepo creates a git repo with a base commit on `main` and a HEAD
// commit that mutates a security-sensitive file, then returns the root.
func prRiskGitRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
run := func(args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = dir
cmd.Env = append(os.Environ(),
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t",
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t",
)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
}
run("init", "-b", "main")
run("config", "user.email", "t@t")
run("config", "user.name", "t")
run("config", "diff.mnemonicPrefix", "false")
run("config", "diff.noprefix", "false")
authDir := filepath.Join(dir, "internal", "auth")
require.NoError(t, os.MkdirAll(authDir, 0o755))
src := "package auth\n\nfunc ValidateToken() int {\n\treturn 1\n}\n"
require.NoError(t, os.WriteFile(filepath.Join(authDir, "login.go"), []byte(src), 0o644))
run("add", ".")
run("commit", "-m", "base")
// Tag the base commit so the diff has a ref that is strictly behind HEAD
// — a `base...HEAD` against the current branch tip would be empty.
run("tag", "base-ref")
// HEAD commit mutates the body inside ValidateToken.
mutated := "package auth\n\nfunc ValidateToken() int {\n\tx := 1\n\treturn x\n}\n"
require.NoError(t, os.WriteFile(filepath.Join(authDir, "login.go"), []byte(mutated), 0o644))
run("add", ".")
run("commit", "-m", "change")
return dir
}
func TestPRRisk_BasePath(t *testing.T) {
dir := prRiskGitRepo(t)
g := graph.New()
reg := testRegistry()
idx := indexer.New(g, reg, config.Default().Index, zap.NewNop())
_, err := idx.Index(dir)
require.NoError(t, err)
srv := NewServer(query.NewEngine(g), g, idx, nil, zap.NewNop(), nil)
srv.RunAnalysis()
res := callPRRisk(t, srv, map[string]any{"base": "base-ref"})
require.False(t, res.IsError, "errored: %v", res)
var out struct {
Risk string `json:"risk"`
SecurityHits []string `json:"security_hits"`
ChangedSymbols int `json:"changed_symbols"`
}
require.NoError(t, json.Unmarshal([]byte(res.Content[0].(mcplib.TextContent).Text), &out))
// The base diff derived at least one changed symbol (ValidateToken),
// and the security axis picked up the auth path.
require.GreaterOrEqual(t, out.ChangedSymbols, 1, "expected the base diff to map a changed symbol")
require.Contains(t, out.SecurityHits, "auth")
}