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
115 lines
3.9 KiB
Go
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))
|
|
}
|