mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
Rebaptise AJEAN en Loki (fork, lignée conservée)
- module github.com/R0m1k3/Loki, cmd/loki, internal/loki (package loki) - LOKI_HOME, LOKI_MODEL_DIRS, LOKI_SERVICE, LOKI_DL_CONNS ; /etc/loki ; units loki-engine / loki-ui ; binaire et aide CLI - updateRepo pointe sur R0m1k3/Loki (l'auto-update ne tirera plus les binaires AJEAN amont) Conservé à l'identique : le domaine ajean.link (service de tunnel amont), les littéraux de migration 0.7.x (migrate_07.go), RELEASE_NOTES.md et LICENSE (historique et licence de l'amont). go build/vet/test : verts.
This commit is contained in:
157 files changed
+928
-928
No files matched your search
@@ -0,0 +1,172 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// helpers pour construire des historiques de test lisibles.
|
||||
func um(s string) Message { return Message{Role: "user", Content: s} }
|
||||
func am(s string) Message { return Message{Role: "assistant", Content: s} }
|
||||
func atc(name string) Message {
|
||||
return Message{Role: "assistant", ToolCalls: []ToolCall{{ID: "c1", Function: ToolCallFunc{Name: name, Arguments: "{}"}}}}
|
||||
}
|
||||
func tm(s string) Message { return Message{Role: "tool", ToolCallID: "c1", Content: s} }
|
||||
|
||||
func TestCompactBoundsProtectsHead(t *testing.T) {
|
||||
msgs := []Message{
|
||||
{Role: "system", Content: "sys"},
|
||||
um("premier"), am("r1"),
|
||||
um("q2"), am("r2"),
|
||||
um("q3"), am("r3"),
|
||||
}
|
||||
// budget minuscule → queue = juste le dernier tour, tête = system + 1er user.
|
||||
head, tail := compactBounds(msgs, 1)
|
||||
if head != 2 { // system + premier user
|
||||
t.Fatalf("head = %d, attendu 2", head)
|
||||
}
|
||||
// Frontière sûre = user ou assistant (jamais un `tool`, qui serait orphelin).
|
||||
if r := msgs[tail].Role; r != "user" && r != "assistant" {
|
||||
t.Fatalf("la queue doit démarrer sur un user ou un assistant, obtenu %q", r)
|
||||
}
|
||||
if tail <= head {
|
||||
t.Fatalf("torse vide (tail=%d head=%d) alors qu'il y a du milieu à compacter", tail, head)
|
||||
}
|
||||
}
|
||||
|
||||
// La queue ne doit jamais démarrer entre un assistant+tool_calls et ses
|
||||
// résultats `tool` : elle recule jusqu'à la frontière sûre précédente (user ou
|
||||
// assistant), donc l'assistant part dans la queue AVEC ses résultats.
|
||||
func TestCompactBoundsKeepsToolPairs(t *testing.T) {
|
||||
msgs := []Message{
|
||||
um("q1"), am("r1"),
|
||||
um("q2"), atc("bash"), tm("sortie longue"), am("r2"),
|
||||
um("q3"), am("r3"),
|
||||
}
|
||||
// budget moyen qui, sans le recul, couperait au milieu du tour outillé.
|
||||
_, tail := compactBounds(msgs, msgTokens(msgs[6])+msgTokens(msgs[7])+msgTokens(msgs[5])+1)
|
||||
if r := msgs[tail].Role; r != "user" && r != "assistant" {
|
||||
t.Fatalf("la queue démarre sur %q : frontière non sûre (orphelin tool possible)", r)
|
||||
}
|
||||
// Vérifie qu'aucun message `tool` de la queue n'a perdu son assistant parent.
|
||||
for i := tail; i < len(msgs); i++ {
|
||||
if msgs[i].Role == "tool" {
|
||||
if i == tail || (msgs[i-1].Role != "assistant" && msgs[i-1].Role != "tool") {
|
||||
t.Fatalf("message tool orphelin à l'index %d de la queue", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Régression : DEUXIÈME compaction à l'intérieur d'un même tour. Une longue
|
||||
// boucle d'outils (recherche web : dix pages lues d'affilée) ne contient AUCUN
|
||||
// message `user` — reculer jusqu'à un `user` faisait donc avaler toute la
|
||||
// séquence par la queue, torse vide, compaction sans effet. La queue doit
|
||||
// pouvoir démarrer sur un `assistant`, et les gros résultats d'outils du début
|
||||
// doivent se retrouver dans le torse (donc résumés).
|
||||
func TestCompactBoundsSplitsLongToolLoop(t *testing.T) {
|
||||
page := func(n int) Message {
|
||||
return tm("contenu de page web très long " + string(rune('a'+n)) + strings.Repeat("x", 400))
|
||||
}
|
||||
msgs := []Message{
|
||||
um("premier"), am("ok"), // head : 1er user protégé
|
||||
um("cherche des trucs"), // la dernière demande utilisateur du tour
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
msgs = append(msgs, atc("web_read"), page(i))
|
||||
}
|
||||
head, tail := compactBounds(msgs, estimateTokens(msgs)/4)
|
||||
if tail <= head {
|
||||
t.Fatalf("torse vide (tail=%d head=%d) : la boucle d'outils n'est pas compactable", tail, head)
|
||||
}
|
||||
if r := msgs[tail].Role; r != "user" && r != "assistant" {
|
||||
t.Fatalf("la queue démarre sur %q : frontière non sûre", r)
|
||||
}
|
||||
// Aucun `tool` orphelin en queue.
|
||||
for i := tail; i < len(msgs); i++ {
|
||||
if msgs[i].Role == "tool" && (i == tail || (msgs[i-1].Role != "assistant" && msgs[i-1].Role != "tool")) {
|
||||
t.Fatalf("message tool orphelin à l'index %d de la queue", i)
|
||||
}
|
||||
}
|
||||
// Le torse doit bien contenir des résultats d'outils (c'est ce qui remplit
|
||||
// la fenêtre) — sinon la compaction ne libérerait rien.
|
||||
tools := 0
|
||||
for _, m := range msgs[head:tail] {
|
||||
if m.Role == "tool" {
|
||||
tools++
|
||||
}
|
||||
}
|
||||
if tools == 0 {
|
||||
t.Fatal("aucun résultat d'outil dans le torse : rien à gagner à compacter")
|
||||
}
|
||||
}
|
||||
|
||||
// Régression : après compaction pendant une recherche web, la DEMANDE EN COURS
|
||||
// doit encore figurer telle quelle dans l'historique. Sans ça, le seul message
|
||||
// `user` restant était le tout premier de la conversation (épinglé en tête) et
|
||||
// le modèle répondait à celui-là au lieu de continuer la recherche.
|
||||
func TestCompactKeepsPendingRequest(t *testing.T) {
|
||||
page := func(n int) Message {
|
||||
return tm("contenu de page web très long " + string(rune('a'+n)) + strings.Repeat("x", 400))
|
||||
}
|
||||
msgs := []Message{
|
||||
um("première question de la conversation"), am("ok"),
|
||||
um("cherche les horaires du train pour Lyon"),
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
msgs = append(msgs, atc("web_read"), page(i))
|
||||
}
|
||||
// summarizeTranscript échoue (pas de llama-server en test) → torse dégraissé,
|
||||
// ce qui n'enlève rien à ce qu'on vérifie ici : la demande doit survivre.
|
||||
out, _ := compactMessages(t.Context(), msgs, Caps{})
|
||||
found := false
|
||||
for _, m := range out {
|
||||
if m.Role == "user" && strings.Contains(msgText(m), "horaires du train") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("la demande en cours a disparu de l'historique compacté")
|
||||
}
|
||||
}
|
||||
|
||||
// Régression : ce qu'on envoie au RÉSUMEUR doit encore contenir les résultats
|
||||
// d'outils. On effaçait les résultats (dégraissage) AVANT de résumer : le
|
||||
// résumeur ne voyait que des marqueurs, le résumé ne pouvait donc porter aucune
|
||||
// information trouvée, et l'IA relançait la même recherche après chaque
|
||||
// compactage — sans jamais s'arrêter.
|
||||
func TestSummaryInputKeepsToolFindings(t *testing.T) {
|
||||
fait := "le train de 14h12 part quai 3"
|
||||
torso := []Message{
|
||||
atc("web_read"),
|
||||
tm(fait + strings.Repeat(" blabla de remplissage", 300)),
|
||||
}
|
||||
// Même transformation que compactMessages avant l'appel au résumeur.
|
||||
forSummary := make([]Message, len(torso))
|
||||
for i, m := range torso {
|
||||
forSummary[i] = m
|
||||
if m.Role == "tool" {
|
||||
if r := []rune(msgText(m)); len(r) > compactToolSummaryLen {
|
||||
forSummary[i].Content = string(r[:compactToolSummaryLen]) + "\n[…suite coupée]"
|
||||
}
|
||||
}
|
||||
}
|
||||
tr := renderTranscript(forSummary)
|
||||
if !strings.Contains(tr, fait) {
|
||||
t.Fatal("le fait trouvé n'atteint pas le résumeur : le résumé sera vide d'information")
|
||||
}
|
||||
if strings.Contains(tr, compactPrunedMarker) {
|
||||
t.Fatal("le résumeur reçoit des marqueurs d'effacement au lieu du contenu")
|
||||
}
|
||||
if len([]rune(tr)) > 4000 {
|
||||
t.Fatalf("transcription non bornée (%d runes) : le résumeur va déborder", len([]rune(tr)))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEstimateTokensGrows(t *testing.T) {
|
||||
small := estimateTokens([]Message{um("court")})
|
||||
big := estimateTokens([]Message{um("un message nettement plus long que le précédent pour dépasser")})
|
||||
if big <= small {
|
||||
t.Fatalf("estimateTokens ne croît pas avec la taille: small=%d big=%d", small, big)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user