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
242 lines
10 KiB
Go
242 lines
10 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/analysis"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// newSurprisingTestServer seeds a small synthetic graph where each
|
|
// signal can be triggered in isolation. Each test asks for one edge's
|
|
// breakdown and asserts that the matching signal fired.
|
|
func newSurprisingTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
g := graph.New()
|
|
s := &Server{
|
|
graph: g,
|
|
session: newSessionState(),
|
|
tokenStats: &tokenStats{},
|
|
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
|
|
sessions: newSessionMap(),
|
|
toolScopes: newScopeRegistry(),
|
|
}
|
|
return s
|
|
}
|
|
|
|
func callSurprisingHandler(t *testing.T, s *Server, args map[string]any) map[string]any {
|
|
t.Helper()
|
|
req := mcp.CallToolRequest{}
|
|
req.Params.Arguments = args
|
|
res, err := s.handleGetSurprisingConnections(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.False(t, res.IsError, "handler error: %+v", res.Content)
|
|
tc, ok := res.Content[0].(mcp.TextContent)
|
|
require.True(t, ok)
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
|
|
return m
|
|
}
|
|
|
|
// findEdgeRow returns the row whose from/to match. Tests narrow the
|
|
// graph enough that each edge has a unique pair.
|
|
func findEdgeRow(out map[string]any, from, to string) map[string]any {
|
|
conns, _ := out["connections"].([]any)
|
|
for _, c := range conns {
|
|
m := c.(map[string]any)
|
|
if m["from"] == from && m["to"] == to {
|
|
return m
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestSurprising_CrossCommunityFires(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
s.graph.AddNode(&graph.Node{ID: "a", Name: "a", Kind: graph.KindFunction, FilePath: "p/a.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "b", Name: "b", Kind: graph.KindFunction, FilePath: "p/b.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "a", To: "b", Kind: graph.EdgeCalls})
|
|
|
|
// Two different communities so cross_community fires.
|
|
s.analysisMu.Lock()
|
|
s.communities = &analysis.CommunityResult{
|
|
NodeToComm: map[string]string{"a": "c1", "b": "c2"},
|
|
}
|
|
s.analysisMu.Unlock()
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1})
|
|
row := findEdgeRow(out, "a", "b")
|
|
require.NotNil(t, row, "a→b should surface when cross_community fires")
|
|
signals := row["signals"].(map[string]any)
|
|
assert.InDelta(t, 0.30, signals["cross_community"].(float64), 1e-9)
|
|
}
|
|
|
|
func TestSurprising_CrossLanguageFires(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
s.graph.AddNode(&graph.Node{ID: "a", Name: "a", Kind: graph.KindFunction, FilePath: "p/a.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "b", Name: "b", Kind: graph.KindFunction, FilePath: "p/b.ts", Language: "typescript"})
|
|
s.graph.AddEdge(&graph.Edge{From: "a", To: "b", Kind: graph.EdgeCalls})
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1})
|
|
row := findEdgeRow(out, "a", "b")
|
|
require.NotNil(t, row)
|
|
signals := row["signals"].(map[string]any)
|
|
assert.InDelta(t, 0.20, signals["cross_language"].(float64), 1e-9)
|
|
}
|
|
|
|
func TestSurprising_PeripheralToHubFires(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
// Hub h with 6 incoming callers (above default hub_threshold=5).
|
|
s.graph.AddNode(&graph.Node{ID: "h", Name: "h", Kind: graph.KindFunction, FilePath: "p/h.go", Language: "go"})
|
|
for i := range 6 {
|
|
id := "c" + string(rune('A'+i))
|
|
s.graph.AddNode(&graph.Node{ID: id, Name: id, Kind: graph.KindFunction, FilePath: "p/c.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: id, To: "h", Kind: graph.EdgeCalls})
|
|
}
|
|
// Peripheral node p — zero in-edges of its own — connects to h.
|
|
s.graph.AddNode(&graph.Node{ID: "p", Name: "p", Kind: graph.KindFunction, FilePath: "p/p.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "p", To: "h", Kind: graph.EdgeCalls})
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1})
|
|
row := findEdgeRow(out, "p", "h")
|
|
require.NotNil(t, row)
|
|
signals := row["signals"].(map[string]any)
|
|
assert.InDelta(t, 0.20, signals["peripheral_to_hub"].(float64), 1e-9, "peripheral→hub edge fires the signal")
|
|
}
|
|
|
|
func TestSurprising_CrossTestFires(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
s.graph.AddNode(&graph.Node{ID: "prod", Name: "Prod", Kind: graph.KindFunction, FilePath: "p/prod.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "test", Name: "Test", Kind: graph.KindFunction, FilePath: "p/prod_test.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "test", To: "prod", Kind: graph.EdgeCalls})
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1})
|
|
row := findEdgeRow(out, "test", "prod")
|
|
require.NotNil(t, row)
|
|
signals := row["signals"].(map[string]any)
|
|
assert.InDelta(t, 0.15, signals["cross_test"].(float64), 1e-9)
|
|
}
|
|
|
|
func TestSurprising_UnusualKindFires(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
// 20 EdgeCalls + 1 EdgeImports = imports is ~5%, on the boundary.
|
|
// Use rare_kind_pct=10 so imports counts as rare; calls (95%) does not.
|
|
for i := range 20 {
|
|
from := "f" + string(rune('A'+i%26))
|
|
to := "g" + string(rune('A'+i%26))
|
|
s.graph.AddNode(&graph.Node{ID: from + string(rune('0'+i)), Name: from, Kind: graph.KindFunction, FilePath: "p/x.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: to + string(rune('0'+i)), Name: to, Kind: graph.KindFunction, FilePath: "p/y.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: from + string(rune('0'+i)), To: to + string(rune('0'+i)), Kind: graph.EdgeCalls})
|
|
}
|
|
s.graph.AddNode(&graph.Node{ID: "uA", Name: "uA", Kind: graph.KindFunction, FilePath: "p/u.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "uB", Name: "uB", Kind: graph.KindFunction, FilePath: "p/u.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "uA", To: "uB", Kind: graph.EdgeImports})
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1, "rare_kind_pct": 10})
|
|
row := findEdgeRow(out, "uA", "uB")
|
|
require.NotNil(t, row, "rare-kind edge should surface")
|
|
signals := row["signals"].(map[string]any)
|
|
assert.InDelta(t, 0.15, signals["unusual_kind"].(float64), 1e-9)
|
|
}
|
|
|
|
func TestSurprising_MinScoreFilters(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
// Edge with no firing signals; same community, same language, low fan-in, no cross-test, common kind.
|
|
s.graph.AddNode(&graph.Node{ID: "a", Name: "a", Kind: graph.KindFunction, FilePath: "p/a.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "b", Name: "b", Kind: graph.KindFunction, FilePath: "p/b.go", Language: "go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "a", To: "b", Kind: graph.EdgeCalls})
|
|
s.analysisMu.Lock()
|
|
s.communities = &analysis.CommunityResult{NodeToComm: map[string]string{"a": "c1", "b": "c1"}}
|
|
s.analysisMu.Unlock()
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.3})
|
|
conns, _ := out["connections"].([]any)
|
|
assert.Empty(t, conns, "no firing signals → no rows")
|
|
}
|
|
|
|
func TestSurprising_LimitTruncates(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
// 30 cross-language edges; cap to 5.
|
|
for i := range 30 {
|
|
fromID := "go" + string(rune('A'+i%26)) + string(rune('0'+i))
|
|
toID := "ts" + string(rune('A'+i%26)) + string(rune('0'+i))
|
|
s.graph.AddNode(&graph.Node{ID: fromID, Name: fromID, Kind: graph.KindFunction, FilePath: "p/a.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: toID, Name: toID, Kind: graph.KindFunction, FilePath: "p/a.ts", Language: "typescript"})
|
|
s.graph.AddEdge(&graph.Edge{From: fromID, To: toID, Kind: graph.EdgeCalls})
|
|
}
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"limit": 5, "min_score": 0.1})
|
|
conns, _ := out["connections"].([]any)
|
|
assert.Len(t, conns, 5)
|
|
assert.Equal(t, true, out["truncated"])
|
|
}
|
|
|
|
func TestSurprising_CompositeScoreStacks(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
// Cross-community + cross-language + cross-test on the same edge.
|
|
s.graph.AddNode(&graph.Node{ID: "a", Name: "a", Kind: graph.KindFunction, FilePath: "p/a.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "b", Name: "b", Kind: graph.KindFunction, FilePath: "p/b.test.ts", Language: "typescript"})
|
|
s.graph.AddEdge(&graph.Edge{From: "a", To: "b", Kind: graph.EdgeCalls})
|
|
s.analysisMu.Lock()
|
|
s.communities = &analysis.CommunityResult{NodeToComm: map[string]string{"a": "c1", "b": "c2"}}
|
|
s.analysisMu.Unlock()
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1})
|
|
row := findEdgeRow(out, "a", "b")
|
|
require.NotNil(t, row)
|
|
// 0.30 (community) + 0.20 (language) + 0.15 (test) = 0.65
|
|
assert.InDelta(t, 0.65, row["score"].(float64), 1e-6)
|
|
reasons, _ := row["reasons"].([]any)
|
|
assert.Len(t, reasons, 3, "three signals contributed")
|
|
}
|
|
|
|
func TestSurprising_PathPrefixFilter(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
s.graph.AddNode(&graph.Node{ID: "scoped", Name: "scoped", Kind: graph.KindFunction, FilePath: "internal/x.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "scoped2", Name: "scoped2", Kind: graph.KindFunction, FilePath: "internal/y.ts", Language: "typescript"})
|
|
s.graph.AddEdge(&graph.Edge{From: "scoped", To: "scoped2", Kind: graph.EdgeCalls})
|
|
|
|
s.graph.AddNode(&graph.Node{ID: "outsideA", Name: "outsideA", Kind: graph.KindFunction, FilePath: "vendor/x.go", Language: "go"})
|
|
s.graph.AddNode(&graph.Node{ID: "outsideB", Name: "outsideB", Kind: graph.KindFunction, FilePath: "vendor/y.ts", Language: "typescript"})
|
|
s.graph.AddEdge(&graph.Edge{From: "outsideA", To: "outsideB", Kind: graph.EdgeCalls})
|
|
|
|
out := callSurprisingHandler(t, s, map[string]any{"min_score": 0.1, "path_prefix": "internal/"})
|
|
conns, _ := out["connections"].([]any)
|
|
require.Len(t, conns, 1)
|
|
row := conns[0].(map[string]any)
|
|
assert.Equal(t, "scoped", row["from"])
|
|
}
|
|
|
|
func TestSurprising_WeightsExposed(t *testing.T) {
|
|
s := newSurprisingTestServer(t)
|
|
out := callSurprisingHandler(t, s, map[string]any{})
|
|
w := out["signal_weights"].(map[string]any)
|
|
assert.InDelta(t, 0.30, w["cross_community"].(float64), 1e-9)
|
|
assert.InDelta(t, 0.20, w["cross_language"].(float64), 1e-9)
|
|
assert.InDelta(t, 0.20, w["peripheral_to_hub"].(float64), 1e-9)
|
|
assert.InDelta(t, 0.15, w["cross_test"].(float64), 1e-9)
|
|
assert.InDelta(t, 0.15, w["unusual_kind"].(float64), 1e-9)
|
|
}
|
|
|
|
func TestIsTestPath(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"foo_test.go": true,
|
|
"foo.test.ts": true,
|
|
"foo.spec.js": true,
|
|
"__tests__/foo.ts": true,
|
|
"test_foo.py": true,
|
|
"foo.go": false,
|
|
"internal/foo.go": false,
|
|
"foo/internal/test.ts": false, // requires test_ prefix, not test as a path segment
|
|
}
|
|
for p, want := range cases {
|
|
assert.Equal(t, want, isTestPath(p), "isTestPath(%q)", p)
|
|
}
|
|
}
|