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
292 lines
9.4 KiB
Go
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")
|
|
}
|