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
169 lines
7.1 KiB
Go
169 lines
7.1 KiB
Go
package analysis
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func buildTestGraph() *graph.Graph {
|
|
g := graph.New()
|
|
|
|
// auth module
|
|
g.AddNode(&graph.Node{ID: "auth.go", Kind: graph.KindFile, Name: "auth.go", FilePath: "pkg/auth/auth.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "auth.go::ValidateToken", Kind: graph.KindFunction, Name: "ValidateToken", FilePath: "pkg/auth/auth.go", Language: "go", StartLine: 10, EndLine: 30})
|
|
g.AddNode(&graph.Node{ID: "auth.go::ParseClaims", Kind: graph.KindFunction, Name: "ParseClaims", FilePath: "pkg/auth/auth.go", Language: "go", StartLine: 32, EndLine: 50})
|
|
|
|
// handler module
|
|
g.AddNode(&graph.Node{ID: "handler.go", Kind: graph.KindFile, Name: "handler.go", FilePath: "pkg/api/handler.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "handler.go::HandleLogin", Kind: graph.KindFunction, Name: "HandleLogin", FilePath: "pkg/api/handler.go", Language: "go", StartLine: 5, EndLine: 40})
|
|
g.AddNode(&graph.Node{ID: "handler.go::HandleLogout", Kind: graph.KindFunction, Name: "HandleLogout", FilePath: "pkg/api/handler.go", Language: "go", StartLine: 42, EndLine: 60})
|
|
|
|
// main
|
|
g.AddNode(&graph.Node{ID: "main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "cmd/main.go", Language: "go"})
|
|
g.AddNode(&graph.Node{ID: "main.go::main", Kind: graph.KindFunction, Name: "main", FilePath: "cmd/main.go", Language: "go", StartLine: 1, EndLine: 20})
|
|
|
|
// db module
|
|
g.AddNode(&graph.Node{ID: "db.go::QueryUser", Kind: graph.KindFunction, Name: "QueryUser", FilePath: "pkg/db/db.go", Language: "go", StartLine: 5, EndLine: 20})
|
|
|
|
// Edges
|
|
g.AddEdge(&graph.Edge{From: "main.go::main", To: "handler.go::HandleLogin", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "main.go::main", To: "handler.go::HandleLogout", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "handler.go::HandleLogin", To: "auth.go::ValidateToken", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "handler.go::HandleLogin", To: "db.go::QueryUser", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "auth.go::ValidateToken", To: "auth.go::ParseClaims", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "handler.go::HandleLogout", To: "auth.go::ValidateToken", Kind: graph.EdgeCalls})
|
|
|
|
return g
|
|
}
|
|
|
|
func TestDetectCommunities(t *testing.T) {
|
|
g := buildTestGraph()
|
|
result := DetectCommunities(g)
|
|
|
|
require.NotNil(t, result)
|
|
// With this small graph, we should get at least 1 community
|
|
assert.GreaterOrEqual(t, len(result.Communities), 1)
|
|
assert.NotEmpty(t, result.NodeToComm)
|
|
|
|
// Check that communities have members
|
|
for _, c := range result.Communities {
|
|
assert.NotEmpty(t, c.ID)
|
|
assert.NotEmpty(t, c.Members)
|
|
assert.Greater(t, c.Size, 1)
|
|
assert.GreaterOrEqual(t, c.Cohesion, 0.0)
|
|
assert.LessOrEqual(t, c.Cohesion, 1.0)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverProcesses(t *testing.T) {
|
|
g := buildTestGraph()
|
|
result := DiscoverProcesses(g)
|
|
|
|
require.NotNil(t, result)
|
|
// main and HandleLogin should be discovered as entry points
|
|
assert.GreaterOrEqual(t, len(result.Processes), 1)
|
|
|
|
// Check process structure
|
|
for _, p := range result.Processes {
|
|
assert.NotEmpty(t, p.ID)
|
|
assert.NotEmpty(t, p.Name)
|
|
assert.NotEmpty(t, p.EntryPoint)
|
|
assert.GreaterOrEqual(t, p.StepCount, 2)
|
|
assert.NotEmpty(t, p.Files)
|
|
}
|
|
|
|
// main should trace through handler → auth → parseclaims
|
|
found := false
|
|
for _, p := range result.Processes {
|
|
if p.EntryPoint == "main.go::main" {
|
|
found = true
|
|
assert.GreaterOrEqual(t, p.StepCount, 3)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "main should be discovered as a process entry point")
|
|
}
|
|
|
|
func TestAnalyzeImpact(t *testing.T) {
|
|
g := buildTestGraph()
|
|
communities := DetectCommunities(g)
|
|
processes := DiscoverProcesses(g)
|
|
|
|
result := AnalyzeImpact(g, []string{"auth.go::ValidateToken"}, communities, processes)
|
|
|
|
require.NotNil(t, result)
|
|
assert.NotEmpty(t, result.Risk)
|
|
assert.NotEmpty(t, result.Summary)
|
|
|
|
// ValidateToken is called by HandleLogin and HandleLogout (depth 1)
|
|
d1 := result.ByDepth[1]
|
|
assert.GreaterOrEqual(t, len(d1), 1, "should have at least 1 direct dependent")
|
|
|
|
// At depth 2, main should appear
|
|
d2 := result.ByDepth[2]
|
|
assert.GreaterOrEqual(t, len(d2), 0)
|
|
|
|
assert.Greater(t, result.TotalAffected, 0)
|
|
}
|
|
|
|
// TestAnalyzeImpact_DropsHeuristicNoiseAtTransitiveDepths pins the
|
|
// regression: a leaf symbol used to surface as "90 transitively
|
|
// affected" because a heuristic / text-matched edge at d=1 fanned
|
|
// out through high-confidence d=2 edges into hundreds of unrelated
|
|
// downstream methods. Now d=2 and d=3 drop entries whose
|
|
// representative edge has confidence == 0 + label "INFERRED"
|
|
// (resolution edges without type info — the noise class). d=1
|
|
// stays untouched.
|
|
func TestAnalyzeImpact_DropsHeuristicNoiseAtTransitiveDepths(t *testing.T) {
|
|
g := graph.New()
|
|
target := &graph.Node{ID: "p/target.go::Target", Name: "Target", Kind: graph.KindFunction, FilePath: "p/target.go"}
|
|
caller := &graph.Node{ID: "p/caller.go::Caller", Name: "Caller", Kind: graph.KindFunction, FilePath: "p/caller.go"}
|
|
intermediate := &graph.Node{ID: "p/middle.go::Middle", Name: "Middle", Kind: graph.KindFunction, FilePath: "p/middle.go"}
|
|
heuristic := &graph.Node{ID: "p/noise.go::Noise", Name: "Noise", Kind: graph.KindFunction, FilePath: "p/noise.go"}
|
|
g.AddNode(target)
|
|
g.AddNode(caller)
|
|
g.AddNode(intermediate)
|
|
g.AddNode(heuristic)
|
|
// Direct caller: real call, high-conf.
|
|
g.AddEdge(&graph.Edge{From: caller.ID, To: target.ID, Kind: graph.EdgeCalls, Confidence: 0.95})
|
|
// d=2 caller arriving via a CONFIDENT edge.
|
|
g.AddEdge(&graph.Edge{From: intermediate.ID, To: caller.ID, Kind: graph.EdgeCalls, Confidence: 0.95})
|
|
// d=2 NOISE entry: heuristic edge with no type info — exactly
|
|
// the shape the user reported as flooding the response.
|
|
g.AddEdge(&graph.Edge{From: heuristic.ID, To: caller.ID, Kind: graph.EdgeCalls, Confidence: 0})
|
|
|
|
result := AnalyzeImpact(g, []string{target.ID}, nil, nil)
|
|
require.NotNil(t, result)
|
|
require.GreaterOrEqual(t, len(result.ByDepth[1]), 1, "direct caller must survive")
|
|
for _, e := range result.ByDepth[2] {
|
|
require.False(t, e.EdgeConfidence == 0 && e.ConfidenceLabel == "INFERRED",
|
|
"d=2 must not include heuristic / text-matched edges (got %s via confidence=%v label=%s)",
|
|
e.ID, e.EdgeConfidence, e.ConfidenceLabel)
|
|
}
|
|
}
|
|
|
|
func TestAnalyzeImpact_RiskLevels(t *testing.T) {
|
|
assert.Equal(t, RiskLow, assessRisk(0, 0))
|
|
assert.Equal(t, RiskLow, assessRisk(1, 1))
|
|
assert.Equal(t, RiskMedium, assessRisk(2, 3))
|
|
assert.Equal(t, RiskHigh, assessRisk(5, 5))
|
|
assert.Equal(t, RiskCritical, assessRisk(10, 10))
|
|
}
|
|
|
|
func TestScoreEntryPoint(t *testing.T) {
|
|
main := &graph.Node{Name: "main", Language: "go", Kind: graph.KindFunction}
|
|
handler := &graph.Node{Name: "HandleLogin", Language: "go", Kind: graph.KindFunction}
|
|
getter := &graph.Node{Name: "getName", Language: "go", Kind: graph.KindFunction}
|
|
|
|
// main with 3 callees, 0 callers should score high
|
|
mainScore := scoreEntryPoint(main, 3, 0)
|
|
handlerScore := scoreEntryPoint(handler, 2, 1)
|
|
getterScore := scoreEntryPoint(getter, 1, 5)
|
|
|
|
assert.Greater(t, mainScore, handlerScore)
|
|
assert.Greater(t, handlerScore, getterScore)
|
|
}
|