Files
Loki/internal/loki/facade_slots_test.go
T
Claude a005626e66 Lot 31 : slots du moteur pour le tour tenu par la façade
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
2026-10-16 02:13:20 +00:00

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)
}