Files
zzet--gortex/internal/indexer/git_watcher_freshness_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

115 lines
3.9 KiB
Go

package indexer
import (
"os"
"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/graph/store_sqlite"
)
// TestGitWatcher_ReconcileUpdatesFreshness is the regression for the
// "repo reported stale after a reconcile event was logged" bug: after a
// commit the git-watcher reconciled the in-memory graph to the new HEAD
// and logged it, but `gortex repos` kept reporting the repo stale. The
// freshness row (repo_index_state) was written only by a FULL index, so
// the incremental reconcile left it pointing at the previous commit —
// only a daemon restart (which re-runs a full index) cleared it.
//
// The fix re-stamps that row at the new HEAD at the end of every
// reconcile. This test proves the persisted IndexedSHA advances to the
// commit the watcher caught up to.
func TestGitWatcher_ReconcileUpdatesFreshness(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git binary not available in PATH")
}
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")
writeFile(t, filepath.Join(repoDir, "a.go"), "package main\n\nfunc Alpha() {}\n")
runGit(t, repoDir, "add", ".")
runGit(t, repoDir, "commit", "-q", "-m", "first")
firstSHA := gitHead(t, repoDir)
// A durable backend is required: the in-memory graph is not a
// RepoIndexStateWriter, so freshness is never persisted there.
store, err := store_sqlite.Open(filepath.Join(t.TempDir(), "store.sqlite"))
require.NoError(t, err)
t.Cleanup(func() { _ = store.Close() })
reader, ok := graph.Store(store).(graph.RepoIndexStateReader)
require.True(t, ok, "sqlite store must implement RepoIndexStateReader")
idx := New(graph.Store(store), newTestRegistry(), config.IndexConfig{Workers: 1}, zap.NewNop())
idx.SetRepoPrefix("repo")
idx.SetRootPath(repoDir)
_, err = idx.IndexCtx(testCtx(), repoDir)
require.NoError(t, err)
// Baseline: the full index recorded the first commit.
st, found, err := reader.GetRepoIndexState("repo")
require.NoError(t, err)
require.True(t, found, "full index must persist a freshness row")
require.Equal(t, firstSHA, st.IndexedSHA, "freshness row must record the indexed commit")
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() })
// A second commit advances HEAD on the watched branch.
writeFile(t, filepath.Join(repoDir, "b.go"), "package main\n\nfunc Beta() {}\n")
runGit(t, repoDir, "add", ".")
runGit(t, repoDir, "commit", "-q", "-m", "second")
secondSHA := gitHead(t, repoDir)
require.NotEqual(t, firstSHA, secondSHA)
select {
case <-drained:
case <-time.After(10 * time.Second):
t.Fatal("git watcher did not reconcile within timeout")
}
st, found, err = reader.GetRepoIndexState("repo")
require.NoError(t, err)
require.True(t, found)
assert.Equal(t, secondSHA, st.IndexedSHA,
"reconcile must re-stamp the freshness row at the new HEAD")
assert.False(t, st.Dirty, "working tree is clean after the commit")
}
// gitHead returns the full HEAD commit SHA of a repo, isolated from any
// machine-global git config the same way runGit is.
func gitHead(t *testing.T, dir string) string {
t.Helper()
cmd := exec.Command("git", "rev-parse", "HEAD")
cmd.Dir = dir
cmd.Env = append(os.Environ(),
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_CONFIG_SYSTEM=/dev/null",
)
out, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "git rev-parse HEAD: %s", string(out))
return strings.TrimSpace(string(out))
}