Files
Loki/internal/loki/normalize_project_test.go
T
MichaelandClaude Opus 5.5 626227f2f2 Projets : changer de projet ne recalcule plus tout le prompt
Repris d'AJEAN 0.15.8, adapté : chez nous le contexte projet n'est jamais
persisté, il est injecté à chaque tour sous forme de messages système
préfixés — ce qui rend le déplacement trivial et sans migration.

LE PROBLÈME
Sur un modèle hybride (Qwen3.5 et suivants, couches récurrentes), llama.cpp
ne peut reprendre un prompt déjà calculé qu'à un point de sauvegarde, et il
n'en pose qu'au DÉBUT d'un message utilisateur. La description du projet,
l'index mémoire et l'index des trackers étaient fusionnés dans le bloc
système : deux projets divergeaient AVANT le premier point de reprise, et le
premier message après un changement de projet recalculait tout (l'amont
mesure ~8 s pour 5 000 tokens sur un 27B).

LE CORRECTIF
normalizeSystemMessages sort ces messages (isProjectSystem, par leurs
préfixes) du bloc système et les place, dans un bloc <project_context>, en
tête du PREMIER message utilisateur de la séquence envoyée. Le système
commun reste identique d'un projet à l'autre, donc en cache. L'historique
persisté et l'affichage ne changent pas (copie, entrée non mutée — testé).

InjectSkills ne fusionne plus son préambule DANS un message projet placé en
tête (cas d'un preset sans prompt système) : il y aurait perdu son préfixe
et serait resté dans le bloc commun.

Le gain ne vaut qu'entre projets de même mode mémoire : la consigne mémoire
fait partie du système commun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:04:51 +02:00

54 lines
2.0 KiB
Go

package loki
import (
"reflect"
"strings"
"testing"
)
// Le contexte PROJET quitte le bloc système pour la tête du 1er message user :
// le système commun reste identique d'un projet à l'autre (cache de prompt), et
// l'historique d'origine n'est pas muté.
func TestNormalizeMovesProjectContextToFirstUser(t *testing.T) {
in := []Message{
{Role: "system", Content: "Préambule commun"},
{Role: "system", Content: projectContextPrefix + " — projet A"},
{Role: "system", Content: memIndexPrefix + " for project A"},
{Role: "user", Content: "bonjour"},
{Role: "assistant", Content: "salut"},
{Role: "user", Content: "suite"},
}
ref := append([]Message(nil), in...)
out := normalizeSystemMessages(in)
if len(out) != 4 || out[0].Content != "Préambule commun" {
t.Fatalf("système commun attendu seul en tête : %#v", out)
}
first, _ := out[1].Content.(string)
if !strings.HasPrefix(first, "<project_context>\n"+projectContextPrefix) ||
!strings.Contains(first, memIndexPrefix) || !strings.HasSuffix(first, "bonjour") {
t.Fatalf("contexte projet absent du 1er message user : %q", first)
}
if out[3].Content != "suite" {
t.Fatal("seul le PREMIER message user reçoit le contexte")
}
if !reflect.DeepEqual(in, ref) {
t.Fatal("l'entrée a été mutée (interdit)")
}
}
// Sans prompt de preset, le premier système est un message projet : le préambule
// ne doit pas l'absorber (il perdrait son préfixe et resterait dans le système).
func TestInjectSkillsKeepsProjectMessageSeparate(t *testing.T) {
in := []Message{
{Role: "system", Content: projectContextPrefix + " — projet A"},
{Role: "user", Content: "bonjour"},
}
out := normalizeSystemMessages(InjectSkills(in, Caps{Agent: true}))
if s, _ := out[0].Content.(string); out[0].Role != "system" || strings.Contains(s, projectContextPrefix) {
t.Fatalf("le contexte projet ne doit pas rester dans le système : %#v", out[0])
}
if u, _ := out[1].Content.(string); !strings.Contains(u, projectContextPrefix) {
t.Fatalf("contexte projet attendu dans le 1er message user : %#v", out[1])
}
}