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

138 lines
5.4 KiB
Go

package review
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/astquery"
"github.com/zzet/gortex/internal/graph"
)
// newGroundGraph builds a tiny in-memory graph:
//
// - app/svc.go::WithLoop — a function with loop_depth=2 (a real loop)
// - app/svc.go::NoLoop — a function with no loop metadata
// - app/svc.go::Mutates — a function with a resolved write edge
// - app/svc.go::ReadOnly — a function with no mutating edge (only a call)
func newGroundGraph() graph.Store {
g := graph.New()
g.AddNode(&graph.Node{
ID: "app/svc.go::WithLoop", Kind: graph.KindFunction, Name: "WithLoop",
FilePath: "app/svc.go", Language: "go", Meta: map[string]any{"loop_depth": 2},
})
g.AddNode(&graph.Node{
ID: "app/svc.go::NoLoop", Kind: graph.KindFunction, Name: "NoLoop",
FilePath: "app/svc.go", Language: "go",
})
g.AddNode(&graph.Node{
ID: "app/svc.go::Mutates", Kind: graph.KindFunction, Name: "Mutates",
FilePath: "app/svc.go", Language: "go",
})
g.AddNode(&graph.Node{
ID: "app/svc.go::ReadOnly", Kind: graph.KindFunction, Name: "ReadOnly",
FilePath: "app/svc.go", Language: "go",
})
g.AddNode(&graph.Node{
ID: "app/svc.go::cache", Kind: graph.KindField, Name: "cache",
FilePath: "app/svc.go", Language: "go",
})
// Mutates writes the shared field; ReadOnly only calls something.
g.AddEdge(&graph.Edge{From: "app/svc.go::Mutates", To: "app/svc.go::cache", Kind: graph.EdgeWrites})
g.AddEdge(&graph.Edge{From: "app/svc.go::ReadOnly", To: "app/svc.go::cache", Kind: graph.EdgeReads})
return g
}
func TestGroundLoopCall(t *testing.T) {
g := newGroundGraph()
// Graph-confirmed N+1: the enclosing function has a loop.
keep := GroundLoopCall(g, astquery.Match{Detector: "go-loop-query-call", SymbolID: "app/svc.go::WithLoop"})
require.True(t, keep, "an N+1 inside a function with loop_depth>=1 must be kept")
// Graph-refuted N+1: the enclosing function provably has no loop.
keep = GroundLoopCall(g, astquery.Match{Detector: "go-loop-query-call", SymbolID: "app/svc.go::NoLoop"})
require.False(t, keep, "an N+1 whose enclosing function has no loop must be dropped")
// No symbol / nil graph → keep (AST verdict stands).
require.True(t, GroundLoopCall(g, astquery.Match{Detector: "go-loop-query-call"}))
require.True(t, GroundLoopCall(nil, astquery.Match{Detector: "go-loop-query-call", SymbolID: "x"}))
}
func TestGroundCheckThenAct(t *testing.T) {
g := newGroundGraph()
keep := GroundCheckThenAct(g, astquery.Match{Detector: "go-check-then-act-map", SymbolID: "app/svc.go::Mutates"})
require.True(t, keep, "a check-then-act in a function that writes state must be kept")
keep = GroundCheckThenAct(g, astquery.Match{Detector: "go-check-then-act-map", SymbolID: "app/svc.go::ReadOnly"})
require.False(t, keep, "a check-then-act in a function with no mutating edge must be dropped")
}
// TestGroundReviewMatches proves the post-pass drops graph-refuted N+1
// / check-then-act rows while keeping genuine ones and passing every
// decidable rule through untouched.
func TestGroundReviewMatches(t *testing.T) {
g := newGroundGraph()
in := []astquery.Match{
// Refuted N+1 — enclosing function has no loop.
{Detector: "go-loop-query-call", File: "app/svc.go", Line: 4, SymbolID: "app/svc.go::NoLoop"},
// Genuine N+1 — enclosing function has a loop.
{Detector: "go-loop-query-call", File: "app/svc.go", Line: 9, SymbolID: "app/svc.go::WithLoop"},
// Refuted check-then-act — enclosing function performs no mutation.
{Detector: "go-check-then-act-map", File: "app/svc.go", Line: 14, SymbolID: "app/svc.go::ReadOnly"},
// Genuine check-then-act — enclosing function writes state.
{Detector: "go-check-then-act-map", File: "app/svc.go", Line: 19, SymbolID: "app/svc.go::Mutates"},
// Decidable rule — always survives, even with an unknown symbol.
{Detector: "go-inverted-err-check", File: "app/svc.go", Line: 24, SymbolID: "app/svc.go::NoLoop"},
// Python decidable rule — survives.
{Detector: "py-self-comparison", File: "app/svc.py", Line: 2, SymbolID: ""},
}
out := GroundReviewMatches(g, in)
kept := map[string]bool{}
for _, m := range out {
kept[m.Detector+"@"+itoa(m.Line)] = true
}
require.False(t, kept["go-loop-query-call@4"], "graph-refuted N+1 must be dropped")
require.True(t, kept["go-loop-query-call@9"], "genuine N+1 must survive")
require.False(t, kept["go-check-then-act-map@14"], "graph-refuted check-then-act must be dropped")
require.True(t, kept["go-check-then-act-map@19"], "genuine check-then-act must survive")
require.True(t, kept["go-inverted-err-check@24"], "decidable rule must pass through")
require.True(t, kept["py-self-comparison@2"], "decidable rule must pass through")
require.Len(t, out, 4, "exactly the two refuted rows should be dropped")
}
// TestGroundReviewMatchesNilGraph confirms a nil store grounds nothing
// — findings degrade to pure-AST behaviour instead of being dropped.
func TestGroundReviewMatchesNilGraph(t *testing.T) {
in := []astquery.Match{
{Detector: "go-loop-query-call", SymbolID: "x"},
{Detector: "go-check-then-act-map", SymbolID: "y"},
}
out := GroundReviewMatches(nil, in)
require.Len(t, out, 2, "nil graph must keep every row")
}
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var b []byte
for n > 0 {
b = append([]byte{byte('0' + n%10)}, b...)
n /= 10
}
if neg {
b = append([]byte{'-'}, b...)
}
return string(b)
}