diff --git a/internal/loki/chat_conversation.go b/internal/loki/chat_conversation.go index 945354a..5468cbf 100644 --- a/internal/loki/chat_conversation.go +++ b/internal/loki/chat_conversation.go @@ -8,6 +8,7 @@ import ( "sort" "strings" "sync" + "sync/atomic" "time" ) @@ -88,6 +89,18 @@ const ( loadConvRetryWait = 250 * time.Millisecond ) +// loadConvRetries compte les ATTENTES de reprise du dernier LoadConversation +// (0 = le premier essai a suffi). Il n'existe que pour être observable. +// +// Le test de non-régression veut vérifier qu'une absence légitime — première +// installation, rien d'enregistré — ne déclenche AUCUNE reprise. Il le mesurait +// au chronomètre (« moins de 250 ms »), ce qui confond « aucune reprise » avec +// « une seule tentative, mais lente » : sur un runner CI chargé, créer et +// ouvrir la base bbolt a pris 435 ms et le test a échoué alors qu'aucune reprise +// n'avait eu lieu. On compte donc les reprises au lieu de les déduire d'un +// temps de mur, qui ne dépend pas que de nous. +var loadConvRetries atomic.Int32 + // LoadConversation recharge l'état persisté au démarrage du process. Sans état // enregistré (première fois) on part d'une conversation vide. // @@ -101,11 +114,16 @@ const ( func LoadConversation() { var b []byte var err error + loadConvRetries.Store(0) + retry := func() { + loadConvRetries.Add(1) + time.Sleep(loadConvRetryWait) + } for attempt := 0; attempt < loadConvAttempts; attempt++ { // Sonde : lire la clé de la discussion active en gardant l'erreur. Tant // qu'elle échoue, convEnsureActive ne doit surtout pas être appelée. if _, err = getBytesErr(bkChat, ckActive); err != nil { - time.Sleep(loadConvRetryWait) + retry() continue } // convEnsureActive reprend au passage le fil unique des versions @@ -113,7 +131,7 @@ func LoadConversation() { if b, err = getBytesErr(bkChat, convKey(convEnsureActive())); err == nil { break } - time.Sleep(loadConvRetryWait) + retry() } if err != nil { // Toujours en échec : on le DIT au lieu de repartir à vide en silence, et diff --git a/internal/loki/chat_conversation_load_test.go b/internal/loki/chat_conversation_load_test.go index b606bc4..70b5788 100644 --- a/internal/loki/chat_conversation_load_test.go +++ b/internal/loki/chat_conversation_load_test.go @@ -108,6 +108,12 @@ func TestLoadConversationBaseIllisibleNeForgePasDeDiscussion(t *testing.T) { if max := time.Duration(loadConvAttempts)*loadConvRetryWait + 2*time.Second; elapsed > max { t.Fatalf("démarrage retardé de %v par une base illisible (budget %v)", elapsed, max) } + // Contrepartie de l'assertion « aucune reprise » du test d'absence légitime : + // un compteur bloqué à zéro la rendrait vraie sans rien prouver. Ici les + // reprises ont bel et bien lieu, et on le vérifie. + if n := loadConvRetries.Load(); int(n) != loadConvAttempts { + t.Fatalf("%d reprise(s) comptée(s) sur une base illisible, attendu %d", n, loadConvAttempts) + } after, err := os.ReadFile(path) if err != nil { t.Fatal(err) @@ -130,12 +136,15 @@ func TestLoadConversationAbsenceLegitimeResteRapide(t *testing.T) { t.Setenv("LOKI_HOME", t.TempDir()) resetConvForTest() - start := time.Now() LoadConversation() - elapsed := time.Since(start) - if elapsed >= loadConvRetryWait { - t.Fatalf("absence légitime traitée comme une erreur : %v d'attente, aucune reprise ne devait être déclenchée", elapsed) + // Mesuré sur le COMPTEUR de reprises, pas au chronomètre : une absence + // légitime doit sortir au premier essai. Le temps de mur ne disait pas ça — + // il mêlait le coût de l'essai lui-même (création de la base bbolt, fsync) + // à l'attente de reprise, et un runner CI chargé l'a fait échouer à 435 ms + // alors qu'aucune reprise n'avait été déclenchée. + if n := loadConvRetries.Load(); n != 0 { + t.Fatalf("absence légitime traitée comme une erreur : %d reprise(s) déclenchée(s), aucune ne devait l'être", n) } conv.mu.Lock() n := len(conv.Messages)