package indexer import ( "os" "path/filepath" "sync" "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/parser" "github.com/zzet/gortex/internal/parser/languages" ) func setupWatcher(t *testing.T) (string, *Indexer, *Watcher) { t.Helper() dir := t.TempDir() writeTestFile(t, filepath.Join(dir, "main.go"), `package main func Original() {} `) g := graph.New() reg := parser.NewRegistry() reg.Register(languages.NewGoExtractor()) cfg := config.Default() cfg.Index.Workers = 1 idx := New(g, reg, cfg.Index, zap.NewNop()) _, err := idx.Index(dir) require.NoError(t, err) wcfg := config.WatchConfig{ Enabled: true, Paths: []string{dir}, DebounceMs: 50, // short debounce for tests Exclude: []string{"**/*.tmp", "**/.git/**"}, } w, err := NewWatcher(idx, wcfg, zap.NewNop()) require.NoError(t, err) require.NoError(t, w.Start([]string{dir})) t.Cleanup(func() { _ = w.Stop() }) return dir, idx, w } func writeTestFile(t *testing.T, path, content string) { t.Helper() require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) } func waitForEvent(t *testing.T, w *Watcher, timeout time.Duration) GraphChangeEvent { t.Helper() select { case ev := <-w.Events(): return ev case <-time.After(timeout): t.Fatal("timeout waiting for watcher event") return GraphChangeEvent{} } } func TestWatcher_FileModify(t *testing.T) { dir, idx, w := setupWatcher(t) require.NotEmpty(t, idx.graph.FindNodesByName("Original")) // Modify the file. writeTestFile(t, filepath.Join(dir, "main.go"), `package main func Modified() {} `) ev := waitForEvent(t, w, 2*time.Second) assert.Equal(t, ChangeModified, ev.Kind) // Graph should reflect the change. assert.Empty(t, idx.graph.FindNodesByName("Original")) assert.NotEmpty(t, idx.graph.FindNodesByName("Modified")) } func TestWatcher_FileCreate(t *testing.T) { dir, idx, w := setupWatcher(t) nodesBefore := idx.graph.NodeCount() writeTestFile(t, filepath.Join(dir, "new.go"), `package main func NewFunc() {} `) ev := waitForEvent(t, w, 2*time.Second) // fsnotify may emit CREATE or WRITE depending on the OS. assert.Contains(t, []ChangeKind{ChangeCreated, ChangeModified}, ev.Kind) assert.Greater(t, idx.graph.NodeCount(), nodesBefore) assert.NotEmpty(t, idx.graph.FindNodesByName("NewFunc")) } func TestWatcher_FileDelete(t *testing.T) { dir, idx, w := setupWatcher(t) require.NotEmpty(t, idx.graph.FindNodesByName("Original")) require.NoError(t, os.Remove(filepath.Join(dir, "main.go"))) ev := waitForEvent(t, w, 2*time.Second) assert.Equal(t, ChangeDeleted, ev.Kind) assert.Empty(t, idx.graph.FindNodesByName("Original")) } func TestWatcher_History(t *testing.T) { dir, _, w := setupWatcher(t) writeTestFile(t, filepath.Join(dir, "main.go"), `package main func Changed() {} `) _ = waitForEvent(t, w, 2*time.Second) history := w.History() require.Len(t, history, 1) assert.Equal(t, ChangeModified, history[0].Kind) } func TestWatcher_SymbolChangeCallback_Modify(t *testing.T) { dir, _, w := setupWatcher(t) type callbackData struct { filePath string oldSymbols []*graph.Node newSymbols []*graph.Node } var mu sync.Mutex var calls []callbackData w.OnSymbolChange(func(filePath string, oldSymbols, newSymbols []*graph.Node) { mu.Lock() defer mu.Unlock() calls = append(calls, callbackData{filePath, oldSymbols, newSymbols}) }) // Modify the file — changes function name. writeTestFile(t, filepath.Join(dir, "main.go"), `package main func Modified() {} `) _ = waitForEvent(t, w, 2*time.Second) mu.Lock() defer mu.Unlock() require.Len(t, calls, 1) // Old symbols should contain "Original", new should contain "Modified". var oldNames, newNames []string for _, n := range calls[0].oldSymbols { oldNames = append(oldNames, n.Name) } for _, n := range calls[0].newSymbols { if n.Kind != graph.KindFile && n.Kind != graph.KindImport { newNames = append(newNames, n.Name) } } assert.Contains(t, oldNames, "Original") assert.Contains(t, newNames, "Modified") } func TestWatcher_SymbolChangeCallback_Delete(t *testing.T) { dir, _, w := setupWatcher(t) type callbackData struct { filePath string oldSymbols []*graph.Node newSymbols []*graph.Node } var mu sync.Mutex var calls []callbackData w.OnSymbolChange(func(filePath string, oldSymbols, newSymbols []*graph.Node) { mu.Lock() defer mu.Unlock() calls = append(calls, callbackData{filePath, oldSymbols, newSymbols}) }) require.NoError(t, os.Remove(filepath.Join(dir, "main.go"))) _ = waitForEvent(t, w, 2*time.Second) mu.Lock() defer mu.Unlock() require.Len(t, calls, 1) // Old symbols should have entries, new should be nil (deleted). assert.NotEmpty(t, calls[0].oldSymbols) assert.Nil(t, calls[0].newSymbols) }