mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
conversation persistee cote serveur + generation autonome (multi-appareils)
L'historique et la generation vivent desormais sur le serveur jean, plus dans le localStorage du navigateur : - conversation.go : store unique (Messages vue modele + Log d'evenements rejouable), persistance disque JeanHome/conversation.json, generation dans une goroutine detachee (context.Background) -> fermer le navigateur ne coupe plus la reponse - runChatStream devient un pur abonne (rejoue Log[from:] puis suit le direct) ; nouveaux endpoints /api/chat/send|stop|reset|state - UI : flux d'abonnement permanent + replay du journal (outils/vitesses/raisonnement reviennent apres refresh), synchro live inter-appareils, reconnexion auto - compactage manuel neutralise (auto cote serveur), vitesses rendues depuis les stats persistees
This commit is contained in:
1 parent
2daea32ade
commit
4514a96585
4 files changed
+605
-265
No files matched your search
@@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user