Files
zzet--gortex/internal/mcp/tools_inspections_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

292 lines
9.4 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/config"
"github.com/zzet/gortex/internal/contracts"
"github.com/zzet/gortex/internal/graph"
)
func newInspectionsTestServer(t *testing.T) *Server {
t.Helper()
g := graph.New()
// Dead code: unexported function with zero in-edges (exported
// names are intentionally skipped by FindDeadCode — they're
// assumed to be called from outside the indexed code).
g.AddNode(&graph.Node{
ID: "p/dead.go::dead", Name: "dead", Kind: graph.KindFunction,
FilePath: "p/dead.go", StartLine: 4, EndLine: 6, Language: "go",
})
// Live function with one caller — not dead.
g.AddNode(&graph.Node{
ID: "p/live.go::live", Name: "live", Kind: graph.KindFunction,
FilePath: "p/live.go", StartLine: 4, Language: "go",
})
g.AddNode(&graph.Node{
ID: "p/live.go::caller", Name: "caller", Kind: graph.KindFunction,
FilePath: "p/live.go", StartLine: 10, Language: "go",
})
g.AddEdge(&graph.Edge{From: "p/live.go::caller", To: "p/live.go::live", Kind: graph.EdgeCalls})
// Coverage gap on live (40%).
g.GetNode("p/live.go::live").Meta = map[string]any{"coverage_pct": 40.0}
// TODO node.
g.AddNode(&graph.Node{
ID: "p/notes.go:42:todo", Kind: graph.KindTodo,
FilePath: "p/notes.go", StartLine: 42,
Meta: map[string]any{
"tag": "TODO",
"text": "wire up retries",
},
})
return &Server{
graph: g,
session: newSessionState(),
tokenStats: &tokenStats{},
symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
sessions: newSessionMap(),
toolScopes: newScopeRegistry(),
}
}
func callListInspections(t *testing.T, s *Server, args map[string]any) map[string]any {
t.Helper()
req := mcp.CallToolRequest{}
req.Params.Arguments = args
res, err := s.handleListInspections(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 callRunInspections(t *testing.T, s *Server, args map[string]any) map[string]any {
t.Helper()
req := mcp.CallToolRequest{}
req.Params.Arguments = args
res, err := s.handleRunInspections(context.Background(), req)
require.NoError(t, err)
require.NotNil(t, res)
if res.IsError {
return map[string]any{"is_error": true}
}
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 TestListInspections_ReturnsCatalog(t *testing.T) {
s := newInspectionsTestServer(t)
out := callListInspections(t, s, map[string]any{})
rows, _ := out["inspections"].([]any)
require.NotEmpty(t, rows)
// Every entry must have id, category, description, severity.
for _, r := range rows {
m := r.(map[string]any)
assert.NotEmpty(t, m["id"])
assert.NotEmpty(t, m["category"])
assert.NotEmpty(t, m["description"])
assert.NotEmpty(t, m["severity"])
}
}
func TestListInspections_CategoryFilter(t *testing.T) {
s := newInspectionsTestServer(t)
out := callListInspections(t, s, map[string]any{"category": "dead-code"})
rows, _ := out["inspections"].([]any)
require.NotEmpty(t, rows)
for _, r := range rows {
m := r.(map[string]any)
assert.Equal(t, "dead-code", m["category"])
}
}
func TestRunInspections_DeadCodeFires(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{
"inspections": "dead_code",
})
results, _ := out["results"].([]any)
require.NotEmpty(t, results)
first := results[0].(map[string]any)
assert.Equal(t, "dead_code", first["inspection"])
violations, _ := first["violations"].([]any)
require.NotEmpty(t, violations, "dead has zero in-edges, should be flagged")
v := violations[0].(map[string]any)
assert.Equal(t, "warning", v["severity"])
assert.Equal(t, "p/dead.go::dead", v["symbol_id"])
}
func TestRunInspections_CoverageGapsFires(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{
"inspections": "coverage_gaps",
})
results, _ := out["results"].([]any)
require.Len(t, results, 1)
violations, _ := results[0].(map[string]any)["violations"].([]any)
require.NotEmpty(t, violations, "live has 40%% coverage — flagged")
v := violations[0].(map[string]any)
assert.Equal(t, "p/live.go::live", v["symbol_id"])
}
func TestRunInspections_TodosFire(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{
"inspections": "todos",
})
results, _ := out["results"].([]any)
require.Len(t, results, 1)
violations, _ := results[0].(map[string]any)["violations"].([]any)
require.Len(t, violations, 1)
v := violations[0].(map[string]any)
assert.Equal(t, "info", v["severity"])
assert.Contains(t, v["message"].(string), "wire up retries")
}
func TestRunInspections_AllRunsEverything(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{"inspections": "all"})
results, _ := out["results"].([]any)
assert.GreaterOrEqual(t, len(results), 5, "every registry entry runs under inspections=all")
}
func TestRunInspections_SeverityFilter(t *testing.T) {
s := newInspectionsTestServer(t)
// info-only filter: dead_code (warning) drops out, todos (info) stays.
out := callRunInspections(t, s, map[string]any{
"inspections": "dead_code,todos",
"severity": "info",
})
results, _ := out["results"].([]any)
totals := map[string]int{}
for _, r := range results {
m := r.(map[string]any)
totals[m["inspection"].(string)] = int(m["total"].(float64))
}
assert.Zero(t, totals["dead_code"], "warning severity filtered out")
assert.NotZero(t, totals["todos"], "info severity kept")
}
func TestRunInspections_MaxPerInspectionCap(t *testing.T) {
s := newInspectionsTestServer(t)
// Add 10 more todos.
for i := range 10 {
s.graph.AddNode(&graph.Node{
ID: "p/extra.go::todo" + string(rune('A'+i)), Kind: graph.KindTodo,
FilePath: "p/extra.go", StartLine: i + 1,
Meta: map[string]any{"tag": "TODO", "text": "x"},
})
}
out := callRunInspections(t, s, map[string]any{
"inspections": "todos",
"max_per_inspection": 3,
})
results, _ := out["results"].([]any)
r := results[0].(map[string]any)
violations, _ := r["violations"].([]any)
assert.LessOrEqual(t, len(violations), 3)
assert.Equal(t, true, r["truncated"].(bool))
}
func TestRunInspections_PathPrefixScopes(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{
"inspections": "dead_code",
"path_prefix": "p/dead", // includes p/dead.go, excludes p/live.go
})
results, _ := out["results"].([]any)
violations, _ := results[0].(map[string]any)["violations"].([]any)
// Only `dead` from p/dead.go should be flagged; caller in p/live.go
// (which also lacks callers and is unexported) must be excluded.
for _, v := range violations {
m := v.(map[string]any)
assert.NotContains(t, m["file"].(string), "p/live.go",
"path_prefix p/dead should exclude p/live.go nodes")
}
}
func TestRunInspections_SummaryAggregates(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{"inspections": "dead_code,todos"})
summary := out["summary"].(map[string]any)
by := summary["by_inspection"].(map[string]any)
total := int(summary["total_violations"].(float64))
assert.GreaterOrEqual(t, total, 2)
deadCount := int(by["dead_code"].(float64))
todoCount := int(by["todos"].(float64))
assert.Equal(t, deadCount+todoCount, total)
}
func TestRunInspections_ContractOrphansFire(t *testing.T) {
s := newInspectionsTestServer(t)
reg := contracts.NewRegistry()
// Provider with no consumer.
reg.Add(contracts.Contract{
ID: "http:GET:/orphan", Type: contracts.ContractType("http"),
Role: contracts.RoleProvider, FilePath: "p/handler.go", SymbolID: "p/handler.go::Orphan",
})
s.contractRegistry = reg
out := callRunInspections(t, s, map[string]any{"inspections": "contracts_orphans"})
results, _ := out["results"].([]any)
violations, _ := results[0].(map[string]any)["violations"].([]any)
require.Len(t, violations, 1)
v := violations[0].(map[string]any)
assert.Contains(t, v["message"].(string), "orphan contract")
}
func TestRunInspections_RejectsMissingInspectionsArg(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{})
assert.True(t, out["is_error"] == true)
}
func TestRunInspections_UnknownInspectionsAreNoOps(t *testing.T) {
s := newInspectionsTestServer(t)
out := callRunInspections(t, s, map[string]any{"inspections": "definitely_not_a_real_inspector"})
results, _ := out["results"].([]any)
assert.Empty(t, results, "unknown ids don't error, they just produce no output")
}
func TestRunInspections_GuardsWhenRulesPresent(t *testing.T) {
s := newInspectionsTestServer(t)
s.guardRules = []config.GuardRule{{Name: "no-cross-package-state"}}
out := callRunInspections(t, s, map[string]any{"inspections": "guards"})
results, _ := out["results"].([]any)
require.Len(t, results, 1)
violations, _ := results[0].(map[string]any)["violations"].([]any)
// At least one violation surfaces per scoped node.
require.NotEmpty(t, violations)
v := violations[0].(map[string]any)
assert.Equal(t, "error", v["severity"])
assert.Contains(t, v["message"].(string), "no-cross-package-state")
}