mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 03:39:29 +02:00
Bug 1 (chat bloque, remonte par Nathan) : trois causes cumulees. - runShell naissait d'un contexte independant du tour : stop n'arretait rien. La commande herite desormais du contexte du tour, et la boucle d'outils s'interrompt des que l'arret est demande. - cmd.WaitDelay : une commande laissant un process detache gardait les tubes ouverts, donc Wait ne revenait JAMAIS (ni delai, ni stop). - Reset debloque toujours l'etat de generation : « nouvelle conversation » etait le geste tente quand le chat coince, il ne servait a rien. Un tour perime ne libere plus le tour courant. Trouve en testant : le workspace disparu cassait TOUTES les commandes suivantes (chdir: no such file or directory) jusqu'au redemarrage ; il est recree au besoin. Bug 2 (raisonnement, remonte par Nathan) : l'UI ecrivait « off » en EFFACANT la ligne REASONING, or sans drapeau le moteur suit le gabarit du modele, qui raisonne. L'UI ecrit maintenant on|off et « off » part en --reasoning off, apres avoir demande au binaire s'il connait le drapeau (ik_llama.cpp ne le connait pas et refuserait de demarrer). Presets existants inchanges ; l'editeur dit desormais « non precise : le modele decide » quand la cle est absente.
113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package ajean
|
|
|
|
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()
|
|
out := runShell(context.Background(), "sleep 30 & echo lance", 2)
|
|
if d := time.Since(start); d > 10*time.Second {
|
|
t.Fatalf("runShell est resté accroché aux tubes du process détaché (%s)", d)
|
|
}
|
|
if !strings.Contains(out, "lance") && !strings.Contains(out, "timeout") {
|
|
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")
|
|
}
|
|
}
|