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
133 lines
5.2 KiB
Go
133 lines
5.2 KiB
Go
package mcp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
mcplib "github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// addOrphanSymbol injects a function node with no edges of any kind
|
|
// into the server's graph, mimicking a symbol the extractor never
|
|
// processed. Any usage / caller / impact query on it comes back empty.
|
|
func addOrphanSymbol(srv *Server, id string) {
|
|
srv.graph.AddNode(&graph.Node{
|
|
ID: id, Kind: graph.KindFunction, Name: id, FilePath: "orphan.go",
|
|
})
|
|
}
|
|
|
|
// decodeJSONResult unmarshals a tool's JSON text content into a map.
|
|
func decodeJSONResult(t *testing.T, res *mcplib.CallToolResult) map[string]any {
|
|
t.Helper()
|
|
require.False(t, res.IsError)
|
|
tc, ok := res.Content[0].(mcplib.TextContent)
|
|
require.True(t, ok)
|
|
var m map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
|
|
return m
|
|
}
|
|
|
|
// TestFindUsages_ZeroEdgeCaveat asserts find_usages attaches the
|
|
// extraction-gap caveat when a symbol has no usages and omits it when
|
|
// the symbol is used.
|
|
func TestFindUsages_ZeroEdgeCaveat(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
addOrphanSymbol(srv, "orphan.go::Orphan")
|
|
|
|
// Zero-edge symbol — the caveat must be present and classify the
|
|
// empty result as an extraction gap.
|
|
empty := decodeJSONResult(t, callTool(t, srv, "find_usages", map[string]any{
|
|
"id": "orphan.go::Orphan",
|
|
}))
|
|
caveat, ok := empty["caveat"].(map[string]any)
|
|
require.True(t, ok, "find_usages on a zero-edge symbol must carry a caveat")
|
|
assert.Equal(t, string(graph.ZeroEdgePossibleExtractionGap), caveat["class"])
|
|
assert.NotEmpty(t, caveat["message"])
|
|
|
|
// `helper` is called by `main` — a non-empty result carries no caveat.
|
|
used := decodeJSONResult(t, callTool(t, srv, "find_usages", map[string]any{
|
|
"id": "main.go::helper",
|
|
}))
|
|
edges, _ := used["edges"].([]any)
|
|
require.NotEmpty(t, edges, "fixture: helper must have usages")
|
|
_, hasCaveat := used["caveat"]
|
|
assert.False(t, hasCaveat, "a non-empty find_usages result must omit the caveat")
|
|
}
|
|
|
|
// TestGetCallers_ZeroEdgeCaveat asserts get_callers attaches the
|
|
// caveat when a symbol has no callers and omits it when it does.
|
|
func TestGetCallers_ZeroEdgeCaveat(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
addOrphanSymbol(srv, "orphan.go::Orphan")
|
|
|
|
empty := decodeJSONResult(t, callTool(t, srv, "get_callers", map[string]any{
|
|
"id": "orphan.go::Orphan",
|
|
}))
|
|
caveat, ok := empty["caveat"].(map[string]any)
|
|
require.True(t, ok, "get_callers on a zero-edge symbol must carry a caveat")
|
|
assert.Equal(t, string(graph.ZeroEdgePossibleExtractionGap), caveat["class"])
|
|
assert.NotEmpty(t, caveat["message"])
|
|
|
|
used := decodeJSONResult(t, callTool(t, srv, "get_callers", map[string]any{
|
|
"id": "main.go::helper",
|
|
}))
|
|
edges, _ := used["edges"].([]any)
|
|
require.NotEmpty(t, edges, "fixture: helper must have callers")
|
|
_, hasCaveat := used["caveat"]
|
|
assert.False(t, hasCaveat, "a non-empty get_callers result must omit the caveat")
|
|
}
|
|
|
|
// TestExplainChangeImpact_ZeroEdgeCaveat asserts explain_change_impact
|
|
// attaches a per-symbol caveat when the blast radius is empty and omits
|
|
// it when the change has affected symbols.
|
|
func TestExplainChangeImpact_ZeroEdgeCaveat(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
addOrphanSymbol(srv, "orphan.go::Orphan")
|
|
|
|
empty := decodeJSONResult(t, callTool(t, srv, "explain_change_impact", map[string]any{
|
|
"ids": "orphan.go::Orphan",
|
|
}))
|
|
require.EqualValues(t, 0, empty["total_affected"])
|
|
caveats, ok := empty["zero_impact_caveat"].([]any)
|
|
require.True(t, ok, "explain_change_impact on a zero-edge symbol must carry a caveat")
|
|
require.Len(t, caveats, 1)
|
|
first, _ := caveats[0].(map[string]any)
|
|
assert.Equal(t, "orphan.go::Orphan", first["id"])
|
|
assert.Equal(t, string(graph.ZeroEdgePossibleExtractionGap), first["class"])
|
|
assert.NotEmpty(t, first["message"])
|
|
|
|
// `helper` is called by `main` — a non-empty blast radius carries
|
|
// no caveat.
|
|
used := decodeJSONResult(t, callTool(t, srv, "explain_change_impact", map[string]any{
|
|
"ids": "main.go::helper",
|
|
}))
|
|
total, _ := used["total_affected"].(float64)
|
|
require.Greater(t, total, 0.0, "fixture: changing helper must affect main")
|
|
_, hasCaveat := used["zero_impact_caveat"]
|
|
assert.False(t, hasCaveat, "a non-empty explain_change_impact result must omit the caveat")
|
|
}
|
|
|
|
// TestZeroEdgeCaveat_LikelyUnusedClassification asserts the caveat
|
|
// distinguishes a genuinely unused symbol (indexed, no callers) from an
|
|
// extraction gap. A symbol the indexer saw carries a structural edge,
|
|
// so its empty usage query is classified likely_unused, not a gap.
|
|
func TestZeroEdgeCaveat_LikelyUnusedClassification(t *testing.T) {
|
|
srv, _ := setupTestServer(t)
|
|
|
|
// `Config` is a real indexed type — the file defines it — but
|
|
// nothing references it in the fixture source.
|
|
res := decodeJSONResult(t, callTool(t, srv, "find_usages", map[string]any{
|
|
"id": "main.go::Config",
|
|
}))
|
|
edges, _ := res["edges"].([]any)
|
|
require.Empty(t, edges, "fixture: Config is unreferenced")
|
|
caveat, ok := res["caveat"].(map[string]any)
|
|
require.True(t, ok, "an unreferenced indexed symbol must still carry a caveat")
|
|
assert.Equal(t, string(graph.ZeroEdgeLikelyUnused), caveat["class"],
|
|
"an indexed-but-unused symbol must be likely_unused, not an extraction gap")
|
|
}
|