mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 13:08:15 +02:00
- LSP (lsp.go) : un signal resté dans le tampon par une republication tardive faisait rendre « aucun diagnostic » à l'édition suivante, avant même que le serveur ait vu la nouvelle version. Le canal est vidé, une publication d'une version antérieure est ignorée, et check attend 350 ms de calme après la dernière publication (liste vide puis analyse : c'est l'analyse qui compte), toujours borné par lspDiagWait. Au passage : interblocage latent levé (writeMsg → markDead reprenait mu, déjà tenu par check) — dead devient atomique. - run_shell : ErrWaitDelay (commande finie, process laissé en arrière-plan sur les tubes) rend la sortie au lieu de « WaitDelay expired » qui l'effaçait (AJEAN 7403a8a0). - Clé de pilotage web : lue depuis un cache à chaque requête au lieu d'ouvrir la base sous dbMu (AJEAN #120). Lectures réussies seulement, invalidé par toute écriture du bucket d'état, par la date/taille du fichier (autre process) et au bout d'une seconde. - MCP : l'appel suit le contexte du tour (le bouton stop l'annule), délai par serveur (callTimeoutSec) et allongé par l'argument timeout de l'outil, plafonné à 30 min ; un délai dépassé n'est plus relancé (OpenFox 2ef584b8). - Recall : les blocs d'une compaction s'archivent en une seule transaction au lieu d'une par bloc (AJEAN #130). - Compactage en cours de tour : après un essai qui n'a rien réduit, pas de nouvel essai avant +5 % de la fenêtre (sauf à 90 %), pour les tâches et sous-agents aussi ; la cause d'un résumé raté est journalisée (AJEAN 68659e73). - Motifs de relance : un motif vide (qui correspondait à tout) est écarté, une surcharge sans motif utilisable retombe sur l'embarqué, et un changement de retry_patterns agit sans redémarrer (OpenFox 8e9b4869, c52c5bf7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
3.8 KiB
Go
117 lines
3.8 KiB
Go
package loki
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Le bouton stop doit VRAIMENT arrêter une commande shell. Elle naissait d'un
|
|
// contexte indépendant du tour : arrêter la génération ne l'arrêtait pas, et le
|
|
// tour restait suspendu jusqu'à l'expiration du délai (5 min au maximum).
|
|
func TestRunShellObeitAuStop(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("commande de veille propre à Unix")
|
|
}
|
|
testHome(t)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
go func() {
|
|
time.Sleep(200 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
start := time.Now()
|
|
out := runShell(ctx, "sleep 30", 300)
|
|
if d := time.Since(start); d > 5*time.Second {
|
|
t.Fatalf("la commande a survécu à l'annulation (%s) : %q", d, out)
|
|
}
|
|
if !strings.Contains(out, "interrompue") {
|
|
t.Errorf("résultat attendu « interrompue », reçu %q", out)
|
|
}
|
|
}
|
|
|
|
// Une commande qui laisse un process en arrière-plan garde les tubes de sortie
|
|
// ouverts. Sans WaitDelay, Wait attend leur fermeture — donc pour toujours — et
|
|
// le tour ne se terminait JAMAIS (seul un redémarrage du service débloquait).
|
|
func TestRunShellNeBloquePasSurUnProcessDetache(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("syntaxe de détachement propre à Unix")
|
|
}
|
|
testHome(t)
|
|
start := time.Now()
|
|
// Délai de 10 s, bien au-delà de WaitDelay (2 s) : c'est le détachement
|
|
// qui rend la main, pas le délai de la commande.
|
|
out := runShell(context.Background(), "sleep 30 & echo lance", 10)
|
|
if d := time.Since(start); d > 8*time.Second {
|
|
t.Fatalf("runShell est resté accroché aux tubes du process détaché (%s)", d)
|
|
}
|
|
// La commande a fini (exit 0) : sa sortie est rendue, pas effacée par
|
|
// « WaitDelay expired » (AJEAN 7403a8a0).
|
|
if !strings.Contains(out, "exit: 0") || !strings.Contains(out, "lance") || !strings.Contains(out, "arrière-plan") {
|
|
t.Errorf("sortie inattendue : %q", out)
|
|
}
|
|
}
|
|
|
|
// Le dossier de travail est résolu une fois par process : s'il disparaît, plus
|
|
// AUCUNE commande ne passait (« chdir : no such file or directory ») jusqu'au
|
|
// redémarrage du service. Il doit être recréé au besoin.
|
|
func TestRunShellRecreeLeWorkspaceDisparu(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("echo se comporte différemment sous cmd.exe")
|
|
}
|
|
testHome(t)
|
|
ws := agentWorkspace()
|
|
if ws == "" {
|
|
t.Skip("pas de workspace résolu")
|
|
}
|
|
if err := os.RemoveAll(ws); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out := runShell(context.Background(), "echo vivant", 10)
|
|
if !strings.Contains(out, "vivant") {
|
|
t.Fatalf("commande cassée par la disparition du workspace : %q", out)
|
|
}
|
|
}
|
|
|
|
// Reset doit TOUJOURS rendre la main, même sur un tour resté coincé : c'est le
|
|
// geste que l'on tente quand le chat est bloqué. Avant, Generating restait vrai
|
|
// et tout envoi suivant était refusé « génération en cours » jusqu'au
|
|
// redémarrage du service.
|
|
func TestResetDebloqueUnTourCoince(t *testing.T) {
|
|
c := newTestConv()
|
|
// Simule un tour parti et jamais terminé (commande accrochée, moteur disparu).
|
|
c.mu.Lock()
|
|
c.Generating = true
|
|
c.mu.Unlock()
|
|
|
|
c.Reset()
|
|
|
|
if st := c.state(); st["generating"] != false {
|
|
t.Fatal("après Reset, la conversation doit être déclarée libre")
|
|
}
|
|
}
|
|
|
|
// Le tour abandonné se termine parfois APRÈS le démarrage du suivant. Sa fin ne
|
|
// doit pas déclarer « libre » une génération toute neuve.
|
|
func TestTourPerimeNeLiberePasLeTourCourant(t *testing.T) {
|
|
c := newTestConv()
|
|
oldEpoch := c.epoch
|
|
c.Reset() // le tour d'epoch `oldEpoch` devient périmé
|
|
c.mu.Lock()
|
|
c.Generating = true // un nouveau tour démarre
|
|
c.mu.Unlock()
|
|
|
|
// Fin (tardive) du tour périmé : même code que le defer de generate.
|
|
c.mu.Lock()
|
|
if c.epoch == oldEpoch {
|
|
c.Generating = false
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if st := c.state(); st["generating"] != true {
|
|
t.Fatal("un tour périmé a libéré le tour courant")
|
|
}
|
|
}
|