package infra import ( "context" "os" "path/filepath" "testing" "time" ) func TestWatchDirFsnotify(t *testing.T) { t.Run("detecta escritura de archivo", func(t *testing.T) { tmpDir := t.TempDir() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() ch, err := WatchDirFsnotify(ctx, tmpDir) if err != nil { t.Fatalf("WatchDirFsnotify: %v", err) } // Dar tiempo al watcher para arrancar time.Sleep(50 * time.Millisecond) // Escribir un archivo testFile := filepath.Join(tmpDir, "test.md") if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil { t.Fatalf("WriteFile: %v", err) } // Esperar evento (debounce 200ms + margen) select { case ev, ok := <-ch: if !ok { t.Fatal("channel closed unexpectedly") } if ev.Path != testFile { t.Errorf("Path: got %q, want %q", ev.Path, testFile) } if ev.Op != "create" && ev.Op != "write" { t.Errorf("Op: got %q, want 'create' or 'write'", ev.Op) } case <-ctx.Done(): t.Fatal("timeout waiting for fs event") } }) t.Run("canal se cierra cuando ctx cancela", func(t *testing.T) { tmpDir := t.TempDir() ctx, cancel := context.WithCancel(context.Background()) ch, err := WatchDirFsnotify(ctx, tmpDir) if err != nil { t.Fatalf("WatchDirFsnotify: %v", err) } // Cancelar inmediatamente cancel() // El canal debe cerrarse timeout := time.After(2 * time.Second) // Drenar cualquier evento pendiente hasta que el canal se cierre for { select { case _, ok := <-ch: if !ok { return // canal cerrado correctamente } case <-timeout: t.Fatal("channel not closed after ctx cancel within 2s") } } }) t.Run("error en directorio inexistente", func(t *testing.T) { ctx := context.Background() _, err := WatchDirFsnotify(ctx, "/nonexistent/dir/that/does/not/exist") if err == nil { t.Error("expected error for nonexistent directory") } }) t.Run("debounce agrupa multiples escrituras", func(t *testing.T) { tmpDir := t.TempDir() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() ch, err := WatchDirFsnotify(ctx, tmpDir) if err != nil { t.Fatalf("WatchDirFsnotify: %v", err) } time.Sleep(50 * time.Millisecond) testFile := filepath.Join(tmpDir, "debounce.md") // Escribir 5 veces rapidamente for i := 0; i < 5; i++ { _ = os.WriteFile(testFile, []byte("content"), 0644) time.Sleep(10 * time.Millisecond) } // Esperar debounce + margen time.Sleep(400 * time.Millisecond) // Debe haber llegado al menos un evento pero no 5 eventCount := 0 drain: for { select { case _, ok := <-ch: if !ok { break drain } eventCount++ default: break drain } } if eventCount == 0 { t.Error("expected at least one debounced event") } if eventCount >= 5 { t.Errorf("debounce failed: got %d events, expected fewer than 5", eventCount) } }) }