a06f331eb8
install-script / powershell-syntax (push) Waiting to run
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
install-script / install (macos-14) (push) Waiting to run
install-script / install (ubuntu-latest) (push) Waiting to run
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
179 lines
6.5 KiB
Go
179 lines
6.5 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/graph"
|
|
)
|
|
|
|
// seedMissingFieldsFixture builds a type Foo with fields {A, B, C}
|
|
// and two instantiators:
|
|
// - good() references A, B, C — should NOT be flagged.
|
|
// - bad() references A only — missing {B, C}.
|
|
//
|
|
// Field references go through EdgeReferences (function → field
|
|
// node); membership goes through EdgeMemberOf (field → type);
|
|
// instantiation goes through EdgeInstantiates (function → type).
|
|
func seedMissingFieldsFixture(t *testing.T) *Server {
|
|
t.Helper()
|
|
g := graph.New()
|
|
|
|
g.AddNode(&graph.Node{ID: "pkg.Foo", Name: "Foo", Kind: graph.KindType, FilePath: "pkg/foo.go"})
|
|
for _, name := range []string{"A", "B", "C"} {
|
|
fid := "pkg.Foo." + name
|
|
g.AddNode(&graph.Node{ID: fid, Name: name, Kind: graph.KindField, FilePath: "pkg/foo.go"})
|
|
g.AddEdge(&graph.Edge{From: fid, To: "pkg.Foo", Kind: graph.EdgeMemberOf})
|
|
}
|
|
|
|
g.AddNode(&graph.Node{ID: "pkg.good", Name: "good", Kind: graph.KindFunction, FilePath: "pkg/good.go", StartLine: 5})
|
|
g.AddNode(&graph.Node{ID: "pkg.bad", Name: "bad", Kind: graph.KindFunction, FilePath: "pkg/bad.go", StartLine: 10})
|
|
|
|
g.AddEdge(&graph.Edge{From: "pkg.good", To: "pkg.Foo", Kind: graph.EdgeInstantiates})
|
|
g.AddEdge(&graph.Edge{From: "pkg.bad", To: "pkg.Foo", Kind: graph.EdgeInstantiates})
|
|
|
|
// good references A + B + C.
|
|
for _, name := range []string{"A", "B", "C"} {
|
|
g.AddEdge(&graph.Edge{From: "pkg.good", To: "pkg.Foo." + name, Kind: graph.EdgeReferences})
|
|
}
|
|
// bad references A only.
|
|
g.AddEdge(&graph.Edge{From: "pkg.bad", To: "pkg.Foo.A", Kind: graph.EdgeReferences})
|
|
|
|
return &Server{
|
|
graph: g,
|
|
session: newSessionState(),
|
|
tokenStats: &tokenStats{},
|
|
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
|
|
sessions: newSessionMap(),
|
|
toolScopes: newScopeRegistry(),
|
|
}
|
|
}
|
|
|
|
func callMissingFields(t *testing.T, s *Server, args map[string]any) map[string]any {
|
|
t.Helper()
|
|
req := mcp.CallToolRequest{}
|
|
req.Params.Arguments = args
|
|
res, err := s.handleAnalyzeConstructorsMissingFields(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
require.False(t, res.IsError)
|
|
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
|
|
}
|
|
|
|
func TestMissingFields_FlagsBadCallerOnly(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
out := callMissingFields(t, s, map[string]any{})
|
|
|
|
sites, _ := out["sites"].([]any)
|
|
require.Len(t, sites, 1, "good caller populates everything; only bad is flagged")
|
|
row := sites[0].(map[string]any)
|
|
assert.Equal(t, "pkg.bad", row["function_id"])
|
|
assert.Equal(t, "pkg.Foo", row["type_id"])
|
|
missing, _ := row["missing_fields"].([]any)
|
|
got := []string{}
|
|
for _, m := range missing {
|
|
got = append(got, m.(string))
|
|
}
|
|
assert.ElementsMatch(t, []string{"B", "C"}, got)
|
|
}
|
|
|
|
func TestMissingFields_MinMissingFilter(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
out := callMissingFields(t, s, map[string]any{"min_missing": 3})
|
|
sites, _ := out["sites"].([]any)
|
|
assert.Empty(t, sites, "bad is missing 2 fields; min_missing=3 filters it out")
|
|
}
|
|
|
|
func TestMissingFields_TypeFilter(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
// Add a second type that's also missing fields.
|
|
s.graph.AddNode(&graph.Node{ID: "pkg.Bar", Name: "Bar", Kind: graph.KindType, FilePath: "pkg/bar.go"})
|
|
s.graph.AddNode(&graph.Node{ID: "pkg.Bar.X", Name: "X", Kind: graph.KindField, FilePath: "pkg/bar.go"})
|
|
s.graph.AddEdge(&graph.Edge{From: "pkg.Bar.X", To: "pkg.Bar", Kind: graph.EdgeMemberOf})
|
|
s.graph.AddEdge(&graph.Edge{From: "pkg.bad", To: "pkg.Bar", Kind: graph.EdgeInstantiates})
|
|
|
|
out := callMissingFields(t, s, map[string]any{"type_id": "pkg.Foo"})
|
|
sites, _ := out["sites"].([]any)
|
|
for _, site := range sites {
|
|
assert.Equal(t, "pkg.Foo", site.(map[string]any)["type_id"])
|
|
}
|
|
}
|
|
|
|
func TestMissingFields_NullableFieldSkipped(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
// Mark field C as nullable — `bad` will only be missing B now.
|
|
nodeC := s.graph.GetNode("pkg.Foo.C")
|
|
require.NotNil(t, nodeC)
|
|
nodeC.Meta = map[string]any{"nullable": true}
|
|
|
|
out := callMissingFields(t, s, map[string]any{})
|
|
sites, _ := out["sites"].([]any)
|
|
require.Len(t, sites, 1)
|
|
missing, _ := sites[0].(map[string]any)["missing_fields"].([]any)
|
|
got := []string{}
|
|
for _, m := range missing {
|
|
got = append(got, m.(string))
|
|
}
|
|
assert.ElementsMatch(t, []string{"B"}, got, "nullable field C not flagged")
|
|
}
|
|
|
|
func TestMissingFields_OmitemptyTagSkipped(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
nodeC := s.graph.GetNode("pkg.Foo.C")
|
|
require.NotNil(t, nodeC)
|
|
nodeC.Meta = map[string]any{"json_tag": `json:"c,omitempty"`}
|
|
|
|
out := callMissingFields(t, s, map[string]any{})
|
|
sites, _ := out["sites"].([]any)
|
|
require.Len(t, sites, 1)
|
|
missing, _ := sites[0].(map[string]any)["missing_fields"].([]any)
|
|
for _, m := range missing {
|
|
assert.NotEqual(t, "C", m.(string), "omitempty field C skipped")
|
|
}
|
|
}
|
|
|
|
func TestMissingFields_PathPrefixFilter(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
out := callMissingFields(t, s, map[string]any{"path_prefix": "vendor/"})
|
|
sites, _ := out["sites"].([]any)
|
|
assert.Empty(t, sites, "path_prefix excludes both the type and its instantiators")
|
|
}
|
|
|
|
func TestMissingFields_AccuracyAdmitsHeuristic(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
out := callMissingFields(t, s, map[string]any{})
|
|
assert.Equal(t, "heuristic", out["accuracy"], "result is explicitly heuristic, not authoritative")
|
|
}
|
|
|
|
func TestMissingFields_IntegrationViaDispatch(t *testing.T) {
|
|
s := seedMissingFieldsFixture(t)
|
|
req := mcp.CallToolRequest{}
|
|
req.Params.Arguments = map[string]any{"kind": "constructors_missing_fields"}
|
|
res, err := s.handleAnalyze(context.Background(), req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, res)
|
|
assert.False(t, res.IsError, "analyze kind=constructors_missing_fields must dispatch")
|
|
}
|
|
|
|
func TestIsNullableField_AllForms(t *testing.T) {
|
|
cases := map[string]map[string]any{
|
|
"nullable": {"nullable": true},
|
|
"optional": {"optional": true},
|
|
"omitempty": {"json_tag": `json:"x,omitempty"`},
|
|
}
|
|
for name, meta := range cases {
|
|
n := &graph.Node{Meta: meta}
|
|
assert.True(t, isNullableField(n), name)
|
|
}
|
|
assert.False(t, isNullableField(&graph.Node{}), "no meta → not nullable")
|
|
assert.False(t, isNullableField(&graph.Node{Meta: map[string]any{"nullable": false}}), "explicit false → not nullable")
|
|
}
|