c468b24d2b
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>
130 lines
2.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|