internal/watcher/watcher_test.go
Ref: Size: 3.3 KiB
package watcher
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/xanderle/claudealong/internal/ignore"
)
func newTestMatcher(t *testing.T, dir string) *ignore.Matcher {
t.Helper()
m, err := ignore.New(dir)
if err != nil {
t.Fatal(err)
}
return m
}
func TestWatcherFiresOnInPlaceWrite(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "a.go")
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
w, err := New(dir, newTestMatcher(t, dir), 50*time.Millisecond)
if err != nil {
t.Fatal(err)
}
defer w.Close()
if err := w.Start(t.Context()); err != nil {
t.Fatal(err)
}
// Trigger WRITE
time.Sleep(20 * time.Millisecond)
if err := os.WriteFile(p, []byte("y"), 0o644); err != nil {
t.Fatal(err)
}
select {
case ev := <-w.Events():
if ev.Path != p {
t.Errorf("got path %q, want %q", ev.Path, p)
}
case <-time.After(2 * time.Second):
t.Fatal("no event received")
}
}
func TestWatcherFiresOnAtomicSave(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "a.go")
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
w, err := New(dir, newTestMatcher(t, dir), 50*time.Millisecond)
if err != nil {
t.Fatal(err)
}
defer w.Close()
if err := w.Start(t.Context()); err != nil {
t.Fatal(err)
}
time.Sleep(20 * time.Millisecond)
// Atomic-save: write tmpfile + rename over original
tmp := filepath.Join(dir, ".a.go.tmp")
if err := os.WriteFile(tmp, []byte("z"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Rename(tmp, p); err != nil {
t.Fatal(err)
}
select {
case ev := <-w.Events():
if ev.Path != p {
t.Errorf("got path %q, want %q", ev.Path, p)
}
case <-time.After(2 * time.Second):
t.Fatal("no event after atomic save (inode re-watch broken?)")
}
}
func TestWatcherDebouncesBurst(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "a.go")
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
w, err := New(dir, newTestMatcher(t, dir), 200*time.Millisecond)
if err != nil {
t.Fatal(err)
}
defer w.Close()
if err := w.Start(t.Context()); err != nil {
t.Fatal(err)
}
time.Sleep(50 * time.Millisecond)
// Burst of writes within debounce window
for i := range 5 {
os.WriteFile(p, []byte{'a' + byte(i)}, 0o644)
time.Sleep(20 * time.Millisecond)
}
// Drain for 500ms; expect exactly 1 event
count := 0
deadline := time.After(500 * time.Millisecond)
loop:
for {
select {
case <-w.Events():
count++
case <-deadline:
break loop
}
}
if count != 1 {
t.Errorf("got %d events, want 1 (debounced)", count)
}
}
func TestWatcherSkipsIgnoredPaths(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "node_modules"), 0o755); err != nil {
t.Fatal(err)
}
p := filepath.Join(dir, "node_modules", "a.go")
if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
w, err := New(dir, newTestMatcher(t, dir), 50*time.Millisecond)
if err != nil {
t.Fatal(err)
}
defer w.Close()
if err := w.Start(t.Context()); err != nil {
t.Fatal(err)
}
time.Sleep(50 * time.Millisecond)
os.WriteFile(p, []byte("y"), 0o644)
select {
case ev := <-w.Events():
t.Fatalf("unexpected event for ignored path: %q", ev.Path)
case <-time.After(300 * time.Millisecond):
// pass
}
}