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
360 lines
12 KiB
Go
360 lines
12 KiB
Go
package rerank
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// --- BM25 + Semantic signals -----------------------------------------
|
|
|
|
func TestBM25SignalRankToScore(t *testing.T) {
|
|
cases := map[int]float64{
|
|
-1: 0,
|
|
0: 1.0, // top hit normalised to 1.0
|
|
1: 61.0 / 62.0, // 1 / (60 + 2) over the top-norm
|
|
60: 61.0 / 121.0, // half-decay at rank 60
|
|
999: 61.0 / 1060.0, // far tail still > 0
|
|
}
|
|
for rank, want := range cases {
|
|
got := BM25Signal{}.Contribute("q", &Candidate{TextRank: rank}, &Context{})
|
|
if !floatNear(got, want, 1e-9) {
|
|
t.Errorf("rank=%d: got %v, want %v", rank, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSemanticSignalRankToScore(t *testing.T) {
|
|
got := SemanticSignal{}.Contribute("q", &Candidate{VectorRank: 0}, &Context{})
|
|
if !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("top vector rank: got %v want 1.0", got)
|
|
}
|
|
got = SemanticSignal{}.Contribute("q", &Candidate{VectorRank: -1}, &Context{})
|
|
if got != 0 {
|
|
t.Errorf("absent vector rank: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
// --- FanIn / FanOut --------------------------------------------------
|
|
|
|
func TestFanInOutSignalsNormalised(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
b := mustNode(g, "f.go::B", "B", graph.KindFunction)
|
|
c := mustNode(g, "f.go::C", "C", graph.KindFunction)
|
|
|
|
// A has 3 callers, B has 1, C has 0.
|
|
g.AddEdge(&graph.Edge{From: "x1", To: a.ID, Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "x2", To: a.ID, Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "x3", To: a.ID, Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: "x4", To: b.ID, Kind: graph.EdgeCalls})
|
|
// A has 2 callees, B has 0, C has 1.
|
|
g.AddEdge(&graph.Edge{From: a.ID, To: "y1", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: a.ID, To: "y2", Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: c.ID, To: "y3", Kind: graph.EdgeCalls})
|
|
|
|
ctx := &Context{Graph: g}
|
|
cands := []*Candidate{candidateFor(a, 0, -1), candidateFor(b, 1, -1), candidateFor(c, 2, -1)}
|
|
ctx.prepare(cands)
|
|
|
|
fi := FanInSignal{}
|
|
if got := fi.Contribute("q", cands[0], ctx); !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("A fan-in: got %v want 1.0", got)
|
|
}
|
|
if got := fi.Contribute("q", cands[2], ctx); got != 0 {
|
|
t.Errorf("C fan-in: got %v want 0", got)
|
|
}
|
|
|
|
fo := FanOutSignal{}
|
|
if got := fo.Contribute("q", cands[0], ctx); !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("A fan-out: got %v want 1.0", got)
|
|
}
|
|
}
|
|
|
|
func TestFanInSignalNoGraphReturnsZero(t *testing.T) {
|
|
a := &graph.Node{ID: "x", Name: "x", Kind: graph.KindFunction}
|
|
if got := (FanInSignal{}).Contribute("q", &Candidate{Node: a}, &Context{}); got != 0 {
|
|
t.Errorf("no-graph fan-in: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
// --- Churn -----------------------------------------------------------
|
|
|
|
func TestChurnSignalFromMeta(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
b := mustNode(g, "f.go::B", "B", graph.KindFunction)
|
|
a.Meta = map[string]any{"churn": 5}
|
|
b.Meta = map[string]any{"churn": 1}
|
|
|
|
ctx := &Context{Graph: g}
|
|
cands := []*Candidate{candidateFor(a, 0, -1), candidateFor(b, 1, -1)}
|
|
ctx.prepare(cands)
|
|
|
|
cs := ChurnSignal{}
|
|
if got := cs.Contribute("q", cands[0], ctx); !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("hottest churn: got %v want 1.0", got)
|
|
}
|
|
if got := cs.Contribute("q", cands[1], ctx); !(got > 0 && got < 1) {
|
|
t.Errorf("medium churn: got %v want in (0,1)", got)
|
|
}
|
|
}
|
|
|
|
func TestChurnSignalChurnOfHookWins(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
a.Meta = map[string]any{"churn": 1}
|
|
|
|
ctx := &Context{Graph: g, ChurnOf: func(id string) int {
|
|
if id == a.ID {
|
|
return 99
|
|
}
|
|
return 0
|
|
}}
|
|
cands := []*Candidate{candidateFor(a, 0, -1)}
|
|
ctx.prepare(cands)
|
|
if got := (ChurnSignal{}).Contribute("q", cands[0], ctx); !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("ChurnOf override should produce 1.0, got %v", got)
|
|
}
|
|
}
|
|
|
|
// --- Community ------------------------------------------------------
|
|
|
|
func TestCommunitySignalTopicCluster(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
b := mustNode(g, "f.go::B", "B", graph.KindFunction)
|
|
c := mustNode(g, "f.go::C", "C", graph.KindFunction)
|
|
|
|
communities := map[string]string{a.ID: "c1", b.ID: "c1", c.ID: "c2"}
|
|
ctx := &Context{Graph: g, CommunityOf: func(id string) string { return communities[id] }}
|
|
cands := []*Candidate{candidateFor(a, 0, -1), candidateFor(b, 1, -1), candidateFor(c, 2, -1)}
|
|
ctx.prepare(cands)
|
|
|
|
cs := CommunitySignal{}
|
|
// A and B share c1 (cluster size 2), C alone in c2 (size 1).
|
|
// maxCommunityCount = 2, so A and B get 2/2 = 1.0, C gets 0
|
|
// (cluster-of-one is not a cluster).
|
|
if got := cs.Contribute("q", cands[0], ctx); !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("A in topic cluster: got %v want 1.0", got)
|
|
}
|
|
if got := cs.Contribute("q", cands[2], ctx); got != 0 {
|
|
t.Errorf("C alone: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestCommunitySignalRepoLocality(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
b := mustNode(g, "f.go::B", "B", graph.KindFunction)
|
|
a.RepoPrefix = "myrepo"
|
|
b.RepoPrefix = "otherrepo"
|
|
|
|
ctx := &Context{Graph: g, RepoPrefix: "myrepo"}
|
|
cands := []*Candidate{candidateFor(a, 0, -1), candidateFor(b, 1, -1)}
|
|
ctx.prepare(cands)
|
|
|
|
cs := CommunitySignal{}
|
|
if got := cs.Contribute("q", cands[0], ctx); !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("same-repo: got %v want 1.0", got)
|
|
}
|
|
if got := cs.Contribute("q", cands[1], ctx); got != 0 {
|
|
t.Errorf("other-repo: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestCommunitySignalProjectLocalityFallback(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
a.RepoPrefix = "repo1"
|
|
a.ProjectID = "myproj"
|
|
|
|
ctx := &Context{Graph: g, ProjectID: "myproj"}
|
|
cands := []*Candidate{candidateFor(a, 0, -1)}
|
|
ctx.prepare(cands)
|
|
|
|
if got := (CommunitySignal{}).Contribute("q", cands[0], ctx); !floatNear(got, 0.7, 1e-9) {
|
|
t.Errorf("same-project: got %v want 0.7", got)
|
|
}
|
|
}
|
|
|
|
// --- MinHash --------------------------------------------------------
|
|
|
|
func TestMinHashSignalCohesion(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "A", graph.KindFunction)
|
|
b := mustNode(g, "f.go::B", "B", graph.KindFunction)
|
|
c := mustNode(g, "f.go::C", "C", graph.KindFunction)
|
|
|
|
g.AddEdge(&graph.Edge{From: a.ID, To: b.ID, Kind: graph.EdgeSimilarTo, Meta: map[string]any{"similarity": 0.8}})
|
|
g.AddEdge(&graph.Edge{From: b.ID, To: a.ID, Kind: graph.EdgeSimilarTo, Meta: map[string]any{"similarity": 0.8}})
|
|
|
|
ctx := &Context{Graph: g}
|
|
cands := []*Candidate{candidateFor(a, 0, -1), candidateFor(b, 1, -1), candidateFor(c, 2, -1)}
|
|
ctx.prepare(cands)
|
|
|
|
cs := MinHashSignal{}
|
|
// A and B are both in cluster with sim=0.8; C is isolated.
|
|
if got := cs.Contribute("q", cands[0], ctx); !floatNear(got, 0.8, 1e-9) {
|
|
t.Errorf("A in similar cluster: got %v want 0.8", got)
|
|
}
|
|
if got := cs.Contribute("q", cands[2], ctx); got != 0 {
|
|
t.Errorf("C isolated: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
// --- Recency --------------------------------------------------------
|
|
|
|
func TestRecencySignalDecay(t *testing.T) {
|
|
now := time.Now().Unix()
|
|
day := int64(86400)
|
|
cases := []struct {
|
|
dtDays int64
|
|
want float64
|
|
}{
|
|
{0, 1.0},
|
|
{30, 0.5},
|
|
{60, 0.25},
|
|
{120, 0.0625},
|
|
}
|
|
for _, c := range cases {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindFunction, Meta: map[string]any{
|
|
"last_authored": map[string]any{"timestamp": now - c.dtDays*day},
|
|
}}
|
|
got := (RecencySignal{}).Contribute("q", &Candidate{Node: n}, &Context{Now: now})
|
|
if !floatNear(got, c.want, 1e-3) {
|
|
t.Errorf("dt=%dd: got %v want %v", c.dtDays, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecencySignalNoMetaReturnsZero(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindFunction}
|
|
if got := (RecencySignal{}).Contribute("q", &Candidate{Node: n}, &Context{Now: time.Now().Unix()}); got != 0 {
|
|
t.Errorf("no last_authored: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
// --- Signature signals ----------------------------------------------
|
|
|
|
func TestAPISignatureSignalOverlap(t *testing.T) {
|
|
n := &graph.Node{
|
|
ID: "x", Kind: graph.KindFunction, Name: "validateToken",
|
|
Meta: map[string]any{"signature": "func validateToken(t string) error"},
|
|
}
|
|
got := (APISignatureSignal{}).Contribute("validate token", &Candidate{Node: n}, &Context{})
|
|
if !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("full overlap: got %v want 1.0", got)
|
|
}
|
|
got = (APISignatureSignal{}).Contribute("hash password", &Candidate{Node: n}, &Context{})
|
|
if got != 0 {
|
|
t.Errorf("no overlap: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestAPISignatureSignalIgnoresNonFunctions(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindType, Name: "User"}
|
|
if got := (APISignatureSignal{}).Contribute("user", &Candidate{Node: n}, &Context{}); got != 0 {
|
|
t.Errorf("type kind: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestTypeSignatureSignalNameOverlap(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindType, Name: "UserRecord"}
|
|
got := (TypeSignatureSignal{}).Contribute("user record", &Candidate{Node: n}, &Context{})
|
|
if !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("type name overlap: got %v want 1.0", got)
|
|
}
|
|
}
|
|
|
|
func TestTypeSignatureIgnoresFunctions(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindFunction, Name: "ParseUser"}
|
|
if got := (TypeSignatureSignal{}).Contribute("user", &Candidate{Node: n}, &Context{}); got != 0 {
|
|
t.Errorf("function kind: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
// --- Feedback (session) ---------------------------------------------
|
|
|
|
func TestFeedbackSignalMaxOfSources(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindFunction}
|
|
ctx := &Context{
|
|
FeedbackOf: func(string) float64 { return 0.5 },
|
|
FrecencyBoostOf: func(string) float64 { return 1.5 }, // saturates → 1.0
|
|
ComboBoostOf: func(string) float64 { return 1.0 }, // no boost
|
|
}
|
|
got := (FeedbackSignal{}).Contribute("q", &Candidate{Node: n}, ctx)
|
|
if !floatNear(got, 1.0, 1e-9) {
|
|
t.Errorf("max-merge feedback: got %v want 1.0", got)
|
|
}
|
|
}
|
|
|
|
func TestFeedbackSignalNegativeFeedbackIgnored(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindFunction}
|
|
ctx := &Context{
|
|
FeedbackOf: func(string) float64 { return -1.0 },
|
|
}
|
|
if got := (FeedbackSignal{}).Contribute("q", &Candidate{Node: n}, ctx); got != 0 {
|
|
t.Errorf("negative feedback: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestFeedbackSignalAbsentHooksReturnsZero(t *testing.T) {
|
|
n := &graph.Node{ID: "x", Kind: graph.KindFunction}
|
|
if got := (FeedbackSignal{}).Contribute("q", &Candidate{Node: n}, &Context{}); got != 0 {
|
|
t.Errorf("no hooks: got %v want 0", got)
|
|
}
|
|
}
|
|
|
|
// --- End-to-end mini integration ------------------------------------
|
|
|
|
func TestPipelineIntegratesAllSignalsWithoutPanic(t *testing.T) {
|
|
g := newTestGraph()
|
|
a := mustNode(g, "f.go::A", "validateToken", graph.KindFunction)
|
|
a.Meta = map[string]any{
|
|
"signature": "func validateToken(t string) error",
|
|
"churn": 3,
|
|
"last_authored": map[string]any{"timestamp": time.Now().Unix()},
|
|
}
|
|
a.RepoPrefix = "myrepo"
|
|
b := mustNode(g, "f.go::B", "B", graph.KindFunction)
|
|
b.RepoPrefix = "otherrepo"
|
|
|
|
g.AddEdge(&graph.Edge{From: "x1", To: a.ID, Kind: graph.EdgeCalls})
|
|
g.AddEdge(&graph.Edge{From: a.ID, To: b.ID, Kind: graph.EdgeSimilarTo, Meta: map[string]any{"similarity": 0.9}})
|
|
|
|
ctx := &Context{
|
|
Graph: g,
|
|
RepoPrefix: "myrepo",
|
|
CommunityOf: func(string) string { return "c1" },
|
|
ChurnOf: func(string) int { return 0 },
|
|
FeedbackOf: func(string) float64 { return 0.3 },
|
|
FrecencyBoostOf: func(string) float64 { return 1.2 },
|
|
ComboBoostOf: func(string) float64 { return 1.0 },
|
|
}
|
|
p := NewDefault()
|
|
cands := []*Candidate{candidateFor(a, 0, 0), candidateFor(b, 1, 1)}
|
|
out := p.Rerank("validate token", cands, ctx)
|
|
if len(out) != 2 {
|
|
t.Fatalf("expected 2 results, got %d", len(out))
|
|
}
|
|
if out[0].Node.ID != a.ID {
|
|
t.Errorf("expected A first (every signal favors it), got %s", out[0].Node.ID)
|
|
}
|
|
// Every signal should have produced a value for A.
|
|
for _, name := range AllSignalNames() {
|
|
if _, ok := out[0].Signals[name]; !ok {
|
|
t.Errorf("expected signal %q recorded", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func floatNear(a, b, eps float64) bool {
|
|
d := a - b
|
|
if d < 0 {
|
|
d = -d
|
|
}
|
|
return d <= eps
|
|
}
|