package main import ( "os" "path/filepath" "testing" "time" "github.com/fsnotify/fsnotify" ) func TestWatcher_PathToEvent_IssuesCreate(t *testing.T) { ev := fsnotify.Event{ Name: "/home/x/fn_registry/dev/issues/0119-frontmatter-migration.md", Op: fsnotify.Create, } board, id, action := classifyEvent(ev) if board != "issues" || id != "0119" || action != "created" { t.Fatalf("got (%q,%q,%q), want (issues,0119,created)", board, id, action) } if sseEventForAction(action) != "card_added" { t.Fatalf("sseEventForAction(created) != card_added") } } func TestWatcher_PathToEvent_FlowsRename(t *testing.T) { ev := fsnotify.Event{ Name: "/x/dev/flows/0042-deploy-vps.md", Op: fsnotify.Rename, } board, id, action := classifyEvent(ev) if board != "flows" || id != "0042" || action != "updated" { t.Fatalf("got (%q,%q,%q), want (flows,0042,updated)", board, id, action) } } func TestWatcher_PathToEvent_Remove(t *testing.T) { ev := fsnotify.Event{ Name: "/x/dev/issues/0050-foo.md", Op: fsnotify.Remove, } board, id, action := classifyEvent(ev) if board != "issues" || id != "0050" || action != "deleted" { t.Fatalf("got (%q,%q,%q), want (issues,0050,deleted)", board, id, action) } if sseEventForAction(action) != "card_removed" { t.Fatalf("sseEventForAction(deleted) != card_removed") } } func TestWatcher_PathToEvent_SkippedNames(t *testing.T) { cases := []string{ "/x/dev/issues/README.md", "/x/dev/issues/INDEX.md", "/x/dev/issues/AGENT_GUIDE.md", "/x/dev/issues/notes.txt", // not .md "/x/somewhere-else/0001-foo.md", // not under dev/issues|flows } for _, p := range cases { ev := fsnotify.Event{Name: p, Op: fsnotify.Create} if board, _, _ := classifyEvent(ev); board != "" { t.Fatalf("expected ignored, got board=%q for %s", board, p) } } } func TestWatcher_DetectsWrite(t *testing.T) { // Build a temp tree that *looks like* dev/issues/ so classifyEvent // will accept it (it matches by path substring "/dev/issues/"). root := t.TempDir() issuesDirPath := filepath.Join(root, "dev", "issues") if err := os.MkdirAll(issuesDirPath, 0o755); err != nil { t.Fatalf("mkdir: %v", err) } w, err := fsnotify.NewWatcher() if err != nil { t.Fatalf("new watcher: %v", err) } defer w.Close() if err := w.Add(issuesDirPath); err != nil { t.Fatalf("watch add: %v", err) } hub := NewHub() ch := hub.Subscribe() defer hub.Unsubscribe(ch) // Drive events in a goroutine via handleFsEvent so we exercise the // full pipeline (classify -> broadcast). done := make(chan struct{}) go func() { defer close(done) for { select { case ev, ok := <-w.Events: if !ok { return } handleFsEvent(hub, ev) case <-w.Errors: case <-time.After(2 * time.Second): return } } }() cardPath := filepath.Join(issuesDirPath, "0999-test.md") if err := os.WriteFile(cardPath, []byte("---\nstatus: pendiente\n---\n"), 0o644); err != nil { t.Fatalf("write: %v", err) } select { case ev := <-ch: if ev.Board != "issues" || ev.CardID != "0999" { t.Fatalf("unexpected event: %+v", ev) } if ev.Action != "created" && ev.Action != "updated" { t.Fatalf("expected created/updated, got action=%q", ev.Action) } case <-time.After(3 * time.Second): t.Fatal("timeout waiting for write event") } <-done }