mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 14:08:11 +02:00
Lot 34 : bloc projet figé (PROJ_SNAPSHOT) dans le tour de la façade
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
This commit is contained in:
19 files changed
+1063
-171
No files matched your search
@@ -27,22 +27,8 @@ type fausseFacade struct {
|
||||
internalBody string
|
||||
}
|
||||
|
||||
// oublierDecisionFacade : la décision « qui tient la conversation » est prise
|
||||
// une fois par process ; chaque test repart de zéro.
|
||||
func oublierDecisionFacade(t *testing.T) {
|
||||
t.Helper()
|
||||
reset := func() {
|
||||
facadeDecisionMu.Lock()
|
||||
facadeDecisionDone, facadeDecisionWhy = false, ""
|
||||
facadeDecisionMu.Unlock()
|
||||
}
|
||||
reset()
|
||||
t.Cleanup(reset)
|
||||
}
|
||||
|
||||
func demarrerFacade(t *testing.T) *fausseFacade {
|
||||
t.Helper()
|
||||
oublierDecisionFacade(t)
|
||||
f := &fausseFacade{t: t, reply: `{"ok":true,"de":"facade"}`}
|
||||
f.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
@@ -266,56 +252,36 @@ func TestFacadeActivationDeProjet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Un réglage que la façade ne sait pas honorer garde la conversation ici :
|
||||
// rien ne lui est remis, et elle l'apprend au démarrage. La décision tient
|
||||
// jusqu'au redémarrage.
|
||||
func TestFacadeReglageQuiGardeLaConversation(t *testing.T) {
|
||||
// Plus aucun réglage ne garde la conversation ici (lot 34) : avec chacune des
|
||||
// clés opt-in posée, la façade la tient et le trafic de chat lui est remis.
|
||||
// Sans LOKI_CHAT=rust, ou sans son adresse : le Go, et pourquoi.
|
||||
func TestFacadeAucunReglageNeGardeLaConversation(t *testing.T) {
|
||||
testHome(t)
|
||||
f := demarrerFacade(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
for _, k := range []string{"PROJ_SNAPSHOT", "NUDGE_IN_TOOL", "SIDE_SLOT", "SLOT_PERSIST", "KEEP_TURN_IMAGES", "PREWARM"} {
|
||||
if err := SetConfigKey(k, "on"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
putStr(bkState, "retry_patterns", `["oups"]`)
|
||||
if !facadeChat() {
|
||||
t.Fatal("réglages opt-in posés : la façade doit tenir la conversation")
|
||||
}
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/chat/holder", ""); w.Code != 200 || !strings.Contains(w.Body.String(), `"holder":"rust"`) {
|
||||
t.Fatalf("détenteur : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
// Posé après la décision : sans effet avant le redémarrage.
|
||||
if err := SetConfigKey("PROJ_SNAPSHOT", "on"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !facadeChat() {
|
||||
t.Fatal("décision changée en cours de route")
|
||||
}
|
||||
oublierDecisionFacade(t)
|
||||
if facadeChat() {
|
||||
t.Fatal("PROJ_SNAPSHOT posé : la conversation doit rester au Go")
|
||||
}
|
||||
w := appelOAIInterne(t, "GET", "/internal/v1/chat/holder", "")
|
||||
if !strings.Contains(w.Body.String(), `"holder":"go"`) || !strings.Contains(w.Body.String(), "PROJ_SNAPSHOT") {
|
||||
t.Fatalf("détenteur : %s", w.Body.String())
|
||||
}
|
||||
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
|
||||
rec := httptest.NewRecorder()
|
||||
withFacadeChat("/api/chat/send", local)(rec, httptest.NewRequest("POST", "/api/chat/send", strings.NewReader(`{}`)))
|
||||
if rec.Body.String() != "go" || len(f.recu()) != 0 {
|
||||
t.Fatalf("remis à la façade malgré le réglage : %q %v", rec.Body.String(), f.recu())
|
||||
}
|
||||
for k, want := range map[string]string{
|
||||
"PROJ_SNAPSHOT": "PROJ_SNAPSHOT", "NUDGE_IN_TOOL": "", "SIDE_SLOT": "",
|
||||
"SLOT_PERSIST": "", "KEEP_TURN_IMAGES": "", "PREWARM": "",
|
||||
} {
|
||||
if got := facadeChatBlocker(map[string]string{k: "on"}); got != want {
|
||||
t.Errorf("%s : %q", k, got)
|
||||
}
|
||||
}
|
||||
if got := facadeChatBlocker(map[string]string{}); got != "" {
|
||||
t.Errorf("sans réglage : %q", got)
|
||||
}
|
||||
// Les motifs de relance personnalisés ne gardent plus la conversation.
|
||||
putStr(bkState, "retry_patterns", `["oups"]`)
|
||||
if got := facadeChatBlocker(map[string]string{}); got != "" {
|
||||
t.Errorf("retry_patterns : %q", got)
|
||||
if rec.Body.String() == "go" || len(f.recu()) != 1 {
|
||||
t.Fatalf("pas remis à la façade : %q %v", rec.Body.String(), f.recu())
|
||||
}
|
||||
putStr(bkState, "retry_patterns", "")
|
||||
// Sans LOKI_CHAT=rust : le Go, et pourquoi.
|
||||
oublierDecisionFacade(t)
|
||||
t.Setenv(facadeAddrEnv, "")
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/chat/holder", ""); !strings.Contains(w.Body.String(), "adresse de la façade") {
|
||||
t.Fatalf("sans adresse : %s", w.Body.String())
|
||||
}
|
||||
t.Setenv(facadeChatEnv, "")
|
||||
if w := appelOAIInterne(t, "GET", "/internal/v1/chat/holder", ""); !strings.Contains(w.Body.String(), "LOKI_CHAT absent") {
|
||||
t.Fatalf("sans la variable : %s", w.Body.String())
|
||||
@@ -1076,7 +1042,6 @@ func TestFacadeBackends(t *testing.T) {
|
||||
func TestFacadeSondeDeGabarit(t *testing.T) {
|
||||
testHome(t)
|
||||
freshTplProbe(t)
|
||||
oublierDecisionFacade(t)
|
||||
verdict := `{"ok":true,"result":{"preserves_history":"no","prefix_stable":"no","renders_reasoning":"yes","model":"m1.gguf","tools":2,"at":"2026-10-15T20:00:00.5Z"}}`
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get(internalTokenHeader) != jetonTest || r.URL.Path != "/internal/facade/v1/chat/tpl" {
|
||||
|
||||
Reference in new issue
Block a user