mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Bug trouve en testant la migration sur une vraie machine (5,4 Go, 51 351 fichiers). Le rename du dossier reussissait, mais config.env contenait BIN=C:\ProgramData\jean\backends\llama.cpp\build\bin\Release\llama-server.exe soit un chemin ABSOLU vers le dossier qu'on venait de renommer. Resultat : le dossier migre intact, et llama-server ne demarre plus. Ca touche tous ceux qui ont fait un `llamacpp install`, donc le cas nominal, pas un cas limite. Les presets de configs/ portent le meme BIN et etaient touches pareil (deux presets concernes sur la machine de test). Apres un rename reussi, on reecrit donc les references a l'ancien chemin dans les fichiers de config qu'on ecrit nous-memes : config.env, model_dirs.json, mcp.json, webprefs.json et les presets de configs/. Les deux ecritures de separateur sont couvertes, Windows acceptant \ et / dans une meme valeur. Restreint a ces fichiers texte : on ne reecrit pas a l'aveugle les 51 000 fichiers d'un dossier de donnees. Les fichiers d'etat du service sont repris au passage (jean.pid/jean.log -> ajean.pid/ajean.log), meme raison que pour le worker de lien : le PID dit si le service tourne, l'ignorer ferait demarrer un second service. Non traite volontairement : l'arborescence de build llama.cpp (CMakeCache.txt et consorts) contient aussi des chemins absolus. Elle est regenerable par `ajean llamacpp update` et la reecrire en masse serait plus risque qu'utile. Verifie de bout en bout sur la machine de test : service demarre, llama-server chargé depuis le nouveau chemin, /health ok, completion reelle ok. Deux tests ajoutes reproduisant le cas.
175 lines
6.3 KiB
Go
175 lines
6.3 KiB
Go
package ajean
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mkHome crée un dossier de données factice contenant un fichier témoin, pour
|
|
// vérifier que la migration déplace bien le CONTENU et pas seulement le dossier.
|
|
func mkHome(t *testing.T, dir string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "config.env"), []byte("MODEL=x.gguf\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Cas nominal : seul l'ancien dossier existe → il est migré, contenu compris.
|
|
func TestMigrateHomeMovesLegacy(t *testing.T) {
|
|
root := t.TempDir()
|
|
legacy := filepath.Join(root, "jean")
|
|
target := filepath.Join(root, "ajean")
|
|
mkHome(t, legacy)
|
|
|
|
if got := migrateHome(target, legacy); got != target {
|
|
t.Fatalf("got %q, attendu le nouveau chemin %q", got, target)
|
|
}
|
|
if b, err := os.ReadFile(filepath.Join(target, "config.env")); err != nil || string(b) != "MODEL=x.gguf\n" {
|
|
t.Fatalf("contenu non migré: %q (%v)", b, err)
|
|
}
|
|
if isDir(legacy) {
|
|
t.Fatalf("l'ancien dossier existe encore après un rename réussi")
|
|
}
|
|
}
|
|
|
|
// Installation neuve : rien à migrer, on renvoie le nouveau chemin sans rien créer.
|
|
func TestMigrateHomeFreshInstall(t *testing.T) {
|
|
root := t.TempDir()
|
|
target := filepath.Join(root, "ajean")
|
|
if got := migrateHome(target, filepath.Join(root, "jean")); got != target {
|
|
t.Fatalf("got %q, attendu %q", got, target)
|
|
}
|
|
}
|
|
|
|
// Déjà migré : le nouveau dossier existe → on ne retouche à rien, même si un
|
|
// vieux dossier « jean » traîne encore (utilisateur qui l'a recréé, restauration
|
|
// de sauvegarde…). Écraser serait perdre les données courantes.
|
|
func TestMigrateHomeKeepsExistingTarget(t *testing.T) {
|
|
root := t.TempDir()
|
|
legacy := filepath.Join(root, "jean")
|
|
target := filepath.Join(root, "ajean")
|
|
mkHome(t, legacy)
|
|
mkHome(t, target)
|
|
if err := os.WriteFile(filepath.Join(target, "config.env"), []byte("MODEL=courant.gguf\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got := migrateHome(target, legacy); got != target {
|
|
t.Fatalf("got %q, attendu %q", got, target)
|
|
}
|
|
b, _ := os.ReadFile(filepath.Join(target, "config.env"))
|
|
if string(b) != "MODEL=courant.gguf\n" {
|
|
t.Fatalf("le dossier courant a été écrasé par l'ancien: %q", b)
|
|
}
|
|
if !isDir(legacy) {
|
|
t.Fatalf("l'ancien dossier a été supprimé — la migration ne doit JAMAIS supprimer")
|
|
}
|
|
}
|
|
|
|
// Rename impossible → on doit retomber sur l'ancien chemin, intact. C'est la
|
|
// garantie « au pire, rien n'a bougé », celle qui protège les .gguf.
|
|
//
|
|
// On reproduit le scénario réel : un service AJEAN tourne encore et tient un
|
|
// handle ouvert dans le dossier de données, ce qui fait échouer le rename du
|
|
// dossier sous Windows. Unix, lui, renomme sans broncher un dossier dont des
|
|
// fichiers sont ouverts — il n'y a donc rien à simuler là-bas.
|
|
func TestMigrateHomeFallsBackWhenRenameFails(t *testing.T) {
|
|
if runtime.GOOS != "windows" {
|
|
t.Skip("un rename de dossier réussit avec des fichiers ouverts hors Windows")
|
|
}
|
|
root := t.TempDir()
|
|
legacy := filepath.Join(root, "jean")
|
|
target := filepath.Join(root, "ajean")
|
|
mkHome(t, legacy)
|
|
|
|
held, err := os.Open(filepath.Join(legacy, "config.env"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer held.Close()
|
|
|
|
if got := migrateHome(target, legacy); got != legacy {
|
|
t.Fatalf("got %q, attendu un repli sur l'ancien chemin %q", got, legacy)
|
|
}
|
|
if b, err := os.ReadFile(filepath.Join(legacy, "config.env")); err != nil || string(b) != "MODEL=x.gguf\n" {
|
|
t.Fatalf("l'ancien dossier a été abîmé par une migration ratée: %q (%v)", b, err)
|
|
}
|
|
}
|
|
|
|
// Le cas qui a casse une vraie machine : config.env contient un chemin ABSOLU
|
|
// vers le dossier de donnees (BIN=<home>\backends\...\llama-server.exe). Apres
|
|
// migration ce chemin doit suivre, sinon llama-server ne demarre plus — et ca
|
|
// concerne tous ceux qui ont fait un `llamacpp install`, donc le cas nominal.
|
|
func TestMigrateHomeRewritesAbsolutePaths(t *testing.T) {
|
|
root := t.TempDir()
|
|
legacy := filepath.Join(root, "jean")
|
|
target := filepath.Join(root, "ajean")
|
|
mkHome(t, legacy)
|
|
if err := os.MkdirAll(filepath.Join(legacy, "configs"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bin := filepath.Join(legacy, "backends", "llama.cpp", "build", "bin", "llama-server")
|
|
write := func(rel, body string) {
|
|
if err := os.WriteFile(filepath.Join(legacy, rel), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("config.env", "BIN="+bin+"\nMODEL=\"x.gguf\"\n")
|
|
// Les presets portent le meme BIN et doivent suivre aussi.
|
|
write(filepath.Join("configs", "gros.env"), "BIN="+bin+"\n")
|
|
// Ecriture avec des slashs : Windows accepte les deux formes.
|
|
write("model_dirs.json", `{"dirs":["`+filepath.ToSlash(legacy)+`/models"]}`)
|
|
|
|
if got := migrateHome(target, legacy); got != target {
|
|
t.Fatalf("migration echouee: %q", got)
|
|
}
|
|
|
|
for _, rel := range []string{"config.env", filepath.Join("configs", "gros.env"), "model_dirs.json"} {
|
|
b, err := os.ReadFile(filepath.Join(target, rel))
|
|
if err != nil {
|
|
t.Fatalf("%s illisible: %v", rel, err)
|
|
}
|
|
if strings.Contains(string(b), legacy) || strings.Contains(string(b), filepath.ToSlash(legacy)) {
|
|
t.Errorf("%s reference encore l'ancien dossier:\n%s", rel, b)
|
|
}
|
|
if !strings.Contains(string(b), target) && !strings.Contains(string(b), filepath.ToSlash(target)) {
|
|
t.Errorf("%s ne pointe pas vers le nouveau dossier:\n%s", rel, b)
|
|
}
|
|
}
|
|
|
|
// Le BIN reecrit doit designer un chemin reellement atteignable.
|
|
newBin := filepath.Join(target, "backends", "llama.cpp", "build", "bin", "llama-server")
|
|
if b, _ := os.ReadFile(filepath.Join(target, "config.env")); !strings.Contains(string(b), newBin) {
|
|
t.Errorf("BIN ne pointe pas sur %s:\n%s", newBin, b)
|
|
}
|
|
}
|
|
|
|
// Les fichiers d'etat du service (jean.pid/jean.log) doivent etre repris : le
|
|
// PID dit si le service tourne, l'ignorer ferait demarrer un second service.
|
|
func TestMigrateHomeAdoptsServiceStateFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
legacy := filepath.Join(root, "jean")
|
|
target := filepath.Join(root, "ajean")
|
|
mkHome(t, legacy)
|
|
if err := os.WriteFile(filepath.Join(legacy, "jean.pid"), []byte("4242\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrateHome(target, legacy)
|
|
|
|
b, err := os.ReadFile(filepath.Join(target, "ajean.pid"))
|
|
if err != nil {
|
|
t.Fatalf("ajean.pid absent apres migration: %v", err)
|
|
}
|
|
if strings.TrimSpace(string(b)) != "4242" {
|
|
t.Fatalf("PID perdu: %q", b)
|
|
}
|
|
}
|