package main import ( "context" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" ) // newTestConv crée une conversation isolée (le global `conv` sert au process réel). func newTestConv() *Conversation { c := &Conversation{} c.cond = sync.NewCond(&c.mu) return c } // Subscribe doit rejouer les événements déjà journalisés PUIS suivre le direct, // et se terminer quand le contexte (la connexion) est annulé. func TestSubscribeReplayAndLive(t *testing.T) { c := newTestConv() c.appendDelta(map[string]any{"user": "salut"}) c.appendDelta(map[string]any{"content": "bon"}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() got := make(chan int, 32) go c.Subscribe(ctx, 0, func(m map[string]any) bool { if s, ok := m["seq"].(int); ok { got <- s } return true }) // Les 2 événements déjà présents (replay). waitSeq(t, got, 1) waitSeq(t, got, 2) // Un événement en direct après abonnement. c.appendDelta(map[string]any{"content": "jour"}) waitSeq(t, got, 3) } // Un abonné qui démarre à from=N ne reçoit que ce qui est plus récent que N. func TestSubscribeFromOffset(t *testing.T) { c := newTestConv() c.appendDelta(map[string]any{"user": "a"}) c.appendDelta(map[string]any{"user": "b"}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() got := make(chan int, 8) go c.Subscribe(ctx, 1, func(m map[string]any) bool { if s, ok := m["seq"].(int); ok { got <- s } return true }) // from=1 → on saute le seq 1, on reçoit 2 en premier. waitSeq(t, got, 2) } // Reset bump l'epoch et pousse un {reset:true} aux abonnés, qui repartent de 0. func TestResetNotifiesSubscribers(t *testing.T) { c := newTestConv() c.appendDelta(map[string]any{"user": "x"}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() resetSeen := make(chan bool, 4) go c.Subscribe(ctx, 0, func(m map[string]any) bool { if _, ok := m["reset"]; ok { resetSeen <- true } return true }) // laisse l'abonné consommer le replay initial time.Sleep(20 * time.Millisecond) c.Reset() select { case <-resetSeen: case <-time.After(time.Second): t.Fatal("l'abonné n'a pas reçu l'événement reset") } if c.Seq != 0 || len(c.Log) != 0 { t.Fatalf("après Reset: Seq=%d len(Log)=%d, attendu 0/0", c.Seq, len(c.Log)) } } // Les handlers de contrôle répondent en JSON sans dépendre du modèle. func TestChatControlHandlers(t *testing.T) { // reset → conversation vide rr := httptest.NewRecorder() handleChatReset(rr, httptest.NewRequest("POST", "/api/chat/reset", nil)) if rr.Code != 200 { t.Fatalf("reset code %d", rr.Code) } // state → seq 0, pas de génération rr = httptest.NewRecorder() handleChatState(rr, httptest.NewRequest("GET", "/api/chat/state", nil)) if rr.Code != 200 || !strings.Contains(rr.Body.String(), "\"seq\":0") { t.Fatalf("state inattendu: %d %s", rr.Code, rr.Body.String()) } // send message vide → 400 rr = httptest.NewRecorder() req := httptest.NewRequest("POST", "/api/chat/send", strings.NewReader(`{"message":""}`)) req.Header.Set("Content-Type", "application/json") handleChatSend(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("send vide devrait être 400, obtenu %d", rr.Code) } } func waitSeq(t *testing.T, ch <-chan int, want int) { t.Helper() select { case s := <-ch: if s != want { t.Fatalf("seq reçu %d, attendu %d", s, want) } case <-time.After(time.Second): t.Fatalf("timeout en attendant seq %d", want) } }