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

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