Files
Loki/internal/ajean/chat_export_test.go
T
nathaninline 52baab5786 v0.8.4 : l'historique s'exporte, le moteur se laisse joindre, les modeles en plusieurs fichiers marchent
Trois retours d'utilisateurs apres la 0.8.3.

Export de la conversation (regression de la 0.8 : conversation.json a
disparu dans ajean.db, verrouille et binaire). Bouton dans l'UI +
commande 'ajean export [--json] [fichier]', endpoint /api/chat/export.

Ecoute reseau du moteur : interrupteur 'joignable depuis le reseau local'
dans le panneau Acces OpenAI (HOST + regle de pare-feu Windows), commande
'ajean network [on|off|status]'. HOST devient un reglage de machine
preserve au changement de preset.

Modeles GGUF decoupes en tranches -00001-of-000NN : une famille = un
modele. Telechargement de toutes les tranches depuis n'importe quel lien,
sonde d'espace disque sur le total, liste et suppression regroupees,
demarrage refuse avec le nom du fichier manquant.
2026-08-08 20:01:54 +02:00

129 lines
3.8 KiB
Go

package ajean
import (
"encoding/json"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// convDeTest pose un fil complet (question, raisonnement, outil, réponse) dans la
// conversation globale et la remet à zéro en fin de test.
func convDeTest(t *testing.T) {
t.Helper()
t.Cleanup(func() { conv.Reset() })
conv.mu.Lock()
conv.Messages = []Message{
{Role: "user", Content: "combien font 2+2 ?"},
{Role: "assistant", Content: "4"},
}
conv.Log = []LogEvent{
{Seq: 1, TS: 1, Delta: map[string]any{"user": "combien font 2+2 ?"}},
{Seq: 2, TS: 2, Delta: map[string]any{"reasoning_content": "addition simple"}},
{Seq: 3, TS: 3, Delta: map[string]any{"tool_used": map[string]any{
"name": "bash", "label": "echo 4", "result": "4", "done": true}}},
{Seq: 4, TS: 4, Delta: map[string]any{"content": "4"}},
{Seq: 5, TS: 5, Delta: map[string]any{"turn_done": true}},
}
conv.Seq = 5
conv.CtxUsed = 123
conv.mu.Unlock()
}
func TestExportMarkdownPorteToutLeFil(t *testing.T) {
convDeTest(t)
md := conv.ExportMarkdown()
for _, want := range []string{
"## Vous", "combien font 2+2 ?",
"## AJEAN",
"<summary>Raisonnement</summary>", "addition simple",
"bash", "echo 4",
} {
if !strings.Contains(md, want) {
t.Errorf("Markdown exporté sans %q :\n%s", want, md)
}
}
// L'en-tête AJEAN ne doit apparaître qu'UNE fois pour un tour, même entrecoupé
// d'un appel d'outil.
if n := strings.Count(md, "\n## AJEAN\n"); n != 1 {
t.Errorf("en-tête assistant répété %d fois", n)
}
}
func TestExportJSONRelisible(t *testing.T) {
convDeTest(t)
b, err := conv.ExportJSON()
if err != nil {
t.Fatal(err)
}
var p exportPayload
if err := json.Unmarshal(b, &p); err != nil {
t.Fatal(err)
}
if len(p.Messages) != 2 || len(p.Log) != 5 {
t.Fatalf("export tronqué : %d messages, %d événements", len(p.Messages), len(p.Log))
}
if p.CtxUsed != 123 || p.Version != Version {
t.Fatalf("métadonnées absentes : ctx=%d version=%q", p.CtxUsed, p.Version)
}
}
// L'endpoint doit se présenter en TÉLÉCHARGEMENT (et pas s'afficher dans
// l'onglet), avec un nom de fichier horodaté.
func TestHandleChatExportEnPieceJointe(t *testing.T) {
convDeTest(t)
for _, tc := range []struct{ format, ctype, ext string }{
{"", "text/markdown", ".md"},
{"json", "application/json", ".json"},
} {
rr := httptest.NewRecorder()
handleChatExport(rr, httptest.NewRequest("GET", "/api/chat/export?format="+tc.format, nil))
if rr.Code != 200 {
t.Fatalf("format=%q : HTTP %d", tc.format, rr.Code)
}
if ct := rr.Header().Get("Content-Type"); !strings.HasPrefix(ct, tc.ctype) {
t.Errorf("format=%q : Content-Type %q", tc.format, ct)
}
cd := rr.Header().Get("Content-Disposition")
if !strings.HasPrefix(cd, "attachment;") || !strings.Contains(cd, tc.ext) {
t.Errorf("format=%q : Content-Disposition %q", tc.format, cd)
}
if rr.Body.Len() == 0 {
t.Errorf("format=%q : corps vide", tc.format)
}
}
}
func TestCmdExportEcritLeFichier(t *testing.T) {
testHome(t)
convDeTest(t)
// LoadConversation (appelé par cmdExport) relit la base : on y persiste d'abord
// le fil, sinon la commande exporterait une conversation vide.
conv.persist()
dir := t.TempDir()
for _, name := range []string{"fil.md", "fil.json"} {
p := filepath.Join(dir, name)
if err := cmdExport([]string{p}); err != nil {
t.Fatalf("%s : %v", name, err)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), "2+2") {
t.Fatalf("%s ne contient pas la conversation :\n%s", name, b)
}
}
// L'extension impose le format, même sans drapeau.
b, _ := os.ReadFile(filepath.Join(dir, "fil.json"))
if !json.Valid(b) {
t.Fatal("fil.json n'est pas du JSON valide")
}
if err := cmdExport([]string{"--zzz"}); err == nil {
t.Fatal("option inconnue acceptée")
}
}