Files
Loki/internal/loki/chat_history_view_test.go
T
MichaelandClaude Opus 5.5 432308c5b2 Compaction : une compaction en cours de tour ne fait plus disparaître le rappel des pages lues
Après une compaction en cours de tour, generate rangeait la vue publiée en
retirant TOUS les messages système de tête. Ceux que la vue d'envoi avait
posés (préambule, prompt du preset, contexte du projet ou bloc figé) n'ont
rien à faire dans l'historique ; mais le rappel « [MEMORY PAGES REMINDER] »,
posé par une compaction de début de tour, en fait partie — il partait avec
eux, et le modèle perdait la liste des pages à relire.

- historyFromView : un système de tête n'est gardé que s'il figurait en tête
  de l'historique d'où la vue est partie (la compaction garde la tête telle
  quelle) ; un système fusionné par InjectSkills devant le préambule est rendu
  tel qu'il était.
- Même correction pour la reprise du builder (mode Code) et le terminal, où
  le /sys de l'utilisateur disparaissait de la même façon.
- Test : la compaction réactive en plein tour garde le rappel, avec et sans
  PROJ_SNAPSHOT, et ne range aucun système injecté ; échoue sur l'ancien code.

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

77 lines
2.8 KiB
Go

package loki
import (
"reflect"
"strings"
"testing"
)
// Compaction EN COURS de tour : la vue publiée perd ce que la vue d'envoi avait
// posé en tête (préambule, prompt du preset, contexte du projet), mais garde le
// rappel des pages lues, qui appartient à l'historique.
func TestCompactionEnTourGardeLeRappel(t *testing.T) {
for _, on := range []bool{false, true} {
m := projSnapSetup(t, on)
if err := saveSysPrompt("Réponds en français."); err != nil {
t.Fatal(err)
}
if err := MemAdd("nas.md", "# NAS\n"); err != nil {
t.Fatal(err)
}
reminder, _ := buildReminderMessage([]string{"nas.md"})
c := newTestConv()
c.Messages = []Message{reminder}
for i := 0; i < 12; i++ {
c.Messages = append(c.Messages, um("continue "+strings.Repeat("x", 600)), am("étape "+strings.Repeat("détail ", 300)))
}
n := len(m.all())
m.mu.Lock()
m.reply = func(k int, _ string) (int, string) {
if k == n {
return 400, `{"error":{"code":400,"message":"request (99999 tokens) exceeds the available context size"}}`
}
return 200, sseChunk("ok.") + sseFinal("stop", 100, 2)
}
m.mu.Unlock()
playTurn(t, m, c, Caps{Agent: true, Mem: MemAlways}, "et le NAS ?")
if len(m.all()) < n+2 {
t.Fatalf("clé %v : pas de requête rejouée après compaction", on)
}
if len(c.Messages) == 0 || !reflect.DeepEqual(c.Messages[0], reminder) {
t.Fatalf("clé %v : rappel des pages lues perdu : %+v", on, c.Messages[0])
}
for _, msg := range c.Messages[1:] {
if msg.Role == "system" {
t.Fatalf("clé %v : système injecté rangé dans l'historique : %q", on, msgText(msg))
}
}
if len(c.Messages) >= 25 {
t.Fatalf("clé %v : compaction non rangée (%d messages)", on, len(c.Messages))
}
if err := saveSysPrompt(""); err != nil {
t.Fatal(err)
}
}
}
func TestHistoryFromView(t *testing.T) {
rem := Message{Role: "system", Content: memReminderPrefix + " Memory pages you read earlier: a.md"}
sys := Message{Role: "system", Content: "/sys perso"}
u := um("q")
cases := []struct {
name string
view, hist []Message
want []Message
}{
{"injectés seuls", []Message{{Role: "system", Content: "préambule"}, {Role: "system", Content: projectContextPrefix + " x"}, u}, []Message{u}, []Message{u}},
{"rappel gardé", []Message{{Role: "system", Content: "préambule"}, {Role: "system", Content: projectContextPrefix + " x"}, rem, u}, []Message{rem, u}, []Message{rem, u}},
{"système fusionné rendu", []Message{{Role: "system", Content: "préambule\n\n/sys perso"}, u}, []Message{sys, u}, []Message{sys, u}},
{"sans système", []Message{u}, []Message{u}, []Message{u}},
}
for _, tc := range cases {
if got := historyFromView(tc.view, tc.hist); !reflect.DeepEqual(got, tc.want) {
t.Errorf("%s : %+v, attendu %+v", tc.name, got, tc.want)
}
}
}