Files
zzet--gortex/internal/indexer/git_watcher_scoped_resolve_test.go
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

114 lines
3.7 KiB
Go

package indexer
import (
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/config"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/search"
)
// TestGitWatcher_SmallCommitResolvesIncoming proves a commit-sized ref
// change resolves cross-file *incoming* references — the caller's edge to
// a symbol newly defined in the committed file is lifted from its
// unresolved stub to the real node. This must hold whether the reconcile
// took the scoped incremental path (the default, exercised with a normal
// cap) or fell back to the whole-graph ResolveAll (cap forced to 0), so
// the scoped path is verified to be resolution-equivalent to the global
// one it replaces for small change sets.
func TestGitWatcher_SmallCommitResolvesIncoming(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git binary not available in PATH")
}
cases := []struct {
name string
maxFiles int
}{
{"scoped", 100},
{"wholeGraphFallback", 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
prev := gitWatcherScopedResolveMaxFiles
gitWatcherScopedResolveMaxFiles = tc.maxFiles
t.Cleanup(func() { gitWatcherScopedResolveMaxFiles = prev })
repoDir := t.TempDir()
runGit(t, repoDir, "init", "-q", "-b", "main")
runGit(t, repoDir, "config", "user.email", "test@example.com")
runGit(t, repoDir, "config", "user.name", "Test")
runGit(t, repoDir, "config", "commit.gpgsign", "false")
// main: caller.go calls Target, which nothing defines yet —
// so the call edge starts life as an unresolved stub.
writeFile(t, filepath.Join(repoDir, "caller.go"),
"package main\n\nfunc Caller() { Target() }\n")
runGit(t, repoDir, "add", ".")
runGit(t, repoDir, "commit", "-q", "-m", "main: caller")
// feature: add callee.go defining Target, then return to main.
runGit(t, repoDir, "checkout", "-q", "-b", "feature")
writeFile(t, filepath.Join(repoDir, "callee.go"),
"package main\n\nfunc Target() {}\n")
runGit(t, repoDir, "add", "-A")
runGit(t, repoDir, "commit", "-q", "-m", "feature: callee")
runGit(t, repoDir, "checkout", "-q", "main")
g := graph.New()
idx := New(g, newTestRegistry(), config.IndexConfig{Workers: 1}, zap.NewNop())
idx.search = search.NewBM25()
idx.SetRootPath(repoDir)
_, err := idx.IndexCtx(testCtx(), repoDir)
require.NoError(t, err)
require.Empty(t, g.FindNodesByName("Target"),
"Target must not be defined on main")
gw, err := NewGitWatcher(repoDir, idx, zap.NewNop())
require.NoError(t, err)
gw.debounce = 50 * time.Millisecond
drained := make(chan int, 1)
gw.drained = func(n int) {
select {
case drained <- n:
default:
}
}
require.NoError(t, gw.Start())
t.Cleanup(func() { _ = gw.Stop() })
runGit(t, repoDir, "checkout", "-q", "feature")
select {
case <-drained:
case <-time.After(10 * time.Second):
t.Fatal("git watcher did not reconcile within timeout")
}
targets := g.FindNodesByName("Target")
require.NotEmpty(t, targets, "Target must be indexed after the reconcile")
// The caller's previously-unresolved call edge must now point at
// the real Target node — only the incoming (reverse) resolution
// pass binds a caller in another file to a symbol defined here.
resolved := false
for _, tn := range targets {
for _, e := range g.GetInEdges(tn.ID) {
if e.Kind == graph.EdgeCalls && strings.Contains(e.From, "caller.go") {
resolved = true
}
}
}
assert.True(t, resolved,
"Caller->Target must resolve into callee.go::Target after the reconcile")
})
}
}