Files
zzet--gortex/internal/semantic/lsp/enrich_defconfirm_test.go
T
pbednarcik 44d1e65f43 lsp: pin the declared-member arm on the heavy default path
The add-not-rebind verdict for dispatched call sites lives in the shared
definition-rebind pass, so it changes edge topology for every server, not
just the heavy-opt-out ones — but every behavior test for the arm set
noHeavyRequests. Cover the default path: references sweep runs and answers
empty, definition answers the declared member, and the site must keep its
devirtualization guess while gaining the compiler-proven edge (the old
behavior rebound the impl edge instead). The dispatched-call fixture moves
into a shared helper for both tests.
2026-08-20 22:45:19 +02:00

492 lines
22 KiB
Go

package lsp
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
)
// The csharp-ls / omnisharp spec opts out of the heavy request classes:
// textDocument/references and callHierarchy/incomingCalls both ride Roslyn's
// FindReferences machinery, which leaks unboundedly per request in csharp-ls.
// Edge confirmation goes through textDocument/definition at the call site
// instead — same verdicts, position-request cost.
func TestCSharpSpecOptsOutOfHeavyRequests(t *testing.T) {
spec := SpecByName("omnisharp")
require.NotNil(t, spec)
assert.True(t, spec.NoHeavyRequests, "csharp spec must skip references/incomingCalls")
p := NewProviderFromSpec(spec, nil)
assert.True(t, p.noHeavyRequests, "spec opt-out must reach the provider")
def := NewProvider("fake-lsp", nil, []string{"go"}, false, 2, nil)
assert.False(t, def.noHeavyRequests, "a spec-less provider keeps the heavy legs")
}
// defConfirmFixture seeds one ambiguous call edge whose site line names the
// target, plus fake-server handlers for references (counting), definition
// (answering the target's declaration), and hover. The returned edge is the
// live store object — ConfirmEdge mutates it in place.
func defConfirmFixture(t *testing.T, repoRoot string, g graph.Store, server *fakeLSPServer) (edge *graph.Edge, refs, defs *atomic.Int64) {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "def.go"),
[]byte("package p\nfunc target() {}\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "call.go"),
[]byte("package p\nfunc caller() { target() }\n"), 0o644))
g.AddNode(&graph.Node{ID: "def.go::target", Kind: graph.KindFunction, Name: "target",
FilePath: "def.go", StartLine: 2, EndLine: 2, Language: "go"})
g.AddNode(&graph.Node{ID: "call.go::caller", Kind: graph.KindFunction, Name: "caller",
FilePath: "call.go", StartLine: 2, EndLine: 2, Language: "go"})
edge = &graph.Edge{
From: "call.go::caller", To: "def.go::target", Kind: graph.EdgeCalls,
FilePath: "call.go", Line: 2,
Confidence: 0.7, ConfidenceLabel: "INFERRED", Origin: graph.OriginTextMatched,
}
g.AddEdge(edge)
refs, defs = &atomic.Int64{}, &atomic.Int64{}
// References would confirm the site (the location matches the edge) — the
// point of the heavy opt-out is that it must never be asked.
server.handle("textDocument/references", func(json.RawMessage) (any, *jsonRPCError) {
refs.Add(1)
return []Location{{
URI: pathToURI(filepath.Join(repoRoot, "call.go")),
Range: Range{Start: Position{Line: 1, Character: 16}, End: Position{Line: 1, Character: 22}},
}}, nil
})
server.handle("textDocument/definition", func(json.RawMessage) (any, *jsonRPCError) {
defs.Add(1)
return []Location{{
URI: pathToURI(filepath.Join(repoRoot, "def.go")),
Range: Range{Start: Position{Line: 1, Character: 5}, End: Position{Line: 1, Character: 11}},
}}, nil
})
server.handle("textDocument/hover", func(json.RawMessage) (any, *jsonRPCError) { return nil, nil })
return edge, refs, defs
}
// With the heavy opt-out, an ambiguous edge is confirmed through definition at
// its call site — and the references round trip never happens.
func TestLSP_Enrich_NoHeavy_DefinitionConfirmsWithoutReferences(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
g := graph.New()
server := newFakeLSPServer()
edge, refs, defs := defConfirmFixture(t, repoRoot, g, server)
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
p.noHeavyRequests = true
require.NoError(t, runEnrich(t, p, g, repoRoot, 5*time.Second))
assert.Zero(t, refs.Load(), "references must never be requested under the heavy opt-out")
assert.Positive(t, defs.Load(), "the confirm verdict must come from definition")
assert.Equal(t, graph.OriginLSPResolved, edge.Origin, "the edge must be promoted")
assert.Equal(t, 1.0, edge.Confidence)
}
// Without the opt-out nothing changes: the references confirm sweep still runs
// first — this pins that gopls-shaped providers are untouched.
func TestLSP_Enrich_HeavyDefault_StillIssuesReferences(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
g := graph.New()
server := newFakeLSPServer()
edge, refs, _ := defConfirmFixture(t, repoRoot, g, server)
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
require.NoError(t, runEnrich(t, p, g, repoRoot, 5*time.Second))
assert.Positive(t, refs.Load(), "the default path still confirms through references")
assert.Equal(t, graph.OriginLSPResolved, edge.Origin)
}
// The heavy opt-out also silences callHierarchy/incomingCalls — even for a
// dispatch-relevant method that the demand default would interrogate. The
// outgoing side stays: it is free today and lights up if the server ever
// implements it.
func TestLSP_Enrich_NoHeavy_SkipsIncomingEvenForDispatchMethod(t *testing.T) {
t.Setenv(SweepEnv, "") // demand default
repoRoot := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "svc.go"),
[]byte("package p\n\ntype Shape interface{ Area() float64 }\n\ntype Circle struct{}\n\nfunc (c Circle) Area() float64 { return 0 }\n"), 0o644))
server := newFakeLSPServer()
prepare, outgoing, incoming := callHierarchyCounters(server, repoRoot)
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
p.noHeavyRequests = true
g := graph.New()
g.AddNode(&graph.Node{ID: "svc.go::Shape", Kind: graph.KindInterface, Name: "Shape",
FilePath: "svc.go", StartLine: 3, EndLine: 3, Language: "go"})
g.AddNode(&graph.Node{ID: "svc.go::Circle", Kind: graph.KindType, Name: "Circle",
FilePath: "svc.go", StartLine: 5, EndLine: 5, Language: "go"})
g.AddNode(&graph.Node{ID: "svc.go::Circle.Area", Kind: graph.KindMethod, Name: "Area",
FilePath: "svc.go", StartLine: 7, EndLine: 7, Language: "go"})
g.AddEdge(&graph.Edge{From: "svc.go::Circle.Area", To: "svc.go::Circle", Kind: graph.EdgeMemberOf})
g.AddEdge(&graph.Edge{From: "svc.go::Circle", To: "svc.go::Shape", Kind: graph.EdgeImplements})
require.NoError(t, runEnrich(t, p, g, repoRoot, 3*time.Second))
assert.Positive(t, prepare.Load())
assert.Positive(t, outgoing.Load(), "outgoing stays on — it is not a heavy request")
assert.Zero(t, incoming.Load(), "incoming rides FindReferences and must be skipped")
}
// Definition on `new T(...)` answers the constructor's declaration line, not
// the type's. An instantiates edge targeting the type must still count that
// as an exact confirm: the constructor is a member of the very type the edge
// names.
func TestLSP_Enrich_DefConfirm_CtorHitConfirmsTypeTarget(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "dom.go"),
[]byte("package p\ntype Crate struct{\n\tn int\n}\nfunc ctor() {}\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "use.go"),
[]byte("package p\nfunc maker() { _ = Crate{} }\n"), 0o644))
g := graph.New()
g.AddNode(&graph.Node{ID: "dom.go::Crate", Kind: graph.KindType, Name: "Crate",
FilePath: "dom.go", StartLine: 2, EndLine: 5, Language: "go"})
// The constructor: a differently-named member on a line inside the type's
// span — the shape a C# `Crate.<init>` node has.
ctor := &graph.Node{ID: "dom.go::Crate.<init>", Kind: graph.KindMethod, Name: "Crate.<init>",
FilePath: "dom.go", StartLine: 5, EndLine: 5, Language: "go"}
g.AddNode(ctor)
g.AddEdge(&graph.Edge{From: ctor.ID, To: "dom.go::Crate", Kind: graph.EdgeMemberOf})
g.AddNode(&graph.Node{ID: "use.go::maker", Kind: graph.KindFunction, Name: "maker",
FilePath: "use.go", StartLine: 2, EndLine: 2, Language: "go"})
edge := &graph.Edge{
From: "use.go::maker", To: "dom.go::Crate", Kind: graph.EdgeInstantiates,
FilePath: "use.go", Line: 2,
Confidence: 0.7, ConfidenceLabel: "INFERRED", Origin: graph.OriginTextMatched,
}
g.AddEdge(edge)
server := newFakeLSPServer()
server.handle("textDocument/definition", func(json.RawMessage) (any, *jsonRPCError) {
// The ctor's declaration line (0-based 4), inside Crate's span.
return []Location{{
URI: pathToURI(filepath.Join(repoRoot, "dom.go")),
Range: Range{Start: Position{Line: 4, Character: 5}, End: Position{Line: 4, Character: 9}},
}}, nil
})
server.handle("textDocument/hover", func(json.RawMessage) (any, *jsonRPCError) { return nil, nil })
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
p.noHeavyRequests = true
require.NoError(t, runEnrich(t, p, g, repoRoot, 5*time.Second))
assert.Equal(t, graph.OriginLSPResolved, edge.Origin,
"a definition landing on the ctor confirms the containing-type target")
assert.Equal(t, 1.0, edge.Confidence)
assert.Equal(t, "dom.go::Crate", edge.To, "the edge keeps its type target — no retarget to the ctor")
}
// dispatchedCallFixture seeds a dispatched call site: an interface with a
// declared member, one concrete impl, and a stored AST-inferred call edge
// targeting the impl. The fake server's definition answers the interface's
// declared member — the compiler vouching for the declaration, not the
// devirtualization guess. Returns the live impl edge.
func dispatchedCallFixture(t *testing.T, repoRoot string, g graph.Store, server *fakeLSPServer) *graph.Edge {
t.Helper()
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "scales.go"),
[]byte("package p\ntype IScale interface {\n\tWeigh() int\n}\n\ntype DrumScale struct{\n\tWeighImpl int\n}\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "station.go"),
[]byte("package p\nfunc station() { Weigh() }\n"), 0o644))
g.AddNode(&graph.Node{ID: "scales.go::IScale", Kind: graph.KindInterface, Name: "IScale",
FilePath: "scales.go", StartLine: 2, EndLine: 4, Language: "go"})
g.AddNode(&graph.Node{ID: "scales.go::IScale.Weigh", Kind: graph.KindMethod, Name: "Weigh",
FilePath: "scales.go", StartLine: 3, EndLine: 3, Language: "go"})
g.AddEdge(&graph.Edge{From: "scales.go::IScale.Weigh", To: "scales.go::IScale", Kind: graph.EdgeMemberOf})
g.AddNode(&graph.Node{ID: "scales.go::DrumScale", Kind: graph.KindType, Name: "DrumScale",
FilePath: "scales.go", StartLine: 6, EndLine: 8, Language: "go"})
g.AddNode(&graph.Node{ID: "scales.go::DrumScale.Weigh", Kind: graph.KindMethod, Name: "Weigh",
FilePath: "scales.go", StartLine: 7, EndLine: 7, Language: "go"})
g.AddEdge(&graph.Edge{From: "scales.go::DrumScale.Weigh", To: "scales.go::DrumScale", Kind: graph.EdgeMemberOf})
g.AddEdge(&graph.Edge{From: "scales.go::DrumScale", To: "scales.go::IScale", Kind: graph.EdgeImplements})
g.AddNode(&graph.Node{ID: "station.go::station", Kind: graph.KindFunction, Name: "station",
FilePath: "station.go", StartLine: 2, EndLine: 2, Language: "go"})
implEdge := &graph.Edge{
From: "station.go::station", To: "scales.go::DrumScale.Weigh", Kind: graph.EdgeCalls,
FilePath: "station.go", Line: 2,
Confidence: 0.7, ConfidenceLabel: "INFERRED", Origin: graph.OriginASTInferred,
}
g.AddEdge(implEdge)
server.handle("textDocument/definition", func(json.RawMessage) (any, *jsonRPCError) {
// The compiler's answer: the interface's declared member (0-based 2).
return []Location{{
URI: pathToURI(filepath.Join(repoRoot, "scales.go")),
Range: Range{Start: Position{Line: 2, Character: 1}, End: Position{Line: 2, Character: 6}},
}}, nil
})
server.handle("textDocument/hover", func(json.RawMessage) (any, *jsonRPCError) { return nil, nil })
return implEdge
}
// A dispatched call site: definition answers the interface's declared member
// while the stored edge targets one concrete impl. The declared-member edge is
// added at LSP grade; the impl edge — an inference the compiler cannot vouch
// for — is left exactly as it was: not retargeted, not demoted, not promoted.
func TestLSP_Enrich_DefConfirm_DispatchedCallAddsDeclaredMemberEdge(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
g := graph.New()
server := newFakeLSPServer()
implEdge := dispatchedCallFixture(t, repoRoot, g, server)
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
p.noHeavyRequests = true
require.NoError(t, runEnrich(t, p, g, repoRoot, 5*time.Second))
assert.Equal(t, "scales.go::DrumScale.Weigh", implEdge.To,
"the impl edge keeps its target — the devirtualization guess is not destroyed")
assert.Equal(t, graph.OriginASTInferred, implEdge.Origin,
"the impl edge keeps its origin — an inference stays labeled as one")
assert.Equal(t, 0.7, implEdge.Confidence)
var declared *graph.Edge
for _, e := range g.GetOutEdges("station.go::station") {
if e.To == "scales.go::IScale.Weigh" && e.Kind == graph.EdgeCalls {
declared = e
}
}
require.NotNil(t, declared, "the compiler-proven edge to the declared member must be added")
assert.Equal(t, graph.OriginLSPResolved, declared.Origin)
assert.Equal(t, 2, declared.Line, "the added edge carries the call site")
}
// The declared-member arm lives in the shared definition-rebind pass, so it is
// default-path behavior, not part of the heavy opt-out. With the heavy legs ON
// and references answering empty, the unconfirmed site falls through to the
// definition pass — and the verdict must be the same add-not-rebind: previously
// this path REBOUND the impl edge to the declared member, so a gopls/tsserver/
// pyright repo now keeps the devirtualization guess AND gains the declared edge.
func TestLSP_Enrich_HeavyDefault_DispatchedCallAddsDeclaredMemberEdge(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
g := graph.New()
server := newFakeLSPServer()
implEdge := dispatchedCallFixture(t, repoRoot, g, server)
// The references sweep runs first and must yield no verdict, leaving the
// site to the definition pass.
refs := &atomic.Int64{}
server.handle("textDocument/references", func(json.RawMessage) (any, *jsonRPCError) {
refs.Add(1)
return []Location{}, nil
})
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
// noHeavyRequests stays false — this is every spec-less server's path.
require.NoError(t, runEnrich(t, p, g, repoRoot, 5*time.Second))
assert.Positive(t, refs.Load(), "the default path must still run its references sweep")
assert.Equal(t, "scales.go::DrumScale.Weigh", implEdge.To,
"the impl edge keeps its target — the devirtualization guess is not destroyed")
assert.Equal(t, graph.OriginASTInferred, implEdge.Origin,
"the impl edge keeps its origin — an inference stays labeled as one")
assert.Equal(t, 0.7, implEdge.Confidence)
var declared *graph.Edge
for _, e := range g.GetOutEdges("station.go::station") {
if e.To == "scales.go::IScale.Weigh" && e.Kind == graph.EdgeCalls {
declared = e
}
}
require.NotNil(t, declared, "the compiler-proven edge must be added on the default path too")
assert.Equal(t, graph.OriginLSPResolved, declared.Origin)
assert.Equal(t, 2, declared.Line, "the added edge carries the call site")
}
// GORTEX_LSP_HEAVY overrides the heavy-request opt-out in both directions:
// "on" restores references / incomingCalls for a spec that opted out (the
// operator runs a patched server without the leak), "off" force-disables
// them for every server. Empty or unrecognised falls through to the spec.
func TestResolveNoHeavyRequests_Precedence(t *testing.T) {
csharp := SpecByName("omnisharp")
require.NotNil(t, csharp)
t.Run("default follows the spec", func(t *testing.T) {
t.Setenv(HeavyRequestsEnv, "")
assert.False(t, resolveNoHeavyRequests(nil), "spec-less providers keep the heavy legs")
assert.True(t, resolveNoHeavyRequests(csharp))
})
t.Run("env on re-enables heavies for an opted-out spec", func(t *testing.T) {
t.Setenv(HeavyRequestsEnv, "on")
assert.False(t, resolveNoHeavyRequests(csharp))
})
t.Run("env off disables heavies for every server", func(t *testing.T) {
t.Setenv(HeavyRequestsEnv, "off")
assert.True(t, resolveNoHeavyRequests(nil))
})
t.Run("unrecognised value falls through to the spec", func(t *testing.T) {
t.Setenv(HeavyRequestsEnv, "garbage")
assert.True(t, resolveNoHeavyRequests(csharp))
assert.False(t, resolveNoHeavyRequests(nil))
})
t.Run("constructors plumb the override", func(t *testing.T) {
t.Setenv(HeavyRequestsEnv, "on")
p := NewProviderFromSpec(csharp, nil)
assert.False(t, p.noHeavyRequests, "a patched server's operator can restore references/incoming")
t.Setenv(HeavyRequestsEnv, "off")
def := NewProvider("fake-lsp", nil, []string{"go"}, false, 2, nil)
assert.True(t, def.noHeavyRequests)
})
}
// The definition pass fans out across site files. The serial loop predates
// the NoDidOpen lifecycle — it existed to keep document opens from
// overlapping — but with no didOpen to serialize, a heavy-opt-out server
// answering 46k definitions one at a time is the pass's longest pole. Six
// site files with a slow definition handler must overlap.
func TestLSP_Enrich_DefinitionPassRunsSiteFilesInParallel(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
g := graph.New()
const n = 6
for i := 0; i < n; i++ {
defFile := fmt.Sprintf("def%d.go", i)
callFile := fmt.Sprintf("call%d.go", i)
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, defFile),
[]byte(fmt.Sprintf("package p\nfunc target%d() {}\n", i)), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, callFile),
[]byte(fmt.Sprintf("package p\nfunc caller%d() { target%d() }\n", i, i)), 0o644))
g.AddNode(&graph.Node{ID: fmt.Sprintf("%s::target%d", defFile, i), Kind: graph.KindFunction,
Name: fmt.Sprintf("target%d", i), FilePath: defFile, StartLine: 2, EndLine: 2, Language: "go"})
g.AddNode(&graph.Node{ID: fmt.Sprintf("%s::caller%d", callFile, i), Kind: graph.KindFunction,
Name: fmt.Sprintf("caller%d", i), FilePath: callFile, StartLine: 2, EndLine: 2, Language: "go"})
g.AddEdge(&graph.Edge{
From: fmt.Sprintf("%s::caller%d", callFile, i), To: fmt.Sprintf("%s::target%d", defFile, i),
Kind: graph.EdgeCalls, FilePath: callFile, Line: 2,
Confidence: 0.7, ConfidenceLabel: "INFERRED", Origin: graph.OriginTextMatched,
})
}
// The instrumented rig dispatches each request in its own goroutine, so
// concurrency is genuinely observable (fakeLSPServer answers inline in
// its read loop and would pin in-flight at 1 regardless of the client).
server := newInstrumentedServer()
var inflight, maxInflight atomic.Int64
server.handle("textDocument/definition", func(params json.RawMessage) (any, *jsonRPCError) {
cur := inflight.Add(1)
for {
prev := maxInflight.Load()
if cur <= prev || maxInflight.CompareAndSwap(prev, cur) {
break
}
}
time.Sleep(40 * time.Millisecond)
inflight.Add(-1)
var req struct {
TextDocument struct {
URI string `json:"uri"`
} `json:"textDocument"`
}
_ = json.Unmarshal(params, &req)
var idx int
if _, err := fmt.Sscanf(filepath.Base(uriToAbsPath(req.TextDocument.URI)), "call%d.go", &idx); err != nil {
return []Location{}, nil
}
return []Location{{
URI: pathToURI(filepath.Join(repoRoot, fmt.Sprintf("def%d.go", idx))),
Range: Range{Start: Position{Line: 1, Character: 5}, End: Position{Line: 1, Character: 12}},
}}, nil
})
server.handle("textDocument/hover", func(json.RawMessage) (any, *jsonRPCError) { return nil, nil })
p, cleanup := providerWithInstrumentedServer(t, server, []string{"go"}, 4)
defer cleanup()
p.noHeavyRequests = true
require.NoError(t, runEnrich(t, p, g, repoRoot, 10*time.Second))
assert.GreaterOrEqual(t, maxInflight.Load(), int64(2),
"definition requests for distinct site files must overlap")
confirmed := 0
for i := 0; i < n; i++ {
for _, e := range g.GetOutEdges(fmt.Sprintf("call%d.go::caller%d", i, i)) {
if e.Kind == graph.EdgeCalls && e.Origin == graph.OriginLSPResolved {
confirmed++
}
}
}
assert.Equal(t, n, confirmed, "parallelism must not cost any confirm verdict")
}
// The no-heavy fallback must include every SITED target, not just those the
// references sweep could serve: groupConfirmTargets drops targets on
// referent-side conditions (no position, unserved referent file) that exist
// only because findReferences opens the referent's file. The definition pass
// opens the call-site file, so a misbound edge whose stored target has no
// usable position must still reach the pass — and get rebound to the real
// declaration.
func TestLSP_Enrich_NoHeavy_RebindsSitedEdgeWithPositionlessTarget(t *testing.T) {
t.Setenv(SweepEnv, "full")
repoRoot := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "def.go"),
[]byte("package p\nfunc target() {}\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "call.go"),
[]byte("package p\nfunc caller() { target() }\n"), 0o644))
g := graph.New()
g.AddNode(&graph.Node{ID: "def.go::target", Kind: graph.KindFunction, Name: "target",
FilePath: "def.go", StartLine: 2, EndLine: 2, Language: "go"})
// The heuristically-bound target: same name, NO position — the exact
// shape groupConfirmTargets cannot serve.
g.AddNode(&graph.Node{ID: "ghost.go::target", Kind: graph.KindFunction, Name: "target",
FilePath: "ghost.go", Language: "go"})
g.AddNode(&graph.Node{ID: "call.go::caller", Kind: graph.KindFunction, Name: "caller",
FilePath: "call.go", StartLine: 2, EndLine: 2, Language: "go"})
edge := &graph.Edge{
From: "call.go::caller", To: "ghost.go::target", Kind: graph.EdgeCalls,
FilePath: "call.go", Line: 2,
Confidence: 0.7, ConfidenceLabel: "INFERRED", Origin: graph.OriginTextMatched,
}
g.AddEdge(edge)
server := newFakeLSPServer()
server.handle("textDocument/definition", func(json.RawMessage) (any, *jsonRPCError) {
return []Location{{
URI: pathToURI(filepath.Join(repoRoot, "def.go")),
Range: Range{Start: Position{Line: 1, Character: 5}, End: Position{Line: 1, Character: 11}},
}}, nil
})
server.handle("textDocument/hover", func(json.RawMessage) (any, *jsonRPCError) { return nil, nil })
p, cleanup := providerWithFakeServer(t, server, []string{"go"})
defer cleanup()
p.noHeavyRequests = true
require.NoError(t, runEnrich(t, p, g, repoRoot, 5*time.Second))
assert.Equal(t, "def.go::target", edge.To,
"the sited edge must be rebound to the declaration the compiler names")
assert.Equal(t, 1.0, edge.Confidence)
}