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
258 lines
8.0 KiB
Go
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)
|
|
}
|