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

236 lines
9.0 KiB
Go

package review
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/analysis"
"github.com/zzet/gortex/internal/graph"
)
// newPackFixture builds a synthetic changeset for the review-pack tiers:
//
// - app/svc.go::Handle — the CHANGED symbol (lines 1..6), diff hunk in tier 1
// - app/svc.go::Caller — a d=1 caller (lines 8..12), full source in tier 2
// - app/api.go::Outer — a d=2 symbol (lines 1..4), outline-only in tier 3
//
// The on-disk files under repoRoot back the tier-2 full-source read; the diff
// text backs the tier-1 hunk slicing.
func newPackFixture(t *testing.T) (graph.Store, *ChangeView, *analysis.DiffResult, *analysis.ImpactResult, string) {
t.Helper()
repoRoot := t.TempDir()
svc := "package svc\n\nfunc Handle() {\n\tdoChanged()\n}\n\nfunc Caller() {\n\tHandle()\n\tlogIt()\n}\n"
api := "package api\n\nfunc Outer() {\n\tCaller()\n}\n"
require.NoError(t, os.MkdirAll(filepath.Join(repoRoot, "app"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "app/svc.go"), []byte(svc), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(repoRoot, "app/api.go"), []byte(api), 0o644))
g := graph.New()
g.AddNode(&graph.Node{
ID: "app/svc.go::Handle", Kind: graph.KindFunction, Name: "Handle",
FilePath: "app/svc.go", Language: "go", StartLine: 3, EndLine: 5,
Meta: map[string]any{"signature": "func Handle()"},
})
g.AddNode(&graph.Node{
ID: "app/svc.go::Caller", Kind: graph.KindFunction, Name: "Caller",
FilePath: "app/svc.go", Language: "go", StartLine: 7, EndLine: 10,
Meta: map[string]any{"signature": "func Caller()"},
})
g.AddNode(&graph.Node{
ID: "app/api.go::Outer", Kind: graph.KindFunction, Name: "Outer",
FilePath: "app/api.go", Language: "go", StartLine: 3, EndLine: 5,
Meta: map[string]any{"signature": "func Outer()"},
})
// A unified diff that changes the body of Handle (lines 3..5 new-side).
diffText := strings.Join([]string{
"diff --git a/app/svc.go b/app/svc.go",
"--- a/app/svc.go",
"+++ b/app/svc.go",
"@@ -3,3 +3,3 @@ package svc",
" func Handle() {",
"-\tdoOld()",
"+\tdoChanged()",
" }",
"",
}, "\n")
view := ChangeViewFromDiff(repoRoot, diffText)
diff := &analysis.DiffResult{
ChangedSymbols: []analysis.ChangedSymbol{
{ID: "app/svc.go::Handle", Name: "Handle", Kind: "function", FilePath: "app/svc.go", Line: 3},
},
ChangedFiles: []string{"app/svc.go"},
}
impact := &analysis.ImpactResult{
ByDepth: map[int][]analysis.ImpactEntry{
1: {{ID: "app/svc.go::Caller", Name: "Caller", Kind: "function", FilePath: "app/svc.go", Line: 7}},
2: {{ID: "app/api.go::Outer", Name: "Outer", Kind: "function", FilePath: "app/api.go", Line: 3}},
},
}
return g, view, diff, impact, repoRoot
}
func TestBuildReviewPack_Tiers(t *testing.T) {
g, view, diff, impact, _ := newPackFixture(t)
pack := BuildReviewPack(g, view, diff, impact, 0) // no budget — every tier kept
// Tier 1: the changed symbol, as diff-hunk text.
require.Len(t, pack.Changed, 1)
c := pack.Changed[0]
require.Equal(t, "app/svc.go::Handle", c.ID)
require.Equal(t, TierChanged, c.Tier)
require.Contains(t, c.Diff, "+\tdoChanged()", "tier-1 entry must carry the added line")
require.Contains(t, c.Diff, "-\tdoOld()", "tier-1 entry must carry the removed line")
require.Empty(t, c.Source, "tier-1 entry must not carry full source")
// Tier 2: the d=1 caller, as FULL source (not a hunk, not an outline).
require.Len(t, pack.Callers, 1)
cl := pack.Callers[0]
require.Equal(t, "app/svc.go::Caller", cl.ID)
require.Equal(t, TierCaller, cl.Tier)
require.Contains(t, cl.Source, "func Caller()", "tier-2 entry must carry full source")
require.Contains(t, cl.Source, "Handle()", "tier-2 full source spans the caller body")
require.Empty(t, cl.Diff)
// Tier 3: the d=2 symbol, as an OUTLINE (signature only).
require.Len(t, pack.Outline, 1)
o := pack.Outline[0]
require.Equal(t, "app/api.go::Outer", o.ID)
require.Equal(t, TierOutline, o.Tier)
require.Equal(t, "func Outer()", o.Signature, "tier-3 entry must carry the signature")
require.Empty(t, o.Source)
require.Empty(t, o.Diff)
require.False(t, pack.Truncated, "an unbounded pack is never truncated")
}
// TestBuildReviewPack_BudgetDemotesOutlineFirst proves the budget fills tier 1,
// then tier 2, then tier 3 — and demotes tier 3 before tier 2 when tight.
func TestBuildReviewPack_BudgetDemotesOutlineFirst(t *testing.T) {
g, view, diff, impact, _ := newPackFixture(t)
full := BuildReviewPack(g, view, diff, impact, 0)
changedCost := 0
for _, e := range full.Changed {
changedCost += entryTokens(e)
}
callerCost := 0
for _, e := range full.Callers {
callerCost += entryTokens(e)
}
// A budget that fits tier 1 + tier 2 but leaves no room for tier 3.
budget := changedCost + callerCost + 1
pack := BuildReviewPack(g, view, diff, impact, budget)
require.Len(t, pack.Changed, 1, "tier 1 always survives")
require.Len(t, pack.Callers, 1, "tier 2 fits and survives")
require.Empty(t, pack.Outline, "tier 3 is demoted first when the budget is tight")
require.True(t, pack.Truncated, "dropping any entry sets truncated")
}
// TestBuildReviewPack_BudgetDemotesCallerNext proves tier 2 is demoted after
// tier 3 but before tier 1: a budget that only fits tier 1 keeps the change and
// drops both callers and outline.
func TestBuildReviewPack_BudgetDemotesCallerNext(t *testing.T) {
g, view, diff, impact, _ := newPackFixture(t)
full := BuildReviewPack(g, view, diff, impact, 0)
changedCost := 0
for _, e := range full.Changed {
changedCost += entryTokens(e)
}
// A budget that fits only tier 1.
budget := changedCost + 1
pack := BuildReviewPack(g, view, diff, impact, budget)
require.Len(t, pack.Changed, 1, "tier 1 always survives")
require.Empty(t, pack.Callers, "tier 2 is demoted when only tier 1 fits")
require.Empty(t, pack.Outline, "tier 3 is demoted first")
require.True(t, pack.Truncated)
}
// TestBuildReviewPack_ChangedSurvivesTinyBudget proves the change itself is never
// dropped, even when a single changed entry overruns the whole budget.
func TestBuildReviewPack_ChangedSurvivesTinyBudget(t *testing.T) {
g, view, diff, impact, _ := newPackFixture(t)
pack := BuildReviewPack(g, view, diff, impact, 1) // budget smaller than any entry
require.Len(t, pack.Changed, 1, "the change is kept even over budget")
require.Empty(t, pack.Callers)
require.Empty(t, pack.Outline)
require.True(t, pack.Truncated)
}
// TestBuildReviewPack_RenderDeterministic proves the rendered pack is byte-stable
// across builds — entries are pre-sorted by id.
func TestBuildReviewPack_RenderDeterministic(t *testing.T) {
g, view, diff, impact, _ := newPackFixture(t)
a := BuildReviewPack(g, view, diff, impact, 0).Render()
b := BuildReviewPack(g, view, diff, impact, 0).Render()
require.Equal(t, a, b, "render must be deterministic")
// The render places the tiers in order with their headers.
require.Contains(t, a, "## changed (diff)")
require.Contains(t, a, "## callers (full source)")
require.Contains(t, a, "## outline (signatures)")
require.True(t,
strings.Index(a, "## changed") < strings.Index(a, "## callers") &&
strings.Index(a, "## callers") < strings.Index(a, "## outline"),
"tiers render in changed→callers→outline order")
}
// TestBuildReviewPack_OutlineSortedDeterministic proves multiple tier-3 entries
// are emitted in a stable id order regardless of impact-map iteration order.
func TestBuildReviewPack_OutlineSortedDeterministic(t *testing.T) {
g, view, diff, _, _ := newPackFixture(t)
g.AddNode(&graph.Node{
ID: "app/api.go::Alpha", Kind: graph.KindFunction, Name: "Alpha",
FilePath: "app/api.go", Language: "go", StartLine: 7, EndLine: 8,
Meta: map[string]any{"signature": "func Alpha()"},
})
impact := &analysis.ImpactResult{
ByDepth: map[int][]analysis.ImpactEntry{
2: {
{ID: "app/api.go::Outer", Name: "Outer", FilePath: "app/api.go", Line: 3},
{ID: "app/api.go::Alpha", Name: "Alpha", FilePath: "app/api.go", Line: 7},
},
},
}
pack := BuildReviewPack(g, view, diff, impact, 0)
require.Len(t, pack.Outline, 2)
require.Equal(t, "app/api.go::Alpha", pack.Outline[0].ID, "outline sorted by id")
require.Equal(t, "app/api.go::Outer", pack.Outline[1].ID)
}
// TestBuildReviewPack_FallbackHunkWhenNoView proves a changed symbol still yields
// a +/- diff (a pure-addition unified diff) when the ChangeView has no lines for
// it — the off-disk / no-hunk fallback.
func TestBuildReviewPack_FallbackHunkWhenNoView(t *testing.T) {
g, _, diff, impact, repoRoot := newPackFixture(t)
// A view rooted at the real repo but with NO recorded diff lines for the
// changed file forces the unified-diff fallback over the symbol's source.
view := &ChangeView{RepoRoot: repoRoot, ByFile: map[string]*FileChange{}}
pack := BuildReviewPack(g, view, diff, impact, 0)
require.Len(t, pack.Changed, 1)
require.NotEmpty(t, pack.Changed[0].Diff, "fallback must still produce hunk text")
require.Contains(t, pack.Changed[0].Diff, "+func Handle()", "fallback renders the new source as additions")
}