mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 04:10:04 +02:00
Quand la façade tient la mémoire (LOKI_MEM=rust), les outils mem_search, mem_read, mem_add, mem_edit et tracker de ses tours s'exécutent chez elle (src/memory/tools.rs, port de chat_memory.go et de l'outil de tracker.go) : mêmes résultats et diffs, mêmes lectures et écritures que les routes de la mémoire (scellées selon le chiffrement, .bak, index tenu par le code, seed au nom du projet). Recherche classée par couverture puis pertinence, extrait coupé à l'octet comme le Go ; trackers vus niveau par niveau. ChatShared porte la même mémoire que les routes ; tracker_add prend l'heure en paramètre (tracker_add_at). Côté Go, l'heure des trackers passe par trackerNow, que le journal fixe. Parité : journal de 57 appels tiré du Go (parity_mem_tools_test.go → tests/parity_mem/outils.json : résultats, diffs, fichiers de la mémoire et trackers), rejoué par tests/memory.rs ; un tour de la façade qui crée une page (tests/lot6.rs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
223 lines
12 KiB
Go
223 lines
12 KiB
Go
//go:build linux
|
|
|
|
package loki
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// parity_mem_tools_test.go — journal de référence des outils mémoire de
|
|
// l'agent (lot 40 de la migration Rust) : mem_search, mem_read, mem_add,
|
|
// mem_edit et tracker. Chaque appel (arguments tels que le modèle les
|
|
// envoie), son résultat et son diff, puis les fichiers de la mémoire du projet
|
|
// et les trackers après coup. Heure fixée, fuseau UTC. Rangé dans
|
|
// rust/loki-rs/tests/parity_mem/outils.json ; src/memory/tools/tests.rs le
|
|
// rejoue.
|
|
//
|
|
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteOutilsMemoire
|
|
|
|
const parityMemToolsFile = "../../rust/loki-rs/tests/parity_mem/outils.json"
|
|
|
|
type parityMemToolStep struct {
|
|
Name string `json:"name"`
|
|
Now string `json:"now,omitempty"`
|
|
Tool string `json:"tool"`
|
|
Args json.RawMessage `json:"args"`
|
|
Label string `json:"label"`
|
|
Out string `json:"out"`
|
|
Diff any `json:"diff"`
|
|
Added int `json:"added"`
|
|
Removed int `json:"removed"`
|
|
Files map[string]string `json:"files"`
|
|
Trackers map[string]any `json:"trackers"`
|
|
}
|
|
|
|
var parityTrackerIDRe = regexp.MustCompile(`\b[0-9]{18,20}\b`)
|
|
|
|
func TestPariteOutilsMemoire(t *testing.T) {
|
|
home := testHome(t)
|
|
oldLocal, oldNow := time.Local, trackerNow
|
|
time.Local = time.UTC
|
|
var now time.Time
|
|
trackerNow = func() time.Time { return now }
|
|
t.Cleanup(func() { time.Local, trackerNow = oldLocal, oldNow })
|
|
activeProjMu.Lock()
|
|
activeProjCache = ""
|
|
activeProjMu.Unlock()
|
|
|
|
ids := map[string]string{}
|
|
var order []string
|
|
mask := func(s string) string {
|
|
return parityTrackerIDRe.ReplaceAllStringFunc(s, func(id string) string {
|
|
if _, ok := ids[id]; !ok {
|
|
order = append(order, id)
|
|
ids[id] = "{{T" + strconv.Itoa(len(order)) + "}}"
|
|
}
|
|
return ids[id]
|
|
})
|
|
}
|
|
unmask := func(s string) string {
|
|
for id, m := range ids {
|
|
s = strings.ReplaceAll(s, m, id)
|
|
}
|
|
return s
|
|
}
|
|
dir := projectMemoryDir(defaultProjectSlug)
|
|
files := func() map[string]string {
|
|
out := map[string]string{}
|
|
_ = filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() {
|
|
return nil
|
|
}
|
|
b, _ := os.ReadFile(p)
|
|
rel, _ := filepath.Rel(dir, p)
|
|
out[rel] = string(b)
|
|
return nil
|
|
})
|
|
return out
|
|
}
|
|
trackers := func() map[string]any {
|
|
out := map[string]any{}
|
|
kv := allKV(bkTracker)
|
|
keys := make([]string, 0, len(kv))
|
|
for k := range kv {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
var v any
|
|
_ = json.Unmarshal([]byte(mask(string(kv[k]))), &v)
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
var steps []parityMemToolStep
|
|
run := func(st parityMemToolStep) {
|
|
if st.Now != "" {
|
|
n, err := time.Parse(time.RFC3339, st.Now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now = n
|
|
}
|
|
var args map[string]any
|
|
if err := json.Unmarshal([]byte(unmask(string(st.Args))), &args); err != nil {
|
|
t.Fatalf("%s : %v", st.Name, err)
|
|
}
|
|
if args == nil {
|
|
args = map[string]any{}
|
|
}
|
|
st.Label = toolCallLabel(st.Tool, args)
|
|
res := execTool(context.Background(), st.Tool, args, st.Label, Caps{Agent: true})
|
|
b, _ := json.Marshal(res.Result)
|
|
var out string
|
|
_ = json.Unmarshal(b, &out)
|
|
st.Out = mask(out)
|
|
db, _ := json.Marshal(res.Diff)
|
|
_ = json.Unmarshal(db, &st.Diff)
|
|
st.Added, st.Removed = res.Added, res.Removed
|
|
st.Files = files()
|
|
st.Trackers = trackers()
|
|
steps = append(steps, st)
|
|
}
|
|
a := func(s string) json.RawMessage { return json.RawMessage(s) }
|
|
|
|
// Pages.
|
|
run(parityMemToolStep{Name: "recherche dans une mémoire vide", Now: "2026-07-20T10:00:00Z", Tool: "mem_search", Args: a(`{"query":"docker"}`)})
|
|
run(parityMemToolStep{Name: "ajout", Tool: "mem_add", Args: a(`{"file":"docker-notes","content":"# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n\n\n"}`)})
|
|
run(parityMemToolStep{Name: "ajout d'une page existante", Tool: "mem_add", Args: a(`{"file":"docker-notes.md","content":"x"}`)})
|
|
run(parityMemToolStep{Name: "ajout d'un nom invalide", Tool: "mem_add", Args: a(`{"file":"../x","content":"x"}`)})
|
|
run(parityMemToolStep{Name: "ajout sans nom", Tool: "mem_add", Args: a(`{"content":"x"}`)})
|
|
run(parityMemToolStep{Name: "ajout en majuscules", Tool: "mem_add", Args: a(`{"file":"Recettes.MD","content":"# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte"}`)})
|
|
run(parityMemToolStep{Name: "ajout d'une page bavarde", Tool: "mem_add", Args: a(`{"file":"nathan","content":"## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019."}`)})
|
|
long := strings.Repeat("Un texte de remplissage pour éloigner le terme. ", 4) + "Le mot RARE est là, " + strings.Repeat("puis encore du texte après lui, assez pour couper. ", 4)
|
|
lb, _ := json.Marshal(map[string]string{"file": "longue", "content": "# Longue\n" + long})
|
|
run(parityMemToolStep{Name: "ajout d'une longue page", Tool: "mem_add", Args: lb})
|
|
run(parityMemToolStep{Name: "couverture d'abord", Tool: "mem_search", Args: a(`{"query":"copine Nathan"}`)})
|
|
run(parityMemToolStep{Name: "rareté et nom", Tool: "mem_search", Args: a(`{"query":"Nathan nathan TARTE"}`)})
|
|
run(parityMemToolStep{Name: "requête vide : tout", Tool: "mem_search", Args: a(`{"query":" "}`)})
|
|
run(parityMemToolStep{Name: "limite", Tool: "mem_search", Args: a(`{"query":"e","limit":2}`)})
|
|
run(parityMemToolStep{Name: "limite hors bornes", Tool: "mem_search", Args: a(`{"query":"e","limit":99}`)})
|
|
run(parityMemToolStep{Name: "extrait coupé des deux côtés", Tool: "mem_search", Args: a(`{"query":"rare"}`)})
|
|
run(parityMemToolStep{Name: "rien", Tool: "mem_search", Args: a(`{"query":"zzz"}`)})
|
|
run(parityMemToolStep{Name: "lecture", Tool: "mem_read", Args: a(`{"file":"docker-notes"}`)})
|
|
run(parityMemToolStep{Name: "lecture d'une plage", Tool: "mem_read", Args: a(`{"file":"docker-notes.md","offset":3,"limit":1}`)})
|
|
run(parityMemToolStep{Name: "lecture au-delà", Tool: "mem_read", Args: a(`{"file":"docker-notes","offset":40}`)})
|
|
run(parityMemToolStep{Name: "lecture d'une page absente", Tool: "mem_read", Args: a(`{"file":"absente"}`)})
|
|
run(parityMemToolStep{Name: "lecture d'un nom invalide", Tool: "mem_read", Args: a(`{"file":"a/b"}`)})
|
|
run(parityMemToolStep{Name: "modification", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"Le port 8080 est pris.","new":"Le port 8080 est pris.\nLe port 9090 aussi."}`)})
|
|
run(parityMemToolStep{Name: "modification rejouée", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"Le port 8080 est pris.\nLe port 9090 aussi.","new":"Le port 8080 est pris.\nLe port 9090 aussi."}`)})
|
|
run(parityMemToolStep{Name: "modification déjà en place", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"compose v1","new":"Utiliser compose v2."}`)})
|
|
run(parityMemToolStep{Name: "ancien texte absent", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"introuvable","new":"x"}`)})
|
|
run(parityMemToolStep{Name: "ancien texte en double", Tool: "mem_edit", Args: a(`{"file":"nathan","old":"Nathan Nathan","new":"N"}`)})
|
|
run(parityMemToolStep{Name: "ancien texte vide", Tool: "mem_edit", Args: a(`{"file":"nathan","old":"","new":"N"}`)})
|
|
run(parityMemToolStep{Name: "modification d'une page absente", Tool: "mem_edit", Args: a(`{"file":"absente","old":"a","new":"b"}`)})
|
|
run(parityMemToolStep{Name: "suppression d'un passage", Tool: "mem_edit", Args: a(`{"file":"recettes","old":"\nTarte aux pommes","new":""}`)})
|
|
|
|
// Trackers.
|
|
run(parityMemToolStep{Name: "aucun tracker", Tool: "tracker", Args: a(`{}`)})
|
|
run(parityMemToolStep{Name: "ajout sans nom", Tool: "tracker", Args: a(`{"action":"add","text":"1"}`)})
|
|
run(parityMemToolStep{Name: "ajout sans texte", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","text":" "}`)})
|
|
run(parityMemToolStep{Name: "date illisible", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","when":"hier","text":"72"}`)})
|
|
run(parityMemToolStep{Name: "ajout daté", Tool: "tracker", Args: a(`{"action":"ADD","name":"Poids","when":"2026-07-15","text":" 72.5 "}`)})
|
|
run(parityMemToolStep{Name: "ajout à l'heure", Tool: "tracker", Args: a(`{"action":"add","name":"poids","when":"2026-07-16T08:30","text":"72.1"}`)})
|
|
run(parityMemToolStep{Name: "ajout l'an passé", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","when":"2025-12","text":"73"}`)})
|
|
run(parityMemToolStep{Name: "ajout maintenant", Now: "2026-07-20T10:15:00Z", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","text":"71.9"}`)})
|
|
run(parityMemToolStep{Name: "un autre tracker", Tool: "tracker", Args: a(`{"action":"add","name":"Séances de sport","when":"2024","text":"course"}`)})
|
|
run(parityMemToolStep{Name: "un nom en nombre", Tool: "tracker", Args: a(`{"action":"add","name":42,"when":"2026-01-02 03:04:05","text":7}`)})
|
|
run(parityMemToolStep{Name: "vue d'ensemble", Tool: "tracker", Args: a(`{"action":"view"}`)})
|
|
run(parityMemToolStep{Name: "squelette", Tool: "tracker", Args: a(`{"name":"Poids"}`)})
|
|
run(parityMemToolStep{Name: "une année", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026"}`)})
|
|
run(parityMemToolStep{Name: "une année vide", Tool: "tracker", Args: a(`{"name":"Poids","when":"2027"}`)})
|
|
run(parityMemToolStep{Name: "un mois", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026-07"}`)})
|
|
run(parityMemToolStep{Name: "un jour à l'heure", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026-07-16T08"}`)})
|
|
run(parityMemToolStep{Name: "tracker inconnu", Tool: "tracker", Args: a(`{"name":"Tension"}`)})
|
|
run(parityMemToolStep{Name: "modifier sans id", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","text":"1"}`)})
|
|
run(parityMemToolStep{Name: "modifier", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","id":"{{T1}}","text":"72.4","when":"2026-07-14"}`)})
|
|
run(parityMemToolStep{Name: "modifier une date illisible", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","id":"{{T1}}","when":"demain"}`)})
|
|
run(parityMemToolStep{Name: "modifier un point inconnu", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","id":"1"}`)})
|
|
run(parityMemToolStep{Name: "modifier un tracker inconnu", Tool: "tracker", Args: a(`{"action":"edit","name":"Tension","id":"1"}`)})
|
|
run(parityMemToolStep{Name: "supprimer sans id", Tool: "tracker", Args: a(`{"action":"delete","name":"Poids"}`)})
|
|
run(parityMemToolStep{Name: "supprimer sans nom", Tool: "tracker", Args: a(`{"action":"delete"}`)})
|
|
run(parityMemToolStep{Name: "supprimer", Tool: "tracker", Args: a(`{"action":"delete","name":"Poids","id":"{{T2}}"}`)})
|
|
run(parityMemToolStep{Name: "supprimer deux fois", Tool: "tracker", Args: a(`{"action":"delete","name":"Poids","id":"{{T2}}"}`)})
|
|
run(parityMemToolStep{Name: "action inconnue", Tool: "tracker", Args: a(`{"action":"purge","name":"Poids"}`)})
|
|
run(parityMemToolStep{Name: "après modifications", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026"}`)})
|
|
|
|
got, err := json.MarshalIndent(steps, "", " ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = append(got, '\n')
|
|
if strings.Contains(string(got), home) {
|
|
t.Fatal("le journal contient le dossier du test")
|
|
}
|
|
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
|
if err := os.MkdirAll(filepath.Dir(parityMemToolsFile), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(parityMemToolsFile, got, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
want, err := os.ReadFile(parityMemToolsFile)
|
|
if err != nil {
|
|
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
|
}
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("les outils mémoire ne produisent plus le journal rangé dans %s.\n"+
|
|
"LOKI_PARITY_UPDATE=1 pour le régénérer si le changement est voulu.", parityMemToolsFile)
|
|
}
|
|
}
|