Files
zzet--gortex/internal/mcp/tools_analyze_unsafe_patterns_test.go
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

258 lines
8.0 KiB
Go

package mcp
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// callAnalyzeUnsafe is the shared harness: build a request with the
// given args, invoke the analyze handler, decode the JSON. Any tool-
// level error fails the test.
func callAnalyzeUnsafe(t *testing.T, srv *Server, extra map[string]any) map[string]any {
t.Helper()
args := map[string]any{"kind": "unsafe_patterns"}
for k, v := range extra {
args[k] = v
}
req := mcplib.CallToolRequest{}
req.Params.Name = "analyze"
req.Params.Arguments = args
res, err := srv.handleAnalyze(context.Background(), req)
require.NoError(t, err)
require.False(t, res.IsError, "analyze unsafe_patterns must not error: %+v", res.Content)
text := res.Content[0].(mcplib.TextContent).Text
var out map[string]any
require.NoError(t, json.Unmarshal([]byte(text), &out), "json: %s", text)
return out
}
// writeUnsafeFixture drops a fixture file into a temp dir and
// registers a KindFile node for it so the AST engine can discover
// and parse the file via buildASTTargets.
func writeUnsafeFixture(t *testing.T, srv *Server, name, lang, src string) string {
t.Helper()
dir := t.TempDir()
abs := filepath.Join(dir, name)
require.NoError(t, os.WriteFile(abs, []byte(src), 0o644))
srv.graph.AddNode(&graph.Node{
ID: abs, Kind: graph.KindFile, Name: abs,
FilePath: abs, Language: lang, StartLine: 1, EndLine: 100,
})
return abs
}
// TestAnalyzeDispatcher_RoutesUnsafePatterns regression-protects
// the dispatcher wiring: passing kind=unsafe_patterns must not
// return an unknown-kind error.
func TestAnalyzeDispatcher_RoutesUnsafePatterns(t *testing.T) {
srv, _ := setupTestServer(t)
req := mcplib.CallToolRequest{}
req.Params.Name = "analyze"
req.Params.Arguments = map[string]any{"kind": "unsafe_patterns"}
res, err := srv.handleAnalyze(context.Background(), req)
require.NoError(t, err)
require.False(t, res.IsError,
"dispatcher must route kind=unsafe_patterns without error; got %v", res)
}
func TestAnalyzeUnsafePatterns_RustUnwrapAndPanic(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run(s: &str) {
let _n: i32 = s.parse().unwrap();
panic!("boom");
todo!("later");
}
`)
out := callAnalyzeUnsafe(t, srv, nil)
total, _ := out["total"].(float64)
require.GreaterOrEqual(t, total, float64(3),
"expected at least 3 matches (unwrap + panic! + todo!) — got %v\n%v", total, out)
matches := out["matches"].([]any)
detectors := map[string]int{}
for _, m := range matches {
row := m.(map[string]any)
detectors[row["detector"].(string)]++
assert.Equal(t, "rust", row["language"])
}
assert.Equal(t, 1, detectors["unsafe-rust-unwrap"])
assert.Equal(t, 2, detectors["unsafe-rust-panic-macro"])
// Severity is set per detector.
summary := out["summary"].([]any)
require.NotEmpty(t, summary)
}
func TestAnalyzeUnsafePatterns_FilterByDetector(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run(s: &str) {
let _ = s.parse::<i32>().unwrap();
panic!("boom");
}
`)
out := callAnalyzeUnsafe(t, srv, map[string]any{"detector": "unsafe-rust-unwrap"})
total, _ := out["total"].(float64)
assert.Equal(t, float64(1), total, "detector filter must restrict to unsafe-rust-unwrap only")
matches := out["matches"].([]any)
for _, m := range matches {
row := m.(map[string]any)
assert.Equal(t, "unsafe-rust-unwrap", row["detector"])
}
}
func TestAnalyzeUnsafePatterns_FilterByLanguage(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run() { panic!("x"); }
`)
writeUnsafeFixture(t, srv, "lib.py", "python", `def run(x):
assert x > 0
`)
out := callAnalyzeUnsafe(t, srv, map[string]any{"language": "python"})
total, _ := out["total"].(float64)
require.Equal(t, float64(1), total, "language=python must drop the Rust match")
matches := out["matches"].([]any)
row := matches[0].(map[string]any)
assert.Equal(t, "python", row["language"])
assert.Equal(t, "unsafe-python-assert", row["detector"])
}
func TestAnalyzeUnsafePatterns_FilterBySeverity(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run() {
panic!("warn");
assert!(1 == 1);
}
`)
// severity=warning keeps panic!; severity=info keeps assert!.
outWarn := callAnalyzeUnsafe(t, srv, map[string]any{"severity": "warning"})
for _, m := range outWarn["matches"].([]any) {
assert.Equal(t, "warning", m.(map[string]any)["severity"])
}
outInfo := callAnalyzeUnsafe(t, srv, map[string]any{"severity": "info"})
for _, m := range outInfo["matches"].([]any) {
assert.Equal(t, "info", m.(map[string]any)["severity"])
}
}
func TestAnalyzeUnsafePatterns_JSThrow(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.ts", "typescript", `function run(x: number): number {
if (x < 0) {
throw new Error("neg");
}
return x;
}
`)
out := callAnalyzeUnsafe(t, srv, map[string]any{"language": "typescript"})
total, _ := out["total"].(float64)
require.Equal(t, float64(1), total)
row := out["matches"].([]any)[0].(map[string]any)
assert.Equal(t, "unsafe-js-throw", row["detector"])
assert.Equal(t, "typescript", row["language"])
}
func TestAnalyzeUnsafePatterns_UnknownDetectorErrors(t *testing.T) {
srv, _ := setupTestServer(t)
req := mcplib.CallToolRequest{}
req.Params.Name = "analyze"
req.Params.Arguments = map[string]any{
"kind": "unsafe_patterns",
"detector": "no-such-detector",
}
res, err := srv.handleAnalyze(context.Background(), req)
require.NoError(t, err)
require.True(t, res.IsError, "unknown detector name must surface as an error result")
}
func TestAnalyzeUnsafePatterns_PathPrefixScopes(t *testing.T) {
srv, _ := setupTestServer(t)
keep := writeUnsafeFixture(t, srv, "in.rs", "rust", `fn run() { panic!("x"); }`)
writeUnsafeFixture(t, srv, "out.rs", "rust", `fn run() { panic!("y"); }`)
out := callAnalyzeUnsafe(t, srv, map[string]any{"path_prefix": keep})
total, _ := out["total"].(float64)
require.Equal(t, float64(1), total, "path_prefix must scope to the named file")
row := out["matches"].([]any)[0].(map[string]any)
assert.Equal(t, keep, row["file"])
}
func TestAnalyzeUnsafePatterns_LimitTruncates(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run() {
panic!("a");
panic!("b");
panic!("c");
}
`)
out := callAnalyzeUnsafe(t, srv, map[string]any{"limit": 2.0})
total, _ := out["total"].(float64)
assert.Equal(t, float64(3), total, "total must report the pre-truncation count")
truncated, _ := out["truncated"].(bool)
assert.True(t, truncated)
matches := out["matches"].([]any)
assert.Equal(t, 2, len(matches))
}
func TestAnalyzeUnsafePatterns_GCXEncodesHeader(t *testing.T) {
srv, _ := setupTestServer(t)
writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run() { panic!("x"); }`)
req := mcplib.CallToolRequest{}
req.Params.Name = "analyze"
req.Params.Arguments = map[string]any{
"kind": "unsafe_patterns",
"format": "gcx",
}
res, err := srv.handleAnalyze(context.Background(), req)
require.NoError(t, err)
require.False(t, res.IsError)
text := res.Content[0].(mcplib.TextContent).Text
assert.Contains(t, text, "analyze.unsafe_patterns")
for _, col := range []string{"detector", "severity", "language", "file", "line", "symbol", "text"} {
assert.Contains(t, text, col, "GCX header must list column %q", col)
}
}
func TestAnalyzeUnsafePatterns_CompactOutput(t *testing.T) {
srv, _ := setupTestServer(t)
abs := writeUnsafeFixture(t, srv, "lib.rs", "rust", `fn run() { panic!("x"); }`)
req := mcplib.CallToolRequest{}
req.Params.Name = "analyze"
req.Params.Arguments = map[string]any{
"kind": "unsafe_patterns",
"compact": true,
}
res, err := srv.handleAnalyze(context.Background(), req)
require.NoError(t, err)
require.False(t, res.IsError)
text := res.Content[0].(mcplib.TextContent).Text
assert.Contains(t, text, "unsafe-rust-panic-macro")
assert.Contains(t, text, abs)
}