mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 04:10:04 +02:00
PROJ_SNAPSHOT était le dernier réglage qui gardait la conversation au Go : la façade la tient désormais dans tous les cas (LOKI_CHAT=go reste la porte de sortie). facadeChatBlocker et la décision prise une fois par processus disparaissent. Go (facade_projsnap.go) : - /internal/v1/turn/view, avec la clé et l'agent : tête du tour portant le bloc figé (ou le vivant à rafraîchir), préambule sans date ni dossier, et la décision projsnap (rafraîchir, mise à jour à poser, instantané à ranger, clé et système du tour) ; dry pour le préchauffage ; - /internal/v1/turn/projsnap/refresh : bloc vivant et instantané qu'il devient, pour une compaction en plein tour. Rust : - ChatService::assemble_turn, partagé par le tour et la relance du builder : instantané rangé avec la discussion, mise à jour posée en tête du dernier message utilisateur (prependCtxUpdate), historique nettoyé au rafraîchissement ; - TurnHost::project_refresh après une compaction ou une réduction en plein tour (replaceProjectHead) ; l'instantané posé devient celui de la discussion ; - le préchauffage demande la vue en dry et garde les mises à jour tant que le bloc n'est pas rafraîchi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
170 lines
6.3 KiB
Go
170 lines
6.3 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// projSnapFacade : /internal/v1/turn/view avec la clé, décodé.
|
|
type projSnapFacadeView struct {
|
|
Head []Message `json:"head"`
|
|
Prefix string `json:"prefix"`
|
|
Tools []Tool `json:"tools"`
|
|
ProjSnap struct {
|
|
Refresh bool `json:"refresh"`
|
|
Delta string `json:"delta"`
|
|
Snap *projSnapshot `json:"snap"`
|
|
Key string `json:"key"`
|
|
SysHash string `json:"sys_hash"`
|
|
} `json:"projsnap"`
|
|
}
|
|
|
|
func projSnapFacade(t *testing.T, caps Caps, snap *projSnapshot, dry bool) projSnapFacadeView {
|
|
t.Helper()
|
|
body, _ := json.Marshal(map[string]any{"caps": caps, "snap": snap, "dry": dry})
|
|
w := appelOAIInterne(t, "POST", "/internal/v1/turn/view", string(body))
|
|
if w.Code != 200 {
|
|
t.Fatalf("vue : %d %s", w.Code, w.Body.String())
|
|
}
|
|
var v projSnapFacadeView
|
|
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// msgsJSON : la séquence telle qu'elle part (forme JSON, sans les champs
|
|
// propres au Go).
|
|
func msgsJSON(t *testing.T, msgs []Message) string {
|
|
t.Helper()
|
|
b, err := json.Marshal(msgs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// La façade assemble ce que turnView aurait envoyé : instantané pris au
|
|
// premier tour, mise à jour posée en tête du message suivant après un
|
|
// changement, l'instantané de la façade rangé et relu.
|
|
func TestFacadeProjSnapCommeTurnView(t *testing.T) {
|
|
projSnapSetup(t, true)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
caps := Caps{Agent: true, Mem: MemAlways}
|
|
|
|
// Premier tour : rien de figé, instantané neuf.
|
|
v := projSnapFacade(t, caps, nil, false)
|
|
if !v.ProjSnap.Refresh || v.ProjSnap.Snap == nil || v.ProjSnap.Delta != "" {
|
|
t.Fatalf("premier tour : %+v", v.ProjSnap)
|
|
}
|
|
c := newTestConv()
|
|
msgs := []Message{um("bonjour")}
|
|
c.Messages = append([]Message(nil), msgs...)
|
|
sent, tools, _ := c.turnView(caps, c.epoch, msgs, true)
|
|
got := withPrefix(append(append([]Message(nil), v.Head...), msgs...), v.Prefix)
|
|
if msgsJSON(t, got) != msgsJSON(t, sent) {
|
|
t.Fatalf("séquence différente de turnView :\n%s\n%s", msgsJSON(t, got), msgsJSON(t, sent))
|
|
}
|
|
if !reflect.DeepEqual(jsonValue(t, v.Tools), jsonValue(t, tools)) {
|
|
t.Fatal("outils différents de turnView")
|
|
}
|
|
if !reflect.DeepEqual(v.ProjSnap.Snap.State, c.ProjSnap.State) || v.ProjSnap.Snap.Key != c.ProjSnap.Key || v.ProjSnap.Snap.SysHash != c.ProjSnap.SysHash {
|
|
t.Fatalf("instantané différent :\n%+v\n%+v", v.ProjSnap.Snap, c.ProjSnap)
|
|
}
|
|
if v.ProjSnap.Key != c.ProjSnap.Key || v.ProjSnap.SysHash != c.ProjSnap.SysHash {
|
|
t.Fatal("clé du tour différente")
|
|
}
|
|
|
|
// Une page ajoutée : bloc figé gardé, mise à jour annoncée.
|
|
if err := MemAdd("nas.md", "# NAS\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snap := v.ProjSnap.Snap
|
|
v2 := projSnapFacade(t, caps, snap, false)
|
|
if v2.ProjSnap.Refresh || !strings.Contains(v2.ProjSnap.Delta, "](nas.md)") {
|
|
t.Fatalf("mise à jour attendue : %+v", v2.ProjSnap)
|
|
}
|
|
if msgsJSON(t, v2.Head) != msgsJSON(t, v.Head) {
|
|
t.Fatal("bloc figé non gardé")
|
|
}
|
|
c.Messages = append(c.Messages, am("ok."), um("et le NAS ?"))
|
|
msgs2 := append([]Message(nil), c.Messages...)
|
|
sent2, _, _ := c.turnView(caps, c.epoch, msgs2, true)
|
|
last := msgText(sent2[len(sent2)-1])
|
|
if last != v2.ProjSnap.Delta+"et le NAS ?" {
|
|
t.Fatalf("mise à jour différente de turnView :\n%q\n%q", last, v2.ProjSnap.Delta+"et le NAS ?")
|
|
}
|
|
next := v2.ProjSnap.Snap
|
|
if next == nil || next.State.Mem["nas.md"] == "" || next.DeltaTok <= snap.DeltaTok || !reflect.DeepEqual(next.State, c.ProjSnap.State) {
|
|
t.Fatalf("état annoncé non suivi : %+v", next)
|
|
}
|
|
// Sans nouveau changement : rien à poser, instantané inchangé.
|
|
v3 := projSnapFacade(t, caps, next, false)
|
|
if v3.ProjSnap.Refresh || v3.ProjSnap.Delta != "" || !reflect.DeepEqual(v3.ProjSnap.Snap.State, next.State) {
|
|
t.Fatalf("tour sans changement : %+v", v3.ProjSnap)
|
|
}
|
|
// dry (préchauffage) : la même décision, rien à ranger.
|
|
if err := MemAdd("docker.md", "# Docker\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := projSnapFacade(t, caps, next, true)
|
|
if d.ProjSnap.Refresh || d.ProjSnap.Snap != nil || d.ProjSnap.Delta != "" || msgsJSON(t, d.Head) != msgsJSON(t, v.Head) {
|
|
t.Fatalf("préchauffage : %+v", d.ProjSnap)
|
|
}
|
|
// Instantané d'un autre lancement (clé) : rafraîchi.
|
|
stale := *next
|
|
stale.Key = "autre"
|
|
if r := projSnapFacade(t, caps, &stale, false); !r.ProjSnap.Refresh {
|
|
t.Fatal("clé changée : rafraîchissement attendu")
|
|
}
|
|
if r := projSnapFacade(t, caps, &stale, true); !r.ProjSnap.Refresh || msgsJSON(t, r.Head) == msgsJSON(t, v.Head) {
|
|
t.Fatal("préchauffage d'un instantané périmé : bloc vivant attendu")
|
|
}
|
|
}
|
|
|
|
// Compaction en plein tour : le bloc vivant, et l'instantané qu'il devient
|
|
// avec la clé et le système du tour.
|
|
func TestFacadeProjSnapRafraichi(t *testing.T) {
|
|
projSnapSetup(t, true)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
if err := MemAdd("nas.md", "# NAS\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w := appelOAIInterne(t, "POST", "/internal/v1/turn/projsnap/refresh", `{"caps":{"Agent":true,"Mem":"always"},"key":"k1","sys_hash":"s1"}`)
|
|
if w.Code != 200 {
|
|
t.Fatalf("rafraîchissement : %d %s", w.Code, w.Body.String())
|
|
}
|
|
var out struct {
|
|
Block []Message `json:"block"`
|
|
Snap *projSnapshot `json:"snap"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
live := projSnapCapture(Caps{Agent: true, Mem: MemAlways, envInCtx: true}, projSnapNow())
|
|
if msgsJSON(t, out.Block) != msgsJSON(t, live.msgs) || len(out.Block) == 0 {
|
|
t.Fatalf("bloc vivant différent : %s", msgsJSON(t, out.Block))
|
|
}
|
|
if out.Snap == nil || out.Snap.Key != "k1" || out.Snap.SysHash != "s1" || out.Snap.State.Mem["nas.md"] == "" {
|
|
t.Fatalf("instantané : %+v", out.Snap)
|
|
}
|
|
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/projsnap/refresh", `{`); w.Code != 400 {
|
|
t.Fatalf("corps illisible : %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// withPrefix : ce que la façade fait du préambule rendu (inject_prefix,
|
|
// comme InjectSkills avec ce texte).
|
|
func withPrefix(msgs []Message, prefix string) []Message {
|
|
if prefix == "" {
|
|
return msgs
|
|
}
|
|
if len(msgs) > 0 && msgs[0].Role == "system" && !isProjectSystem(msgs[0]) {
|
|
existing, _ := msgs[0].Content.(string)
|
|
return append([]Message{{Role: "system", Content: prefix + "\n\n" + existing}}, msgs[1:]...)
|
|
}
|
|
return append([]Message{{Role: "system", Content: prefix}}, msgs...)
|
|
}
|