package ui import ( "sync" "testing" "time" "github.com/kamelion/ratatoskr-go/internal/events" "github.com/kamelion/ratatoskr-go/internal/model" ) // recordingView фиксирует события, дошедшие до View. type recordingView struct { mu sync.Mutex created int updated int deleted int status []events.TaskStatusChanged history []events.HistoryAppended traces int act []events.AgentActivity logs []events.LogLine } func (v *recordingView) OnTaskCreated(events.TaskCreated) { v.created++ } func (v *recordingView) OnTaskUpdated(events.TaskUpdated) { v.updated++ } func (v *recordingView) OnTaskDeleted(events.TaskDeleted) { v.deleted++ } func (v *recordingView) OnHistoryAppended(e events.HistoryAppended) { v.mu.Lock() v.history = append(v.history, e) v.mu.Unlock() } func (v *recordingView) OnTraceAppended(events.TraceAppended) { v.mu.Lock() v.traces++ v.mu.Unlock() } func (v *recordingView) OnTaskStatusChanged(e events.TaskStatusChanged) { v.mu.Lock() v.status = append(v.status, e) v.mu.Unlock() } func (v *recordingView) OnAgentActivity(e events.AgentActivity) { v.mu.Lock() v.act = append(v.act, e) v.mu.Unlock() } func (v *recordingView) OnLog(e events.LogLine) { v.mu.Lock() v.logs = append(v.logs, e) v.mu.Unlock() } // waitFor ждёт, пока условие не станет истинным (до 2 секунд). func waitFor(cond func() bool) bool { deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if cond() { return true } time.Sleep(10 * time.Millisecond) } return cond() } func TestControllerDeliversDomainEvents(t *testing.T) { dbus := events.New(64) lbus := events.NewLogBus(64) view := &recordingView{} c := New(dbus, lbus, view) c.Start() defer c.Close() dbus.Publish(events.TaskStatusChanged{ID: 1, From: model.StatusReady, To: model.StatusRunning}) dbus.Publish(events.HistoryAppended{TaskID: 1, Role: "user", Content: "hello"}) dbus.Publish(events.AgentActivity{TaskID: 1, Agent: "dev", Stage: "run"}) if !waitFor(func() bool { return len(view.status) == 1 }) { t.Fatal("TaskStatusChanged not delivered") } if !waitFor(func() bool { return len(view.history) == 1 }) { t.Fatal("HistoryAppended not delivered") } if !waitFor(func() bool { return len(view.act) == 1 }) { t.Fatal("AgentActivity not delivered") } sc := view.status[0] if sc.ID != 1 || sc.From != model.StatusReady || sc.To != model.StatusRunning { t.Fatalf("bad status event: %+v", sc) } } func TestControllerDeliversLogEvents(t *testing.T) { dbus := events.New(64) lbus := events.NewLogBus(64) view := &recordingView{} c := New(dbus, lbus, view) c.Start() defer c.Close() lbus.Bus.Publish(events.LogLine{Level: "log", Text: "worker started"}) if !waitFor(func() bool { return len(view.logs) == 1 }) { t.Fatal("LogLine not delivered") } if got := view.logs[0]; got.Text != "worker started" || got.Level != "log" { t.Fatalf("bad log event: %+v", got) } } func TestControllerCloseStopsDelivery(t *testing.T) { dbus := events.New(64) lbus := events.NewLogBus(64) view := &recordingView{} c := New(dbus, lbus, view) c.Start() dbus.Publish(events.TaskCreated{ID: 1}) if !waitFor(func() bool { return view.created == 1 }) { t.Fatal("initial delivery failed") } c.Close() dbus.Publish(events.TaskCreated{ID: 2}) lbus.Bus.Publish(events.LogLine{Level: "log", Text: "after close"}) time.Sleep(150 * time.Millisecond) if view.created > 1 { t.Fatalf("TaskCreated delivered %d times after Close", view.created) } if len(view.logs) > 0 { t.Fatalf("LogLine delivered %d times after Close", len(view.logs)) } } func TestControllerIdempotentClose(t *testing.T) { dbus := events.New(16) lbus := events.NewLogBus(16) c := New(dbus, lbus, &recordingView{}) c.Start() c.Close() c.Close() // не должно падать }