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

250 lines
7.0 KiB
Go

package mcp
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/zzet/gortex/internal/graph"
)
// callSearchAST is the test harness: build a request with the given
// args, invoke the MCP handler, decode the JSON result. Any tool
// error fails the test.
func callSearchAST(t *testing.T, srv *Server, args map[string]any) map[string]any {
t.Helper()
req := mcplib.CallToolRequest{}
req.Params.Name = "search_ast"
req.Params.Arguments = args
res, err := srv.handleSearchAST(context.Background(), req)
if err != nil {
t.Fatalf("handleSearchAST: %v", err)
}
if res.IsError {
t.Fatalf("error result: %+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
}
// writeTempGoFile drops fixture content into a temp dir and returns
// the absolute path. Tests register a KindFile node pointing at the
// returned path so the engine can find and parse it.
func writeTempGoFile(t *testing.T, name, src string) string {
t.Helper()
dir := t.TempDir()
abs := filepath.Join(dir, name)
if err := os.WriteFile(abs, []byte(src), 0o644); err != nil {
t.Fatalf("write fixture: %v", err)
}
return abs
}
func TestSearchAST_RawPattern_GoPanic(t *testing.T) {
srv, _ := setupTestServer(t)
abs := writeTempGoFile(t, "lib.go", `package x
func F() {
panic("boom")
}
`)
srv.graph.AddNode(&graph.Node{
ID: abs, Kind: graph.KindFile, Name: abs,
FilePath: abs, Language: "go", StartLine: 1, EndLine: 5,
})
srv.graph.AddNode(&graph.Node{
ID: abs + "::F",
Kind: graph.KindFunction,
Name: "F",
FilePath: abs,
StartLine: 3, EndLine: 5,
})
out := callSearchAST(t, srv, map[string]any{
"pattern": `((call_expression function: (identifier) @fn) @match (#eq? @fn "panic"))`,
"language": "go",
})
if got, _ := out["total"].(float64); got != 1 {
t.Fatalf("expected 1 match, got %v\n%v", got, out)
}
matches := out["matches"].([]any)
first := matches[0].(map[string]any)
if first["symbol_id"] != abs+"::F" {
t.Errorf("expected enclosing symbol_id %q, got %q", abs+"::F", first["symbol_id"])
}
if first["symbol_name"] != "F" {
t.Errorf("expected symbol_name F, got %v", first["symbol_name"])
}
}
func TestSearchAST_BundledDetector_HardcodedSecret(t *testing.T) {
srv, _ := setupTestServer(t)
abs := writeTempGoFile(t, "creds.go", `package x
func F() {
password := "hunter2hunter2hunter"
emptyDefault := ""
_ = password
_ = emptyDefault
}
`)
srv.graph.AddNode(&graph.Node{
ID: abs, Kind: graph.KindFile, Name: abs,
FilePath: abs, Language: "go", StartLine: 1, EndLine: 8,
})
out := callSearchAST(t, srv, map[string]any{
"detector": "hardcoded-secret",
})
total, _ := out["total"].(float64)
if total != 1 {
t.Fatalf("expected 1 match, got %v\n%v", total, out)
}
first := out["matches"].([]any)[0].(map[string]any)
if first["detector"] != "hardcoded-secret" {
t.Errorf("expected detector enrichment, got %v", first["detector"])
}
}
func TestSearchAST_PathPrefixFilter(t *testing.T) {
srv, _ := setupTestServer(t)
matched := writeTempGoFile(t, "match.go", `package x
func F() { panic("hit") }
`)
excluded := writeTempGoFile(t, "skip.go", `package x
func G() { panic("skip") }
`)
for _, p := range []string{matched, excluded} {
srv.graph.AddNode(&graph.Node{
ID: p, Kind: graph.KindFile, Name: p,
FilePath: p, Language: "go", StartLine: 1, EndLine: 2,
})
}
out := callSearchAST(t, srv, map[string]any{
"pattern": `((call_expression function: (identifier) @fn) @match (#eq? @fn "panic"))`,
"language": "go",
"path_prefix": filepath.Dir(matched),
})
total, _ := out["total"].(float64)
if total != 1 {
t.Fatalf("expected 1 match (path_prefix scopes the file set), got %v", total)
}
first := out["matches"].([]any)[0].(map[string]any)
if first["file"] != matched {
t.Errorf("expected match in %q, got %q", matched, first["file"])
}
}
func TestSearchAST_RejectsBadInputs(t *testing.T) {
srv, _ := setupTestServer(t)
// No pattern, no detector.
{
req := mcplib.CallToolRequest{}
req.Params.Name = "search_ast"
req.Params.Arguments = map[string]any{}
res, err := srv.handleSearchAST(context.Background(), req)
if err != nil {
t.Fatalf("handleSearchAST: %v", err)
}
if !res.IsError {
t.Fatal("expected error when neither pattern nor detector is set")
}
}
// Pattern without language.
{
req := mcplib.CallToolRequest{}
req.Params.Name = "search_ast"
req.Params.Arguments = map[string]any{"pattern": "(identifier) @match"}
res, err := srv.handleSearchAST(context.Background(), req)
if err != nil {
t.Fatalf("handleSearchAST: %v", err)
}
if !res.IsError {
t.Fatal("expected error for pattern without language")
}
}
// Both pattern and detector.
{
req := mcplib.CallToolRequest{}
req.Params.Name = "search_ast"
req.Params.Arguments = map[string]any{
"pattern": "(identifier) @match",
"detector": "panic-in-library",
"language": "go",
}
res, err := srv.handleSearchAST(context.Background(), req)
if err != nil {
t.Fatalf("handleSearchAST: %v", err)
}
if !res.IsError {
t.Fatal("expected error for mutually-exclusive args")
}
}
// Unknown detector.
{
req := mcplib.CallToolRequest{}
req.Params.Name = "search_ast"
req.Params.Arguments = map[string]any{"detector": "no-such-rule"}
res, err := srv.handleSearchAST(context.Background(), req)
if err != nil {
t.Fatalf("handleSearchAST: %v", err)
}
if !res.IsError {
t.Fatal("expected error for unknown detector")
}
}
}
func TestSearchAST_MinFanInOfEnclosingFunc(t *testing.T) {
srv, _ := setupTestServer(t)
abs := writeTempGoFile(t, "two.go", `package x
func Hot() { panic("a") }
func Cold() { panic("b") }
`)
srv.graph.AddNode(&graph.Node{
ID: abs, Kind: graph.KindFile, Name: abs,
FilePath: abs, Language: "go", StartLine: 1, EndLine: 4,
})
hot := &graph.Node{ID: abs + "::Hot", Kind: graph.KindFunction, Name: "Hot",
FilePath: abs, StartLine: 3, EndLine: 3}
cold := &graph.Node{ID: abs + "::Cold", Kind: graph.KindFunction, Name: "Cold",
FilePath: abs, StartLine: 4, EndLine: 4}
srv.graph.AddNode(hot)
srv.graph.AddNode(cold)
// Three callers on Hot, zero on Cold.
for i := 0; i < 3; i++ {
caller := &graph.Node{
ID: abs + "::caller" + string(rune('A'+i)),
Kind: graph.KindFunction,
Name: "caller",
FilePath: abs, StartLine: 10 + i, EndLine: 10 + i,
}
srv.graph.AddNode(caller)
srv.graph.AddEdge(&graph.Edge{From: caller.ID, To: hot.ID, Kind: graph.EdgeCalls})
}
out := callSearchAST(t, srv, map[string]any{
"detector": "panic-in-library",
"min_fan_in_of_enclosing_func": float64(2),
})
total, _ := out["total"].(float64)
if total != 1 {
t.Fatalf("expected only Hot's panic to survive min_fan_in=2 filter, got total=%v", total)
}
first := out["matches"].([]any)[0].(map[string]any)
if first["symbol_name"] != "Hot" {
t.Errorf("expected enclosing symbol Hot, got %v", first["symbol_name"])
}
}