Files
zzet--gortex/internal/mcp/tools_analyze_missing_fields_test.go
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

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")
}