mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +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
164 lines
6.0 KiB
Go
164 lines
6.0 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func turnJSON(t *testing.T, path, body string, want int) map[string]any {
|
|
t.Helper()
|
|
w := appelOAIInterne(t, "POST", path, body)
|
|
if w.Code != want {
|
|
t.Fatalf("%s : %d %s", path, w.Code, w.Body.String())
|
|
}
|
|
var m map[string]any
|
|
if w.Body.Len() > 0 {
|
|
if err := json.Unmarshal(w.Body.Bytes(), &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Début d'un tour : message vu par le modèle, modèle, capacités du tour,
|
|
// puce « mode Code » une seule fois par discussion.
|
|
func TestTourInterneDebut(t *testing.T) {
|
|
testHome(t)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
turnJSON(t, "/internal/v1/turn/start", `{"message":" "}`, 400)
|
|
if err := SetConfigKey("MODEL", "/data/models/Qwen3-8B.gguf"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := turnJSON(t, "/internal/v1/turn/start", `{"message":"salut"}`, 200)
|
|
if got["content"] != "salut" || got["model"] != "Qwen3-8B" || got["code_hint"] != false {
|
|
t.Fatalf("début : %v", got)
|
|
}
|
|
if files, _ := got["files"].([]any); files == nil || len(files) != 0 {
|
|
t.Fatalf("files : %v", got["files"])
|
|
}
|
|
if c, _ := got["caps"].(map[string]any); c["Agent"] != false {
|
|
t.Fatalf("caps sans agent : %v", c)
|
|
}
|
|
|
|
// Agent actif : un message qui ressemble à du code fait apparaître la puce,
|
|
// une seule fois.
|
|
if err := setAgentEnabled(true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = turnJSON(t, "/internal/v1/turn/start", "{\"message\":\"corrige main.go\"}", 200)
|
|
if got["code_hint"] != true {
|
|
t.Fatalf("puce attendue : %v", got)
|
|
}
|
|
if got = turnJSON(t, "/internal/v1/turn/start", "{\"message\":\"corrige main.go\"}", 200); got["code_hint"] != false {
|
|
t.Fatalf("puce montrée deux fois : %v", got)
|
|
}
|
|
|
|
// Mode Code : rôle du tour figé (plan demandé → planner).
|
|
if err := setConvMode(convEnsureActive(), "code"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = turnJSON(t, "/internal/v1/turn/start", `{"message":"fais un plan"}`, 200)
|
|
tc, _ := got["turn_caps"].(map[string]any)
|
|
c, _ := got["caps"].(map[string]any)
|
|
if tc["Code"] != true || tc["Role"] == "" || c["Role"] != "" {
|
|
t.Fatalf("rôle du tour : caps=%v turn_caps=%v", c, tc)
|
|
}
|
|
}
|
|
|
|
// Vue d'un tour : rien du projet sans agent ; tête et préambule avec ; refus
|
|
// sous PROJ_SNAPSHOT.
|
|
func TestTourInterneVue(t *testing.T) {
|
|
testHome(t)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
got := turnJSON(t, "/internal/v1/turn/view", `{"caps":{"Agent":false,"Mem":"off"}}`, 200)
|
|
if h, _ := got["head"].([]any); len(h) != 0 {
|
|
t.Fatalf("sans agent, rien du projet : %v", got["head"])
|
|
}
|
|
off := Caps{Mem: MemOff}
|
|
if got["prefix"] != injectPrefix(off, EnabledTools(off)) {
|
|
t.Fatalf("préambule sans agent : %v", got["prefix"])
|
|
}
|
|
if err := saveSysPrompt("Réponds en vers."); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = turnJSON(t, "/internal/v1/turn/view", `{"caps":{"Agent":true,"Mem":"off"}}`, 200)
|
|
tools, _ := got["tools"].([]any)
|
|
prefix, _ := got["prefix"].(string)
|
|
head, _ := got["head"].([]any)
|
|
if len(tools) == 0 || prefix == "" || len(head) == 0 {
|
|
t.Fatalf("agent : %d outils, préambule %q, tête %v", len(tools), prefix, head)
|
|
}
|
|
if h0, _ := head[0].(map[string]any); h0["content"] != "Réponds en vers." {
|
|
t.Fatalf("prompt du preset en tête : %v", head[0])
|
|
}
|
|
// Même préambule que celui que pose InjectSkills.
|
|
var caps Caps
|
|
_ = json.Unmarshal([]byte(`{"Agent":true,"Mem":"off"}`), &caps)
|
|
if want := injectPrefix(caps, EnabledTools(caps)); prefix != want {
|
|
t.Fatal("préambule différent de celui d'InjectSkills")
|
|
}
|
|
if err := SetConfigKey("PROJ_SNAPSHOT", "on"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Avec la clé : le bloc figé et la décision (facade_projsnap.go).
|
|
if got := turnJSON(t, "/internal/v1/turn/view", `{"caps":{"Agent":true}}`, 200); got["projsnap"] == nil {
|
|
t.Fatalf("PROJ_SNAPSHOT sans décision : %v", got)
|
|
}
|
|
turnJSON(t, "/internal/v1/turn/view", `{"caps":{"Agent":false,"Mem":"off"}}`, 200)
|
|
}
|
|
|
|
// État par discussion : l'active suit la façade ; une discussion supprimée
|
|
// emporte son mode ; fin de tour sans abonnés push.
|
|
func TestTourInterneDiscussions(t *testing.T) {
|
|
testHome(t)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
turnJSON(t, "/internal/v1/conv/active", `{"id":"c99"}`, 204)
|
|
if got := convActiveCached(); got != "c99" {
|
|
t.Fatalf("active en cache : %q", got)
|
|
}
|
|
turnJSON(t, "/internal/v1/conv/active", `{"id":" "}`, 400)
|
|
if err := setConvMode("c99", "code"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
turnJSON(t, "/internal/v1/conv/dropped", `{"id":"c99"}`, 204)
|
|
if getStr(bkChat, modeKey("c99")) != "" {
|
|
t.Fatal("mode de la discussion supprimée resté en base")
|
|
}
|
|
turnJSON(t, "/internal/v1/turn/done", `{"ms":1200}`, 204)
|
|
if w := appelOAIInterne(t, "POST", "/internal/v1/conv/dropped", "{"); w.Code != 400 || strings.Count(w.Body.String(), "error") != 1 {
|
|
t.Fatalf("corps illisible : %d %q", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// La passe de vérification de la façade reçoit exactement le contexte isolé
|
|
// de runVerifyPass : prompt du rôle et faits de la machine, outils fermés,
|
|
// rôle verifier (seul autorisé à marquer un critère « passed »).
|
|
func TestTourInterneVerificateur(t *testing.T) {
|
|
testHome(t)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
got := turnJSON(t, "/internal/v1/turn/verifier", `{"caps":{"Agent":true,"Code":true,"Mem":"off","Role":"builder"}}`, 200)
|
|
vcaps := Caps{Agent: true, Code: true, Mem: MemOff, Role: "verifier"}
|
|
if got["system"] != rolePrompt("verifier")+"\n\n"+machineSystemPrompt(vcaps) {
|
|
t.Fatalf("système : %v", got["system"])
|
|
}
|
|
caps, _ := got["caps"].(map[string]any)
|
|
if caps["Role"] != "verifier" || caps["Code"] != true {
|
|
t.Fatalf("caps : %v", caps)
|
|
}
|
|
tools, _ := got["tools"].([]any)
|
|
var names []string
|
|
for _, tl := range tools {
|
|
f, _ := tl.(map[string]any)["function"].(map[string]any)
|
|
names = append(names, f["name"].(string))
|
|
}
|
|
var want []string
|
|
for _, tl := range EnabledTools(vcaps) {
|
|
want = append(want, tl.Function.Name)
|
|
}
|
|
if strings.Join(names, ",") != strings.Join(want, ",") || !strings.Contains(strings.Join(names, ","), "criteria") {
|
|
t.Fatalf("outils : %v, attendu %v", names, want)
|
|
}
|
|
turnJSON(t, "/internal/v1/turn/verifier", `{`, 400)
|
|
}
|