Files
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

325 lines
11 KiB
Go

package callpath
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
func newGraph(ids ...string) *graph.Graph {
g := graph.New()
for _, id := range ids {
g.AddNode(&graph.Node{ID: id, Kind: graph.KindFunction, Name: id, FilePath: "test.go"})
}
return g
}
func calls(g *graph.Graph, from, to string) {
g.AddEdge(&graph.Edge{From: from, To: to, Kind: graph.EdgeCalls, FilePath: "test.go", Origin: graph.OriginASTResolved})
}
func callsTier(g *graph.Graph, from, to, origin string) {
g.AddEdge(&graph.Edge{From: from, To: to, Kind: graph.EdgeCalls, FilePath: "test.go", Origin: origin})
}
func TestShortestPath_Direct(t *testing.T) {
g := newGraph("A", "B")
calls(g, "A", "B")
res := New(g).ShortestPath("A", "B", Options{})
if !res.Found || len(res.Paths) != 1 {
t.Fatalf("expected 1 path, got found=%v paths=%d gap=%+v", res.Found, len(res.Paths), res.Gap)
}
p := res.Paths[0]
if p.Length != 1 || len(p.Nodes) != 2 || p.Nodes[0] != "A" || p.Nodes[1] != "B" {
t.Errorf("unexpected path: %+v", p)
}
if p.Confidence <= 0 || p.Confidence > 1 {
t.Errorf("confidence out of range: %v", p.Confidence)
}
}
func TestShortestPath_MultiHop(t *testing.T) {
g := newGraph("A", "B", "C", "D")
calls(g, "A", "B")
calls(g, "B", "C")
calls(g, "C", "D")
res := New(g).ShortestPath("A", "D", Options{})
if !res.Found || len(res.Paths) != 1 {
t.Fatalf("expected found, got %+v", res)
}
got := res.Paths[0].Nodes
want := []string{"A", "B", "C", "D"}
if len(got) != len(want) {
t.Fatalf("unexpected nodes: %v", got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("node %d = %q, want %q (path %v)", i, got[i], want[i], got)
}
}
}
// TestShortestPath_BidirectionalMeet asserts the engine returns the SHORTEST of
// two routes (the length-2 diamond top, not a longer detour).
func TestShortestPath_BidirectionalMeet(t *testing.T) {
g := newGraph("A", "B", "C", "D", "X", "Y", "Z")
// Short route: A->B->D
calls(g, "A", "B")
calls(g, "B", "D")
// Long route: A->C->X->Y->Z->D
calls(g, "A", "C")
calls(g, "C", "X")
calls(g, "X", "Y")
calls(g, "Y", "Z")
calls(g, "Z", "D")
res := New(g).ShortestPath("A", "D", Options{})
if !res.Found {
t.Fatalf("expected found, got %+v", res.Gap)
}
if res.Paths[0].Length != 2 {
t.Errorf("expected shortest length 2, got %d (%v)", res.Paths[0].Length, res.Paths[0].Nodes)
}
}
func TestShortestPath_Disconnected(t *testing.T) {
g := newGraph("A", "B", "C", "D")
calls(g, "A", "B")
calls(g, "C", "D")
res := New(g).ShortestPath("A", "D", Options{})
if res.Found {
t.Fatalf("expected no path, got %+v", res.Paths)
}
if res.Gap == nil || res.Gap.Reason != ReasonDisconnected {
t.Fatalf("expected disconnected, got %+v", res.Gap)
}
if res.Gap.ForwardReached != 1 || res.Gap.BackwardReached != 1 {
t.Errorf("expected 1 reached each side, got fwd=%d bwd=%d", res.Gap.ForwardReached, res.Gap.BackwardReached)
}
if len(res.Gap.FurthestFromSource) == 0 || len(res.Gap.NearestToSink) == 0 {
t.Errorf("expected frontier reports, got %+v", res.Gap)
}
}
func TestShortestPath_DynamicDispatchBoundary(t *testing.T) {
g := newGraph("A", "B", "sink")
calls(g, "A", "B")
// B's only onward call is to an unresolved (dynamic-dispatch) target.
g.AddNode(&graph.Node{ID: "unresolved::handler", Kind: graph.KindFunction, Name: "handler"})
calls(g, "B", "unresolved::handler")
// sink has an incoming edge from an unrelated, unreachable node.
g.AddNode(&graph.Node{ID: "Z", Kind: graph.KindFunction, Name: "Z"})
calls(g, "Z", "sink")
res := New(g).ShortestPath("A", "sink", Options{})
if res.Found {
t.Fatalf("expected no path, got %+v", res.Paths)
}
if res.Gap.Reason != ReasonDynamicDispatch {
t.Fatalf("expected dynamic_dispatch, got %s", res.Gap.Reason)
}
found := false
for _, b := range res.Gap.BoundaryHits {
if b.Target == "unresolved::handler" && b.Reason == boundaryDynamicDispatch {
found = true
}
}
if !found {
t.Errorf("expected boundary hit for unresolved::handler, got %+v", res.Gap.BoundaryHits)
}
}
func TestShortestPath_StubBoundary(t *testing.T) {
g := newGraph("A", "B", "sink")
calls(g, "A", "B")
g.AddNode(&graph.Node{ID: "stdlib::fmt.Println", Kind: graph.KindFunction})
calls(g, "B", "stdlib::fmt.Println")
g.AddNode(&graph.Node{ID: "Z", Kind: graph.KindFunction})
calls(g, "Z", "sink")
res := New(g).ShortestPath("A", "sink", Options{})
if res.Found {
t.Fatalf("expected no path")
}
if res.Gap.Reason != ReasonExternalBoundary {
t.Fatalf("expected external boundary, got %s", res.Gap.Reason)
}
if len(res.Gap.BoundaryHits) == 0 || res.Gap.BoundaryHits[0].Reason != "stdlib" {
t.Errorf("expected stdlib boundary hit, got %+v", res.Gap.BoundaryHits)
}
}
func TestShortestPath_DepthExceeded(t *testing.T) {
g := newGraph("A", "B", "C", "D", "E")
calls(g, "A", "B")
calls(g, "B", "C")
calls(g, "C", "D")
calls(g, "D", "E")
res := New(g).ShortestPath("A", "E", Options{MaxDepth: 2})
if res.Found {
t.Fatalf("expected no path within depth 2, got %+v", res.Paths)
}
if res.Gap.Reason != ReasonDepthExceeded {
t.Fatalf("expected depth_exceeded, got %s", res.Gap.Reason)
}
}
func TestShortestPath_NotFound(t *testing.T) {
g := newGraph("A", "B")
calls(g, "A", "B")
if res := New(g).ShortestPath("MISSING", "B", Options{}); res.Found || res.Gap.Reason != ReasonSrcNotFound {
t.Errorf("expected src_not_found, got %+v", res)
}
if res := New(g).ShortestPath("A", "MISSING", Options{}); res.Found || res.Gap.Reason != ReasonSinkNotFound {
t.Errorf("expected sink_not_found, got %+v", res)
}
}
func TestShortestPath_SrcNoOutSinkNoIn(t *testing.T) {
g := newGraph("A", "B", "C")
calls(g, "B", "C")
// A has no out edges.
if res := New(g).ShortestPath("A", "C", Options{}); res.Found || res.Gap.Reason != ReasonSrcNoOut {
t.Errorf("expected src_no_out, got %+v", res.Gap)
}
// A has out edge to B but B is the target with no incoming... use a fresh
// graph where the sink genuinely has no in-edges.
g2 := newGraph("A", "B", "C")
calls(g2, "A", "B")
if res := New(g2).ShortestPath("A", "C", Options{}); res.Found || res.Gap.Reason != ReasonSinkNoIn {
t.Errorf("expected sink_no_in, got %+v", res.Gap)
}
}
func TestShortestPath_SameNode(t *testing.T) {
g := newGraph("A")
res := New(g).ShortestPath("A", "A", Options{})
if !res.Found || len(res.Paths) != 1 || res.Paths[0].Length != 0 {
t.Errorf("expected trivial 0-length path, got %+v", res)
}
}
func TestShortestPath_KShortest(t *testing.T) {
g := newGraph("A", "B", "C", "D")
// Two equal-length routes A->B->D and A->C->D.
calls(g, "A", "B")
calls(g, "B", "D")
calls(g, "A", "C")
calls(g, "C", "D")
res := New(g).ShortestPath("A", "D", Options{K: 2})
if !res.Found {
t.Fatalf("expected found, got %+v", res.Gap)
}
if len(res.Paths) != 2 {
t.Fatalf("expected 2 paths with K=2, got %d: %+v", len(res.Paths), res.Paths)
}
for _, p := range res.Paths {
if p.Length != 2 {
t.Errorf("expected length-2 path, got %d", p.Length)
}
}
}
func TestShortestPath_KShortest_RankedByConfidence(t *testing.T) {
g := newGraph("A", "B", "C", "D")
// A->B->D is all ast_resolved; A->C->D goes through a text_matched edge.
callsTier(g, "A", "B", graph.OriginASTResolved)
callsTier(g, "B", "D", graph.OriginASTResolved)
callsTier(g, "A", "C", graph.OriginTextMatched)
callsTier(g, "C", "D", graph.OriginASTResolved)
res := New(g).ShortestPath("A", "D", Options{K: 2})
if len(res.Paths) != 2 {
t.Fatalf("expected 2 paths, got %d", len(res.Paths))
}
if res.Paths[0].Nodes[1] != "B" {
t.Errorf("expected higher-confidence path (via B) first, got %v", res.Paths[0].Nodes)
}
if res.Paths[0].Confidence <= res.Paths[1].Confidence {
t.Errorf("expected paths ranked by confidence desc, got %v then %v",
res.Paths[0].Confidence, res.Paths[1].Confidence)
}
}
func TestShortestPath_MinTierPrune(t *testing.T) {
g := newGraph("A", "B", "C")
// Only route A->B->C exists, and B->C is text_matched.
callsTier(g, "A", "B", graph.OriginASTResolved)
callsTier(g, "B", "C", graph.OriginTextMatched)
// Without min_tier the path is found.
if res := New(g).ShortestPath("A", "C", Options{}); !res.Found {
t.Fatalf("expected found without min_tier, got %+v", res.Gap)
}
// With min_tier=ast_resolved the weak edge is pruned and no path remains.
res := New(g).ShortestPath("A", "C", Options{MinTier: graph.OriginASTResolved})
if res.Found {
t.Errorf("expected min_tier prune to drop the path, got %+v", res.Paths)
}
}
func TestShortestPath_EdgeMatchesCrossService(t *testing.T) {
g := newGraph("consumer", "providerContract", "handler")
g.AddNode(&graph.Node{ID: "consumerContract", Kind: graph.KindContract})
// consumer -> calls -> consumerContract -> matches -> providerContract -> calls -> handler
calls(g, "consumer", "consumerContract")
g.AddEdge(&graph.Edge{From: "consumerContract", To: "providerContract", Kind: graph.EdgeMatches, Origin: graph.OriginASTResolved})
calls(g, "providerContract", "handler")
res := New(g).ShortestPath("consumer", "handler", Options{})
if !res.Found {
t.Fatalf("expected cross-service path via EdgeMatches, got %+v", res.Gap)
}
if res.Paths[0].Length != 3 {
t.Errorf("expected length 3, got %d (%v)", res.Paths[0].Length, res.Paths[0].Nodes)
}
}
func TestShortestPath_IncludeReferencesFalse(t *testing.T) {
g := newGraph("A", "B", "C")
calls(g, "A", "B")
// Only wiring from B to C is an EdgeReferences (method-value registration).
g.AddEdge(&graph.Edge{From: "B", To: "C", Kind: graph.EdgeReferences, Origin: graph.OriginASTResolved})
// Default (references included) finds it.
if res := New(g).ShortestPath("A", "C", Options{IncludeReferences: true}); !res.Found {
t.Fatalf("expected path with references, got %+v", res.Gap)
}
// references excluded drops the wiring hop.
if res := New(g).ShortestPath("A", "C", Options{IncludeReferences: false}); res.Found {
t.Errorf("expected no pure-call path, got %+v", res.Paths)
}
}
func TestPathsToAnchor(t *testing.T) {
// c→anchor (len 1); a→b→anchor (len 2); d has no route to anchor.
g := newGraph("a", "b", "c", "d", "anchor")
calls(g, "a", "b")
calls(g, "b", "anchor")
calls(g, "c", "anchor")
got := New(g).PathsToAnchor([]string{"a", "c", "d", "anchor", "a"}, "anchor", Options{})
// d (no path) and anchor (== anchor) and the duplicate a are dropped.
if len(got) != 2 {
t.Fatalf("expected 2 anchored paths, got %d: %+v", len(got), got)
}
// Ordered shortest first: c (len 1) before a (len 2).
if got[0].Root != "c" || got[0].Path.Length != 1 {
t.Errorf("first should be c (len 1), got %+v", got[0])
}
if got[1].Root != "a" || got[1].Path.Length != 2 {
t.Errorf("second should be a (len 2), got %+v", got[1])
}
want := []string{"a", "b", "anchor"}
if len(got[1].Path.Nodes) != 3 {
t.Fatalf("a path nodes = %v", got[1].Path.Nodes)
}
for i := range want {
if got[1].Path.Nodes[i] != want[i] {
t.Errorf("a path = %v, want %v", got[1].Path.Nodes, want)
}
}
}
func TestPathsToAnchor_EmptyAnchor(t *testing.T) {
g := newGraph("a", "anchor")
calls(g, "a", "anchor")
if got := New(g).PathsToAnchor([]string{"a"}, "", Options{}); got != nil {
t.Errorf("empty anchor should return nil, got %+v", got)
}
}