mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 12:16:21 +02:00
SIDE_SLOT et SLOT_PERSIST ne gardent plus la conversation au Go. La machinerie reste au Go (configuration, requêtes en vol, tampon du slot) ; la façade la consulte avant chaque requête au moteur local. Go : - /internal/v1/turn/slot (facade_slots.go) rend engineSlotFor et, pour une étape de tour, recharge d'abord l'état gardé de la discussion (slotPersistRestore). La sauvegarde à la bascule voit les étapes de la façade : /internal/v1/oai/served marque le slot (engineMarkMain). - facadeChatBlocker : SIDE_SLOT et SLOT_PERSIST retirés. Rust : - TurnHost::engine_slot : l'id_slot posé dans chaque requête (0 pour le fil de la discussion, 1 pour la vérification), le ticket « en vol » ouvert sur le second slot. - Cache KV plein en plein calcul (sideSlotPoolError) : rejoué deux fois tel quel, sans compaction, puis l'erreur. - Résumés de compaction comptés en vol comme postSummary : transcription sur le second slot, continuation sur le slot 0. - La passe de vérification est un travail annexe (engineSideJob) : le Go efface le slot à la fin s'il a servi (écart du lot 7 levé). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
130 lines
4.0 KiB
Go
130 lines
4.0 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// Le slot d'une requête de la façade : aucun sans la clé (et /props jamais
|
|
// lu), le fil de la discussion sur le 0 et le reste sur le 1 quand le second
|
|
// slot est en service.
|
|
func TestFacadeTurnSlot(t *testing.T) {
|
|
testHome(t)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
slot := func(body string) (int, float64) {
|
|
w := appelOAIInterne(t, "POST", "/internal/v1/turn/slot", body)
|
|
var out struct {
|
|
Slot float64 `json:"slot"`
|
|
}
|
|
_ = json.Unmarshal(w.Body.Bytes(), &out)
|
|
return w.Code, out.Slot
|
|
}
|
|
prev := sideSlotProps
|
|
sideSlotProps = func(chatEndpoint) (int, int, bool) { t.Fatal("/props lu sans la clé"); return 0, 0, false }
|
|
for _, b := range []string{`{"conv":"c1","main":true}`, `{"main":false}`} {
|
|
if code, s := slot(b); code != 200 || s != -1 {
|
|
t.Fatalf("sans la clé %s : %d %v", b, code, s)
|
|
}
|
|
}
|
|
sideSlotProps = prev
|
|
if err := SetConfigKey("SIDE_SLOT", "on"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SetConfigKey("CTX", "8192"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stubSideProps(t, 2, 8192)
|
|
if _, s := slot(`{"conv":"c1","main":true}`); s != 0 {
|
|
t.Fatalf("étape du tour : slot %v", s)
|
|
}
|
|
if _, s := slot(`{"main":false}`); s != 1 {
|
|
t.Fatalf("vérification : slot %v", s)
|
|
}
|
|
if _, s := slot(`{"main":true}`); s != 0 {
|
|
t.Fatalf("résumé en continuation : slot %v", s)
|
|
}
|
|
// Second slot hors service : plus d'id_slot.
|
|
stubSideProps(t, 1, 8192)
|
|
if _, s := slot(`{"main":false}`); s != -1 {
|
|
t.Fatalf("un seul slot : %v", s)
|
|
}
|
|
if code, _ := slot(`{`); code != 400 {
|
|
t.Fatalf("corps illisible : %d", code)
|
|
}
|
|
if w := appelOAIInterne(t, "GET", "/internal/v1/turn/slot", ""); w.Code != 405 {
|
|
t.Fatalf("GET : %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// SLOT_PERSIST : l'état gardé à la bascule est rechargé avant la première
|
|
// étape du tour de CETTE discussion, une seule fois ; jamais pour une autre
|
|
// discussion, une requête annexe ou un résumé.
|
|
func TestFacadeTurnSlotRecharge(t *testing.T) {
|
|
m, home := persistSetup(t, true)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
snap := slotPersistPrepare()
|
|
if snap == nil {
|
|
t.Fatal("rien à garder")
|
|
}
|
|
snap.save()
|
|
name := slotPersistName(persistTestKey, "c1")
|
|
for _, b := range []string{
|
|
`{"conv":"autre","main":true}`,
|
|
`{"conv":"c1","main":false}`,
|
|
`{"main":true}`,
|
|
`{"conv":"c1","main":true}`,
|
|
`{"conv":"c1","main":true}`,
|
|
} {
|
|
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/slot", b); w.Code != 200 {
|
|
t.Fatalf("%s : %d", b, w.Code)
|
|
}
|
|
}
|
|
want := []string{"save tmp-" + name, "restore " + name}
|
|
if got := m.got(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("appels %v, attendu %v", got, want)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(home, "slots", name)); !os.IsNotExist(err) {
|
|
t.Fatal("fichier gardé après la tentative")
|
|
}
|
|
}
|
|
|
|
// Une étape de tour de la façade acceptée par le moteur marque le slot :
|
|
// c'est ce que la sauvegarde SLOT_PERSIST relit à la bascule.
|
|
func TestFacadeTurnSlotSauvegarde(t *testing.T) {
|
|
m, home := persistSetup(t, true)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
engineGate.mu.Lock()
|
|
engineGate.main = engineMainStamp{}
|
|
engineGate.mu.Unlock()
|
|
if slotPersistPrepare() != nil {
|
|
t.Fatal("slot sans discussion : rien à garder")
|
|
}
|
|
w := appelOAIInterne(t, "POST", "/internal/v1/oai/begin", `{"main_conv":"c2"}`)
|
|
var b struct {
|
|
Ticket uint64 `json:"ticket"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &b); err != nil || b.Ticket == 0 {
|
|
t.Fatalf("begin : %s", w.Body.String())
|
|
}
|
|
tk := `{"ticket":` + jsonNum(b.Ticket) + `}`
|
|
appelOAIInterne(t, "POST", "/internal/v1/oai/served", tk)
|
|
appelOAIInterne(t, "POST", "/internal/v1/oai/end", tk)
|
|
snap := slotPersistPrepare()
|
|
if snap == nil {
|
|
t.Fatal("étape de la façade non vue par la sauvegarde")
|
|
}
|
|
snap.save()
|
|
name := slotPersistName(persistTestKey, "c2")
|
|
if _, err := os.Stat(filepath.Join(home, "slots", name)); err != nil {
|
|
t.Fatalf("état non gardé : %v (%v)", err, m.got())
|
|
}
|
|
}
|
|
|
|
func jsonNum(n uint64) string {
|
|
b, _ := json.Marshal(n)
|
|
return string(b)
|
|
}
|