CLI elaguee et tests remis sur la base

- suppression des alias de commandes herites (skills, machine, tools, mem, upgrade...)
- aide reecrite et regroupee
- helper de test testHome + fermeture de la base (verrou bbolt sous Windows)
- tests des dossiers de modeles alignes sur models/
This commit is contained in:
nathaninline committed 2026-08-07 17:03:27 +02:00
1 parent 6de356f649
commit 62ad4155a2
18 files changed
+202 -153

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ func TestCompiledFile(t *testing.T) {
`[123/456] Building CUDA object ggml/src/ggml-cuda/CMakeFiles/ggml-cuda.dir/acc.cu.o`: "acc.cu",
// La ligne de commande nvcc géante ne doit PAS être prise pour un fichier.
` C:\...\nvcc.exe -x cu ... -o ggml-cuda.dir\Release\acc.obj "C:\...\acc.cu"`: "",
`Building Custom Rule C:/ProgramData/ajean/...`: "",
`Building Custom Rule C:/ProgramData/ajean/...`: "",
`-- UI: running npm install`: "",
}
for in, want := range cases {
+2 -2
View File
@@ -39,9 +39,9 @@ func cmdLlamacpp(args []string) error {
switch sub {
case "install":
return llamacppInstall(args)
case "update", "upgrade":
case "update":
return llamacppUpdate(args)
case "status", "info", "":
case "status", "":
return llamacppStatus(args)
case "prebuilt":
// Binaires officiels précompilés (aucune compilation) — voir backend_prebuilt.go.
+10 -9
View File
@@ -6,12 +6,11 @@ import (
"testing"
)
// Un modèle posé hors de AJEAN_HOME (disque externe) doit être utilisable dès
// Un modèle posé hors de models/ (disque externe) doit être utilisable dès
// que son dossier est déclaré — et refusé tant qu'il ne l'est pas.
func TestResolveModelPathExtraDir(t *testing.T) {
home := t.TempDir()
testHome(t)
ext := t.TempDir()
t.Setenv("AJEAN_HOME", home)
t.Setenv("AJEAN_MODEL_DIRS", "")
extModel := filepath.Join(ext, "gros.gguf")
@@ -39,20 +38,22 @@ func TestResolveModelPathExtraDir(t *testing.T) {
}
}
// AJEAN_HOME garde la priorité et les non-.gguf restent refusés.
// models/ garde la priorité et les non-.gguf restent refusés.
func TestResolveModelPathHomeFirst(t *testing.T) {
home := t.TempDir()
testHome(t)
ext := t.TempDir()
t.Setenv("AJEAN_HOME", home)
t.Setenv("AJEAN_MODEL_DIRS", ext)
for _, d := range []string{home, ext} {
if err := os.MkdirAll(modelsDir(), 0o755); err != nil {
t.Fatal(err)
}
for _, d := range []string{modelsDir(), ext} {
if err := os.WriteFile(filepath.Join(d, "m.gguf"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
}
got, err := resolveModelPath("m.gguf")
if err != nil || got != filepath.Join(home, "m.gguf") {
t.Fatalf("got %q (%v), attendu le fichier de AJEAN_HOME", got, err)
if err != nil || got != filepath.Join(modelsDir(), "m.gguf") {
t.Fatalf("got %q (%v), attendu le fichier de models/", got, err)
}
if _, err := resolveModelPath("/etc/passwd"); err == nil {
t.Fatal("un fichier non-.gguf devrait être refusé")
+5 -6
View File
@@ -12,17 +12,16 @@ import (
)
func TestDownloadDestPathDir(t *testing.T) {
home := t.TempDir()
testHome(t)
extra := t.TempDir()
t.Setenv("AJEAN_HOME", home)
t.Setenv("AJEAN_MODEL_DIRS", extra)
// Dossier vide → AJEAN_HOME.
// Dossier vide → models/.
got, err := downloadDestPath("m.gguf", "")
if err != nil {
t.Fatal(err)
}
if want := filepath.Join(home, "m.gguf"); got != want {
if want := filepath.Join(modelsDir(), "m.gguf"); got != want {
t.Fatalf("défaut = %s, attendu %s", got, want)
}
@@ -76,8 +75,8 @@ func TestDiskFreeAndCheck(t *testing.T) {
// Les .part orphelins doivent aussi être nettoyés dans les dossiers ajoutés.
func TestCleanStalePartFilesExtraDirs(t *testing.T) {
home, extra := t.TempDir(), t.TempDir()
t.Setenv("AJEAN_HOME", home)
testHome(t)
extra := t.TempDir()
t.Setenv("AJEAN_MODEL_DIRS", extra)
stale := filepath.Join(extra, "coupe.gguf.part")
if err := os.WriteFile(stale, []byte("x"), 0o644); err != nil {
+5 -2
View File
@@ -75,8 +75,11 @@ func TestRunDownloadCancelLeavesNothing(t *testing.T) {
// cleanStalePartFiles doit balayer les .part orphelins sans toucher aux .gguf.
func TestCleanStalePartFiles(t *testing.T) {
dir := t.TempDir()
t.Setenv("AJEAN_HOME", dir)
testHome(t)
dir := modelsDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
keep := filepath.Join(dir, "bon.gguf")
stale := filepath.Join(dir, "coupe.gguf.part")
for _, p := range []string{keep, stale} {
+1 -2
View File
@@ -67,8 +67,7 @@ func TestExtractArchiveSymlink(t *testing.T) {
// comme sur une machine mise à jour plusieurs fois.
func fakePrebuilt(t *testing.T, version string, tags ...string) string {
t.Helper()
home := t.TempDir()
t.Setenv("AJEAN_HOME", home)
home := testHome(t)
dir := filepath.Join(home, "backends", "llama.cpp-prebuilt")
name := "llama-server"
if runtime.GOOS == "windows" {
+13 -7
View File
@@ -6,6 +6,14 @@ import (
"testing"
)
// setConfig installe une configuration décrite au format des presets.
func setConfig(t *testing.T, body string) {
t.Helper()
if err := WriteConfig(parseEnv(body)); err != nil {
t.Fatal(err)
}
}
// TestPresetFingerprintIgnoresCosmetics vérifie que la détection du preset actif
// ne dépend QUE de la config effective (ensemble KEY=VALUE), pas de la mise en
// forme : ordre des lignes, commentaires (dont `# NAME=`), lignes vides, `export`,
@@ -32,15 +40,14 @@ func TestPresetFingerprintIgnoresCosmetics(t *testing.T) {
// suivante l'écrasait, llama.cpp revoyait les deux cartes et réétalait le modèle
// sur la petite — pendant que `ajean gpu` réaffichait « auto ».
func TestSwitchToPresetGardeLesReglagesMachine(t *testing.T) {
home := t.TempDir()
t.Setenv("AJEAN_HOME", home)
home := testHome(t)
write := func(p, body string) {
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
// config.env courante : un preset + les réglages machine.
write(confPath(), "MODEL=ancien.gguf\nCTX=4096\nCUDA_VISIBLE_DEVICES=1\nWEB_ENGINE=go\nMEM_MODE=always\n")
// Configuration courante : un preset + les réglages machine.
setConfig(t, "MODEL=ancien.gguf\nCTX=4096\nCUDA_VISIBLE_DEVICES=1\nWEB_ENGINE=go\nMEM_MODE=always\n")
target := filepath.Join(home, "cible.env")
write(target, "MODEL=nouveau.gguf\nCTX=8192\n")
@@ -66,14 +73,13 @@ func TestSwitchToPresetGardeLesReglagesMachine(t *testing.T) {
// --tensor-split ait deux cartes. Écraser ça par une sélection mono-GPU faisait
// mourir le chargement sur « cudaMalloc failed: out of memory ».
func TestSwitchToPresetLaisseLePresetImposerSesGPU(t *testing.T) {
home := t.TempDir()
t.Setenv("AJEAN_HOME", home)
home := testHome(t)
write := func(p, body string) {
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write(confPath(), "MODEL=ancien.gguf\nCUDA_VISIBLE_DEVICES=1\nWEB_ENGINE=go\n")
setConfig(t, "MODEL=ancien.gguf\nCUDA_VISIBLE_DEVICES=1\nWEB_ENGINE=go\n")
deuxGPU := filepath.Join(home, "deux-gpu.env")
write(deuxGPU, "MODEL=fable.gguf\nCUDA_VISIBLE_DEVICES=1,0\n")
if err := applyPresetFile(deuxGPU); err != nil {
+1 -1
View File
@@ -26,7 +26,7 @@ func cmdAgent(args []string) error {
return err
}
fmt.Println(green("[ok]") + " mode agent désactivé")
case "", "status", "list":
case "", "status":
state := dim("off")
if agentEnabled() {
state = green("on")
+5 -5
View File
@@ -317,9 +317,9 @@ const (
// actuel). Toute valeur inconnue retombe sur always.
func memMode() MemMode {
switch strings.ToLower(strings.TrimSpace(ReadConfig()["MEM_MODE"])) {
case "off", "0", "false", "none", "no", "non":
case "off":
return MemOff
case "ondemand", "on-demand", "demand", "manual", "manuel":
case "ondemand":
return MemOnDemand
default: // "always", "auto", "" et inconnus
return MemAlways
@@ -343,7 +343,7 @@ func cmdMemory(args []string) error {
MemAlways: "auto (l'IA cherche et sauve d'elle-même)",
}
switch sub {
case "off", "none", "0", "false":
case "off":
if err := setMemMode(MemOff); err != nil {
return err
}
@@ -351,11 +351,11 @@ func cmdMemory(args []string) error {
if err := setMemMode(MemOnDemand); err != nil {
return err
}
case "always", "auto", "on":
case "always":
if err := setMemMode(MemAlways); err != nil {
return err
}
case "", "status", "list":
case "", "status":
m := memMode()
fmt.Printf("%s mode: %s — %s\n", cyan("Mémoire"), bold(string(m)), label[m])
pages := MemList()
+2 -2
View File
@@ -12,7 +12,7 @@ import (
// tentative de connexion : sans ça, connecter en parallèle multiplierait les
// process stdio lancés pour un même serveur.
func TestMCPEnsureDeduplicatesConcurrentConnects(t *testing.T) {
t.Setenv("AJEAN_HOME", t.TempDir())
testHome(t)
t.Cleanup(mcpCloseAll)
// Commande inexistante : la connexion échoue, mais l'entrée du pool est bien
@@ -53,7 +53,7 @@ func TestMCPEndToEnd(t *testing.T) {
if _, err := exec.LookPath("npx"); err != nil {
t.Skip("npx indisponible — test MCP e2e ignoré")
}
t.Setenv("AJEAN_HOME", t.TempDir())
testHome(t)
cfg := MCPServerConfig{
Command: "npx",
+1 -1
View File
@@ -87,7 +87,7 @@ func cmdLink(args []string) error {
// éviter de prendre un mot pour un token et d'écraser le vrai).
printLinkHelp()
return nil
case "serve", "--foreground", "fg":
case "serve":
// Le worker réel (boucle de connexion au relais), pendant de `ajean serve`.
// C'est ce que lance l'unité systemd ; il NE doit PAS rendre la main.
return runLinkForeground()
+29 -45
View File
@@ -66,15 +66,11 @@ func Main() {
mustExit(cmdLink(args))
case "oai":
mustExit(cmdOAI(args))
case "agent", "skills", "machine", "tools":
// « mode agent » unifie l'ancien couple machine + skills : un seul
// interrupteur active TOUS les outils de l'IA (shell + mémoire ; les
// skills ont été fondus dans la mémoire). Les anciens noms restent
// acceptés comme alias rétro-compatibles.
case "agent":
mustExit(cmdAgent(args))
case "internet", "web-access":
case "internet":
mustExit(cmdInternet(args))
case "memory", "mem":
case "memory":
mustExit(cmdMemory(args))
case "serve":
mustExit(cmdServe(args))
@@ -82,15 +78,15 @@ func Main() {
mustExit(cmdTest(args))
case "bench":
mustExit(cmdBench(args))
case "llamacpp", "llama":
case "llamacpp":
mustExit(cmdLlamacpp(args))
case "update", "upgrade", "self-update":
case "update":
mustExit(cmdUpdate(args))
case "where", "paths":
case "where":
mustExit(cmdWhere(args))
case restartArg:
// Sous-commande interne, absente de l'aide : l'accompagnateur detache qui
// attend la fermeture de l'app, migre, puis la relance.
// Sous-commande interne, absente de l'aide : l'accompagnateur détaché qui
// attend la fermeture de l'app puis la relance.
mustExit(cmdRestartAfterUpdate(args))
case "install":
mustExit(cmdInstall(args))
@@ -116,58 +112,46 @@ Application:
app lance l'UI web + ouvre le navigateur (auto au double-clic du binaire)
Service:
start | stop | restart gérer le service (systemd sous Linux, processus en arrière-plan sous Windows)
start | stop | restart gérer le service
status | logs état / logs en direct
enable | disable auto-démarrage au boot
edit éditer $AJEAN_HOME/config.env
set-api-key [clé] protéger llama-server (clé Bearer); vide = générer, "" = retirer
set-web-key [clé] protéger l'API de pilotage 'ajean web'; vide = générer, "" = retirer
edit éditer la configuration dans $EDITOR
switch [N] activer un preset de presets/ (interactif ou par numéro)
test | bench [N] vérifier que l'IA répond / mesurer prefill + decode tok/s
vram utilisation GPU/VRAM (nvidia-smi)
gpu [index…] liste les GPU / choisit le(s)quel(s) utiliser (gpu all = tous)
test vérifie que l'IA répond (health + completion)
bench [N] mesure prefill + decode tok/s (prompt 2000 tok, N=200 par défaut)
Presets:
switch [N] choisir un preset dans configs/ (interactif ou par numéro)
set-api-key [clé] protéger llama-server (clé Bearer); vide = générer, "" = retirer
set-web-key [clé] protéger l'API de pilotage; vide = générer, "" = retirer
Interaction:
chat [system-prompt] chat terminal streamé
web [PORT] UI web (défaut :8090) — chat + presets + mode agent
agent [on|off|status] donne à l'IA ses outils (shell, fichiers, mémoire)
memory [off|ondemand|always|status] mode mémoire de l'IA
internet [on|off|status|engine <go|crawl4ai>|url <url>|key <clé>]
accès web de l'IA (web_search/open/read/grep) — moteur intégré ou serveur Crawl4AI
memory [off|ondemand|always|status] mode mémoire de l'IA (off / sur demande / auto)
accès web de l'IA (moteur intégré ou serveur Crawl4AI)
Accès distant (ajean.link) :
link <token> enregistre le token et démarre le lien au relais (token = 1re fois / pour le changer)
link start | restart | stop démarre / redémarre / arrête le service de lien
link code génère un code d'appairage (valable 10 min, à usage unique) pour le portail
link <token> enregistre le token et démarre le lien au relais
link start | restart | stop pilote le service de lien
link code code d'appairage (10 min, usage unique) pour le portail
link status | logout état du lien / oublier le token
link (sans argument) affiche l'aide des sous-commandes link
link serve exécute le worker au premier plan (utilisé par ajean-link.service ; pendant de 'ajean serve')
Mode agent:
agent [on|off|status] active TOUS les outils de l'IA (shell complet + mémoire) — un seul interrupteur
link serve worker au premier plan (lancé par ajean-link.service)
Backend llama.cpp :
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), pointe BIN dessus
llamacpp update git pull + recompile le backend existant (arrête/redémarre le service)
llamacpp install clone + compile llama.cpp (CUDA/ROCm/Metal/CPU), pointe BIN dessus
llamacpp update git pull + recompile le backend existant
llamacpp status commit courant, backend détecté, retard sur origin
Entrypoint (utilisé par ajean.service) :
serve lit config.env et exec le binaire llama-server
Installation:
where affiche où sont le binaire, la config, la mémoire et le dossier de travail de l'agent
install installer (Linux: unité systemd, sudoers, dossiers ; Windows: dossiers + config)
uninstall désinstaller
update [--check] mettre à jour AJEAN depuis les releases GitHub (--check = signale sans installer)
where affiche l'emplacement du binaire, de la base et des dossiers
install | uninstall installer / désinstaller
update [--check] mettre à jour depuis les releases GitHub
serve entrypoint du service : exec llama-server (usage interne)
Env:
AJEAN_HOME racine (défaut: /etc/ajean sur Linux/macOS, %%ProgramData%%\ajean sur Windows)
AJEAN_HOME reste accepté (héritage)
EDITOR éditeur pour 'ajean edit' (défaut: nano sur Unix, notepad sur Windows)
Config: $AJEAN_HOME/config.env
AJEAN_HOME racine des données (défaut : /etc/ajean, %%ProgramData%%\ajean sous Windows)
EDITOR éditeur pour 'ajean edit' (défaut : nano, notepad sous Windows)
`, Version)
}
+49 -29
View File
@@ -33,49 +33,69 @@ const (
bkChat = "chat" // conversation partagée
)
// Les connexions sont mises en cache PAR CHEMIN, et non dans un singleton.
// $AJEAN_HOME peut désigner un autre dossier d'un appel à l'autre — c'est le cas
// dans les tests, qui donnent à chacun son dossier temporaire. Un singleton
// figerait la toute première base et tous les appels suivants écriraient dans le
// mauvais fichier, sans erreur et sans bruit.
var (
dbOnce sync.Once
dbConn *bolt.DB
dbErr error
dbMu sync.Mutex
dbConns = map[string]*bolt.DB{}
)
func dbPath() string { return filepath.Join(AjeanHome(), "ajean.db") }
// db ouvre la base à la première utilisation et la garde ouverte pour la durée
// du process. bbolt prend un verrou exclusif sur le fichier : le délai d'attente
// évite qu'un second process (l'app lancée deux fois) se bloque indéfiniment.
// db ouvre la base (ou renvoie celle déjà ouverte pour ce chemin) et la garde
// ouverte pour la durée du process. bbolt prend un verrou exclusif sur le
// fichier : le délai d'attente évite qu'un second process (l'app lancée deux
// fois) se bloque indéfiniment.
func db() (*bolt.DB, error) {
dbOnce.Do(func() {
if err := os.MkdirAll(AjeanHome(), 0o755); err != nil {
dbErr = err
return
}
d, err := bolt.Open(dbPath(), 0o600, &bolt.Options{Timeout: 3 * time.Second})
if err != nil {
dbErr = fmt.Errorf("base %s inaccessible : %w", dbPath(), err)
return
}
err = d.Update(func(tx *bolt.Tx) error {
for _, b := range []string{bkConfig, bkPrefs, bkState, bkChat} {
if _, err := tx.CreateBucketIfNotExists([]byte(b)); err != nil {
return err
}
path := dbPath()
dbMu.Lock()
defer dbMu.Unlock()
if d, ok := dbConns[path]; ok {
return d, nil
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
d, err := bolt.Open(path, 0o600, &bolt.Options{Timeout: 3 * time.Second})
if err != nil {
return nil, fmt.Errorf("base %s inaccessible : %w", path, err)
}
err = d.Update(func(tx *bolt.Tx) error {
for _, b := range []string{bkConfig, bkPrefs, bkState, bkChat} {
if _, err := tx.CreateBucketIfNotExists([]byte(b)); err != nil {
return err
}
return nil
})
if err != nil {
d.Close()
dbErr = err
return
}
dbConn = d
return nil
})
return dbConn, dbErr
if err != nil {
d.Close()
return nil, err
}
dbConns[path] = d
return d, nil
}
// getBytes lit une valeur. Une base inaccessible se comporte comme une base
// vide : les appelants sont des lecteurs de réglages, aucun n'a de recours utile
// face à une erreur d'E/S, et tous ont déjà un défaut.
// closeDB ferme la base ouverte pour le dossier courant, s'il y en a une. bbolt
// garde un verrou exclusif sur son fichier tant qu'il est ouvert : sans ça,
// impossible de supprimer ou déplacer le dossier de données sous Windows.
func closeDB() {
path := dbPath()
dbMu.Lock()
defer dbMu.Unlock()
if d, ok := dbConns[path]; ok {
_ = d.Close()
delete(dbConns, path)
}
}
func getBytes(bucket, key string) []byte {
d, err := db()
if err != nil {
+67
View File
@@ -0,0 +1,67 @@
package ajean
import "testing"
// testHome donne au test un $AJEAN_HOME à lui et ferme la base à la fin.
//
// La fermeture n'est pas une politesse : bbolt garde un verrou exclusif sur son
// fichier, et sous Windows le ménage de t.TempDir() échoue tant qu'il est ouvert.
func testHome(t *testing.T) string {
t.Helper()
home := t.TempDir()
t.Setenv("AJEAN_HOME", home)
t.Cleanup(closeDB)
return home
}
// La configuration écrite doit se relire à l'identique, et une clé vidée
// disparaître au lieu de rester à "".
func TestConfigAllerRetour(t *testing.T) {
testHome(t)
if err := WriteConfig(map[string]string{"MODEL": "m.gguf", "CTX": "4096"}); err != nil {
t.Fatal(err)
}
if cfg := ReadConfig(); cfg["MODEL"] != "m.gguf" || cfg["CTX"] != "4096" {
t.Fatalf("configuration relue incorrecte : %v", cfg)
}
if err := SetConfigKey("CTX", ""); err != nil {
t.Fatal(err)
}
if _, ok := ReadConfig()["CTX"]; ok {
t.Error("CTX vidée mais toujours présente")
}
}
// WriteConfig REMPLACE : aucune clé de l'ancienne configuration ne doit
// survivre, sans quoi une bascule de preset laisserait des réglages fantômes.
func TestWriteConfigRemplaceTout(t *testing.T) {
testHome(t)
if err := WriteConfig(map[string]string{"A": "1", "B": "2"}); err != nil {
t.Fatal(err)
}
if err := WriteConfig(map[string]string{"B": "3"}); err != nil {
t.Fatal(err)
}
cfg := ReadConfig()
if _, ok := cfg["A"]; ok {
t.Error("A a survécu au remplacement")
}
if cfg["B"] != "3" {
t.Errorf("B = %q, attendu 3", cfg["B"])
}
}
// Une base absente se comporte comme une base vide : les lecteurs ont tous un
// défaut, aucun ne doit paniquer.
func TestLecturesSurBaseVide(t *testing.T) {
testHome(t)
if len(ReadConfig()) != 0 {
t.Error("configuration non vide sur une base neuve")
}
if agentEnabled() || internetEnabled() {
t.Error("interrupteurs actifs sur une base neuve")
}
if readAPIKey() != "" || readWebKey() != "" || readLinkToken() != "" {
t.Error("clés non vides sur une base neuve")
}
}
-1
View File
@@ -17,7 +17,6 @@ import (
// sur « systemctl: command not found » / « /etc/default/ajean », cf. issue #4).
// Implémentation prudente ; à valider sur une machine Apple.
// launchdPlistTemplate — champs formatés : Label, chemin du binaire, UserName,
// WorkingDirectory, AJEAN_HOME, StandardOutPath, StandardErrorPath.
// KeepAlive/SuccessfulExit=false ≈ Restart=on-failure ; RunAtLoad relance au
+1 -2
View File
@@ -11,7 +11,6 @@ import (
"strings"
)
const sudoersTemplate = `# Allow %s to manage the %s systemd unit without a password (installed by ajean).
%s ALL=(root) NOPASSWD: /bin/systemctl start %s, /bin/systemctl stop %s, /bin/systemctl restart %s, /bin/systemctl enable %s, /bin/systemctl disable %s
`
@@ -110,7 +109,7 @@ func cmdInstall(args []string) error {
}
fmt.Printf(" %s /etc/default/ajean\n", green("✓"))
// 5. Write the systemd unit (ExecStart = `ajean serve`, no start.sh needed)
// 4. Write the systemd unit (ExecStart = `ajean serve`, no start.sh needed)
unit := fmt.Sprintf(serviceUnitTemplate, targetUser, ajeanHome, installedExePath()+" serve")
unitPath := "/etc/systemd/system/" + svc + ".service"
if err := os.WriteFile(unitPath, []byte(unit), 0o644); err != nil {
-1
View File
@@ -16,7 +16,6 @@ import (
// service itself is managed by the PID-file supervisor in sys_service_windows.go
// (ajean start / stop / status), which needs no admin rights.
func cmdInstall(args []string) error {
ajeanHome := AjeanHome()
+10 -37
View File
@@ -15,48 +15,21 @@ func relWith(names ...string) *ghRelease {
return rel
}
// Une release de transition publie les deux noms : on doit préférer ajean-*.
func TestPickAssetPrefersAjean(t *testing.T) {
names := updateAssetNames()
got, url, _ := pickAsset(relWith(names[1], names[0]))
if got != names[0] {
t.Fatalf("got %q, attendu la préférence %q", got, names[0])
// L'asset de la plateforme est trouvé au milieu des autres, et le nom renvoyé
// est bien celui qui servira à vérifier le SHA-256.
func TestPickAssetTrouveSaPlateforme(t *testing.T) {
want := updateAssetName()
got, url, size := pickAsset(relWith("SHA256SUMS", "ajean-plan9-386", want))
if got != want {
t.Fatalf("got %q, attendu %q", got, want)
}
if url == "" {
t.Fatal("URL vide")
}
}
// Release future, purgée des anciens noms : ajean-* seul suffit.
func TestPickAssetAjeanOnly(t *testing.T) {
names := updateAssetNames()
if got, _, _ := pickAsset(relWith(names[0])); got != names[0] {
t.Fatalf("got %q, attendu %q", got, names[0])
}
}
// Ancienne release, antérieure au renommage : le repli sur ajean-* doit marcher,
// sinon un client neuf ne pourrait pas s'installer depuis une release existante.
func TestPickAssetFallsBackToLegacy(t *testing.T) {
names := updateAssetNames()
got, _, _ := pickAsset(relWith(names[1]))
if got != names[1] {
t.Fatalf("got %q, attendu le repli %q", got, names[1])
}
}
// Le nom renvoyé sert à vérifier le SHA-256 : il doit être celui réellement
// choisi, pas le nom préféré, sinon la vérification échoue sur un repli.
func TestPickAssetReturnsCheckedName(t *testing.T) {
names := updateAssetNames()
got, url, size := pickAsset(relWith(names[1]))
if got != names[1] || url != "https://example.invalid/"+names[1] || size != 42 {
t.Fatalf("incohérence nom/URL/taille: %q %q %d", got, url, size)
if url != "https://example.invalid/"+want || size != 42 {
t.Fatalf("incohérence URL/taille : %q %d", url, size)
}
}
// Aucun asset pour cette plateforme → pas d'URL, l'appelant doit pouvoir le voir.
func TestPickAssetNoMatch(t *testing.T) {
func TestPickAssetSansCorrespondance(t *testing.T) {
if _, url, _ := pickAsset(relWith("ajean-plan9-386", "SHA256SUMS")); url != "" {
t.Fatalf("URL inattendue: %q", url)
}