commit 2578e56de39a4744a75def559258c8c9e275d929
parent 3290272c523f71239e030be9e15a7732264381bd
Author: Erik Loualiche <[email protected]>
Date: Sun, 22 Mar 2026 10:15:51 -0500
feat: watcher skips directories with broken symlinks instead of crashing
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Diffstat:
2 files changed, 45 insertions(+), 8 deletions(-)
diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go
@@ -124,13 +124,14 @@ func (d *Debouncer) Stop() {
// Events are debounced so that a burst of rapid changes results in a single
// call to the configured handler.
type Watcher struct {
- fsw *fsnotify.Watcher
- debouncer *Debouncer
- path string
- rootPath string
- ignores []string
- includes []string
- done chan struct{}
+ fsw *fsnotify.Watcher
+ debouncer *Debouncer
+ path string
+ rootPath string
+ ignores []string
+ includes []string
+ done chan struct{}
+ BrokenSymlinks []BrokenSymlink
}
// New creates a Watcher for the given directory path. debounceMs sets the
@@ -318,7 +319,13 @@ func (w *Watcher) addRecursive(path string) error {
}
if info.IsDir() {
- return w.fsw.Add(p)
+ if err := w.fsw.Add(p); err != nil {
+ if broken := findBrokenSymlinks(p); len(broken) > 0 {
+ w.BrokenSymlinks = append(w.BrokenSymlinks, broken...)
+ return nil // skip dir, continue walking
+ }
+ return err // non-symlink error, propagate
+ }
}
return nil
diff --git a/internal/watcher/watcher_test.go b/internal/watcher/watcher_test.go
@@ -195,3 +195,33 @@ func TestFindBrokenSymlinks(t *testing.T) {
t.Errorf("path base = %q, want %q", filepath.Base(broken[0].Path), "bad.txt")
}
}
+
+// ---------------------------------------------------------------------------
+// 8. TestAddRecursiveSkipsBrokenSymlinks — watcher starts despite broken symlinks
+// ---------------------------------------------------------------------------
+func TestAddRecursiveSkipsBrokenSymlinks(t *testing.T) {
+ dir := t.TempDir()
+ sub := filepath.Join(dir, "subdir")
+ os.Mkdir(sub, 0755)
+
+ // Create a broken symlink inside subdir
+ os.Symlink("/nonexistent/target", filepath.Join(sub, "broken.csv"))
+
+ w, err := New(dir, 100, nil, nil, func() {})
+ if err != nil {
+ t.Fatalf("New: %v", err)
+ }
+ defer w.Stop()
+
+ // Start should succeed despite broken symlinks
+ if err := w.Start(); err != nil {
+ t.Fatalf("Start failed: %v", err)
+ }
+
+ if len(w.BrokenSymlinks) != 1 {
+ t.Fatalf("BrokenSymlinks = %d, want 1", len(w.BrokenSymlinks))
+ }
+ if w.BrokenSymlinks[0].Target != "/nonexistent/target" {
+ t.Errorf("target = %q, want %q", w.BrokenSymlinks[0].Target, "/nonexistent/target")
+ }
+}