Files
fn_registry/functions/infra/watch_dir_fsnotify_test.go
Egutierrez c468b24d2b feat(0130): kanban_cpp v2 — backend Go + 5 registry parser fns + epic/sub-issues
Registry (issue 0130a):
- 5 fns infra: parse_issue_md, write_issue_md, scan_issues_dir,
  scan_flows_dir, watch_dir_fsnotify
- 3 tipos: Issue, Flow, FsEvent
- Tests round-trip + scan reales + watcher fsnotify (all PASS)
- Capability group 'kanban' nuevo (docs/capabilities/kanban.md)

Apps:
- apps/kanban_cpp/ (sub-repo) — frontend ImGui: board drag-drop,
  flows, filters, detail con CSV editors
- apps/kanban_cpp/backend/ — Go service port 8487: REST + SSE +
  fsnotify watcher, parser bidireccional MD<->SQLite cache

Issues:
- dev/issues/0130-kanban-cpp-v2.md (epic)
- 0130a parser, 0130b backend, 0130c frontend

CMakeLists.txt: add_subdirectory apps/kanban_cpp (registrado por
init_cpp_app scaffolder).

End-to-end verde: backend devuelve 189 issues + 9 flows; PATCH a
/api/issues/{id} reescribe .md (solo frontmatter, body intacto);
frontend --self-test exit 0; tests Go infra 5/5 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:20:15 +02:00

130 lines
2.9 KiB
Go

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)
}
})
}