mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
La façade sert l'éditeur des pages mémoire (lecture, enregistrement, renommage, suppression, déplacement vers un autre projet, index MEMORY.md tenu par le code), le panneau des trackers, le mode mémoire du projet actif et les interrupteurs du mode agent, quand LOKI_MEM=rust (expérimental ; le Go reste le défaut). Les pages restent des fichiers memory/<projet>/*.md écrits de façon sûre (.bak, temporaire synchronisé puis renommé, relecture vérifiée), les trackers des valeurs du bucket tracker. Le chiffrement reste au Go : la façade fait ouvrir et sceller ses contenus par /internal/v1/crypt/*, et n'écrit jamais en clair quand la mémoire est verrouillée. gojson.rs reproduit le décodage d'encoding/json (première valeur, EOF, erreurs du scanner et de type, structs anonymes) pour renvoyer au client les mêmes messages que le Go ; il servira aux lots suivants. Côté Go : facade_mem.go remet ces routes à la façade quand elles arrivent par le tunnel ; coffre, instantanés, sauvegarde et projets restent au Go. Parité : parity_gojson_test.go (81 corps) → tests/gojson.rs ; parity_mem_test.go (noms, titres, slugs, modes, 36 dates, 102 appels de route avec le disque et la base après chacun) → tests/memory.rs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
443 lines
19 KiB
Go
443 lines
19 KiB
Go
package loki
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io/fs"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// parity_mem_test.go — journal de référence de la mémoire (lot 13 de la
|
||
// migration Rust) : noms et titres de pages, slugs, modes mémoire, dates des
|
||
// trackers, puis une suite d'appels aux routes (éditeur des pages, index
|
||
// MEMORY.md, déplacement entre projets, mode agent, mode mémoire, trackers)
|
||
// avec, après chacun, le contenu du dossier memory/ et des clés touchées en
|
||
// base. Rangé dans rust/loki-rs/tests/parity_mem/mem.json ; tests/memory.rs le
|
||
// rejoue par la façade.
|
||
//
|
||
// Les dates sont lues dans un fuseau fixe (UTC+2) ; les identifiants de points
|
||
// (horodatages en ns) deviennent {{ID1}}, {{ID2}}… dans l'ordre d'apparition,
|
||
// et les instants « maintenant » {{NOW}}.
|
||
//
|
||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteMemoire
|
||
|
||
const parityMemFile = "../../rust/loki-rs/tests/parity_mem/mem.json"
|
||
|
||
type parityMemStep struct {
|
||
// Avant l'appel : fichiers sous memory/ et valeurs en base (« bucket/clé »)
|
||
// posés ; une valeur vide supprime.
|
||
Files map[string]string `json:"files,omitempty"`
|
||
Store map[string]string `json:"store,omitempty"`
|
||
Method string `json:"method"`
|
||
Path string `json:"path"`
|
||
Body string `json:"body,omitempty"`
|
||
Status int `json:"status"`
|
||
Out any `json:"out"`
|
||
// Après l'appel.
|
||
Disk map[string]string `json:"disk"`
|
||
State any `json:"state"`
|
||
}
|
||
|
||
type parityMemWhen struct {
|
||
In string `json:"in"`
|
||
TS any `json:"ts,omitempty"`
|
||
DateOnly bool `json:"date_only,omitempty"`
|
||
Err string `json:"err,omitempty"`
|
||
}
|
||
|
||
type parityMemFixture struct {
|
||
Names []parityNameCase `json:"names"`
|
||
Titles map[string]string `json:"titles"`
|
||
Slugs map[string]string `json:"slugs"`
|
||
Modes map[string]string `json:"modes"`
|
||
When []parityMemWhen `json:"when"`
|
||
Seed map[string]string `json:"seed"`
|
||
Steps []parityMemStep `json:"steps"`
|
||
}
|
||
|
||
type parityNameCase struct {
|
||
In string `json:"in"`
|
||
Out string `json:"out,omitempty"`
|
||
Err string `json:"err,omitempty"`
|
||
}
|
||
|
||
var (
|
||
parityIDRe = regexp.MustCompile(`"id":"(\d{15,})"`)
|
||
parityNowRe = regexp.MustCompile(`"(ts|first_ts|last_ts)":(\d{12,14})`)
|
||
)
|
||
|
||
// parityMasker remplace identifiants et instants « maintenant » d'un JSON
|
||
// canonique (clés triées), dans l'ordre d'apparition.
|
||
type parityMasker struct {
|
||
ids map[string]string
|
||
}
|
||
|
||
func (m *parityMasker) mask(s string) string {
|
||
s = parityIDRe.ReplaceAllStringFunc(s, func(x string) string {
|
||
id := parityIDRe.FindStringSubmatch(x)[1]
|
||
ph, ok := m.ids[id]
|
||
if !ok {
|
||
ph = "{{ID" + strconv.Itoa(len(m.ids)+1) + "}}"
|
||
m.ids[id] = ph
|
||
}
|
||
return `"id":"` + ph + `"`
|
||
})
|
||
now := time.Now().UnixMilli()
|
||
return parityNowRe.ReplaceAllStringFunc(s, func(x string) string {
|
||
sm := parityNowRe.FindStringSubmatch(x)
|
||
v, _ := strconv.ParseInt(sm[2], 10, 64)
|
||
if d := v - now; d > -86400000 && d < 86400000 {
|
||
return `"` + sm[1] + `":"{{NOW}}"`
|
||
}
|
||
return x
|
||
})
|
||
}
|
||
|
||
// unmask : les {{IDn}} d'un corps de requête rendus à leur valeur.
|
||
func (m *parityMasker) unmask(s string) string {
|
||
for id, ph := range m.ids {
|
||
s = strings.ReplaceAll(s, ph, id)
|
||
}
|
||
return s
|
||
}
|
||
|
||
// canon : JSON aux clés triées, masqué, relu.
|
||
func (m *parityMasker) canon(t *testing.T, raw []byte) any {
|
||
var v any
|
||
if err := json.Unmarshal(raw, &v); err != nil {
|
||
t.Fatalf("JSON illisible : %v (%s)", err, raw)
|
||
}
|
||
b, _ := json.Marshal(v)
|
||
var out any
|
||
if err := json.Unmarshal([]byte(m.mask(string(b))), &out); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func TestPariteMemoire(t *testing.T) {
|
||
home := testHome(t)
|
||
old := time.Local
|
||
time.Local = time.FixedZone("essai", 7200)
|
||
t.Cleanup(func() { time.Local = old })
|
||
|
||
var fx parityMemFixture
|
||
for _, in := range []string{"", " ", "notes", "notes.md", "NOTES.MD", "a b", "../x", "x/y", "é", "a.b-c_d",
|
||
".md", ".", "..", "x.md.md", " pad ", "tab\t", "MEMORY", "memory.md", "x.Md"} {
|
||
c := parityNameCase{In: in}
|
||
if out, err := memFileName(in); err != nil {
|
||
c.Err = err.Error()
|
||
} else {
|
||
c.Out = out
|
||
}
|
||
fx.Names = append(fx.Names, c)
|
||
}
|
||
fx.Titles = map[string]string{}
|
||
for _, in := range []string{"", "\n\n", "# Titre", "## Titre ##", " ### ", "\n \n# A\n# B", "sans dièse\nsuite",
|
||
"#\n#\nVrai titre", "\t# tab", "# espace insécable ", "# # double"} {
|
||
fx.Titles[in] = titleOf(in)
|
||
}
|
||
fx.Slugs = map[string]string{}
|
||
for _, in := range []string{"Réseau NAS", " Hello World ", "àâäáã éèêë", "İstanbul", "Ωmega", strings.Repeat("ab", 30),
|
||
"---x---", "", "Ça_va?", "ÉCOLE", "Straße", strings.Repeat("a", 39) + "-bcd", "!!!", "Perso & Cie", "ÎLE ÔÔ ÙÛ Ñ Ç"} {
|
||
fx.Slugs[in] = slugifyIdent(in)
|
||
}
|
||
fx.Modes = map[string]string{}
|
||
for _, in := range []string{"", "off", " OFF ", "ondemand", "on-demand", "search", "always", "auto", "bizarre"} {
|
||
fx.Modes[in] = string(normalizeMemMode(in))
|
||
}
|
||
mk := &parityMasker{ids: map[string]string{}}
|
||
for _, in := range []string{"", "now", "NOW", " Maintenant ", "2024-03-10", "2024-03-10T08:15", "2024-03-10 7:05",
|
||
"2024-03-10 07:5", "2024-03-10 07:05:09", "2024-03-10 07:05:09.5", "2024-03-10 07:05:09,123456789999",
|
||
"2024-03-10 24:00", "2024-03-10 23:60", "2024-13-01", "2024-02-29", "2023-02-29", "2024-04-31", "2024-03",
|
||
"2024", "24", "2024-3", "2024-03-1", " 2024-03-10 ", "2024-03-10 07:05", "demain", "2024-03-10T07:05Z",
|
||
"today", "+024", "0000-01-01", "9999-12-31 23:59:59", "1234", "2024-03-10 07", "2024-03-10 7",
|
||
"x\x01y", "2024-03-10T", "2024-03-10 07:05:60"} {
|
||
c := parityMemWhen{In: in}
|
||
ts, dateOnly, err := parseWhen(in)
|
||
if err != nil {
|
||
c.Err = err.Error()
|
||
} else {
|
||
c.TS = ts
|
||
if d := ts - time.Now().UnixMilli(); d > -86400000 && d < 86400000 {
|
||
c.TS = "{{NOW}}"
|
||
}
|
||
c.DateOnly = dateOnly
|
||
}
|
||
fx.When = append(fx.When, c)
|
||
}
|
||
|
||
// Base de départ : deux projets, le premier actif, mode mémoire global.
|
||
fx.Seed = map[string]string{
|
||
"state/projects": `[{"slug":"generale","name":"Générale","created_at":1000},{"slug":"perso","name":"Perso & Cie","created_at":2000,"desc":"à moi"}]`,
|
||
"state/active_project": "generale",
|
||
"config/MEM_MODE": "ondemand",
|
||
}
|
||
apply := func(store map[string]string) {
|
||
keys := make([]string, 0, len(store))
|
||
for k := range store {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
for _, k := range keys {
|
||
b, key, _ := strings.Cut(k, "/")
|
||
if b == bkState && key == stActiveProjectKey {
|
||
if store[k] == "" {
|
||
putStr(bkState, key, "")
|
||
activeProjMu.Lock()
|
||
activeProjCache = ""
|
||
activeProjMu.Unlock()
|
||
} else if err := setActiveProject(store[k]); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
continue
|
||
}
|
||
if err := putStr(b, key, store[k]); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
}
|
||
apply(fx.Seed)
|
||
|
||
memRoot := filepath.Join(home, "memory")
|
||
disk := func() map[string]string {
|
||
out := map[string]string{}
|
||
_ = filepath.WalkDir(memRoot, func(p string, d fs.DirEntry, err error) error {
|
||
if err != nil || d.IsDir() {
|
||
return nil
|
||
}
|
||
rel, _ := filepath.Rel(memRoot, p)
|
||
b, _ := os.ReadFile(p)
|
||
out[filepath.ToSlash(rel)] = string(b)
|
||
return nil
|
||
})
|
||
return out
|
||
}
|
||
state := func() any {
|
||
st := map[string]any{
|
||
"agent": getStr(bkState, "agent"),
|
||
"active": getStr(bkState, stActiveProjectKey),
|
||
"compact": ReadConfig()["COMPACT"],
|
||
"mem_mode": ReadConfig()["MEM_MODE"],
|
||
"projects": nil,
|
||
"trackers": map[string]any{},
|
||
"tracker_raw": map[string]string{},
|
||
}
|
||
if b := getBytes(bkState, stProjectsKey); len(b) > 0 {
|
||
var v any
|
||
if json.Unmarshal(b, &v) == nil {
|
||
st["projects"] = v
|
||
}
|
||
}
|
||
for k, v := range allKV(bkTracker) {
|
||
var x any
|
||
if json.Unmarshal([]byte(v), &x) == nil {
|
||
st["trackers"].(map[string]any)[k] = x
|
||
} else {
|
||
st["tracker_raw"].(map[string]string)[k] = v
|
||
}
|
||
}
|
||
b, _ := json.Marshal(st)
|
||
return mk.canon(t, b)
|
||
}
|
||
|
||
run := func(st parityMemStep) parityMemStep {
|
||
for rel, content := range st.Files {
|
||
p := filepath.Join(memRoot, filepath.FromSlash(rel))
|
||
if content == "" {
|
||
_ = os.RemoveAll(p)
|
||
continue
|
||
}
|
||
if strings.HasSuffix(rel, "/") {
|
||
_ = os.MkdirAll(p, 0o755)
|
||
continue
|
||
}
|
||
_ = os.MkdirAll(filepath.Dir(p), 0o755)
|
||
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
apply(st.Store)
|
||
w := httptest.NewRecorder()
|
||
r := httptest.NewRequest(st.Method, mk.unmask(st.Path), strings.NewReader(mk.unmask(st.Body)))
|
||
switch r.URL.Path {
|
||
case "/api/mem":
|
||
handleMem(w, r)
|
||
case "/api/mem/save":
|
||
handleMemSave(w, r)
|
||
case "/api/mem/delete":
|
||
handleMemDelete(w, r)
|
||
case "/api/mem/move":
|
||
handleMemMove(w, r)
|
||
case "/api/agent", "/api/tools", "/api/skills":
|
||
handleAgent(w, r)
|
||
case "/api/agent/toggle", "/api/tools/toggle", "/api/skills/toggle":
|
||
handleAgentToggle(w, r)
|
||
case "/api/agent/compact":
|
||
handleCompactToggle(w, r)
|
||
case "/api/memory":
|
||
handleMemoryMode(w, r)
|
||
case "/api/tracker":
|
||
handleTracker(w, r)
|
||
default:
|
||
t.Fatalf("route inattendue %s", st.Path)
|
||
}
|
||
st.Status = w.Code
|
||
st.Out = mk.canon(t, w.Body.Bytes())
|
||
st.Disk = disk()
|
||
st.State = state()
|
||
return st
|
||
}
|
||
|
||
for _, st := range []parityMemStep{
|
||
// --- pages ---
|
||
{Method: "GET", Path: "/api/mem"},
|
||
{Method: "GET", Path: "/api/mem?name=absente"},
|
||
{Method: "GET", Path: "/api/agent"},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"courses","content":"# Courses\n\nlait\n\n\n"}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"notes.md","content":"Sans titre"}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"../evil","content":"x"}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":" ","content":"x"}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `pas du json`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":5}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `[1]`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: ``},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"courses-v2","old":"courses","content":"# Courses v2\nœufs & <pain>"}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"notes","old":"notes","content":"## Titre\nmaj"}`},
|
||
{Method: "GET", Path: "/api/mem?name=courses-v2"},
|
||
{Method: "GET", Path: "/api/mem?name=+courses-v2.md+"},
|
||
{Method: "GET", Path: "/api/mem?name=a&name=courses-v2"},
|
||
{Method: "GET", Path: "/api/mem?name=../x"},
|
||
{Files: map[string]string{"generale/Zeta.MD": "# Zêta\n", "generale/.cache.md": "caché", "generale/dossier.md/": "-",
|
||
"generale/notes.txt": "pas une page", "generale/vide.md": "\n\n"},
|
||
Method: "GET", Path: "/api/tools"},
|
||
{Method: "POST", Path: "/api/mem/delete", Body: `{"name":"absente"}`},
|
||
{Method: "POST", Path: "/api/mem/delete", Body: `{"name":"bad/name"}`},
|
||
{Method: "POST", Path: "/api/mem/delete", Body: `{"name":"vide"}`},
|
||
{Method: "POST", Path: "/api/mem/delete", Body: `{nope}`},
|
||
// Accroche écrite à la main : un réenregistrement du même nom la perd
|
||
// (ligne retirée puis rajoutée à la fin), comme le Go.
|
||
{Files: map[string]string{"generale/MEMORY.md": "# Index mémoire — Générale\n\n- [Courses v2](courses-v2.md) — la liste\n- [Titre](notes.md)\n"},
|
||
Method: "POST", Path: "/api/mem/save", Body: `{"name":"courses-v2","old":"courses-v2","content":"# Courses v3"}`},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"neuve","content":"# Neuve"}`},
|
||
// --- déplacement ---
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"notes","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"notes","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"courses-v2","to":"generale"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"courses-v2","to":" generale "}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"courses-v2","to":"inconnu"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"MEMORY","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"x/y","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `[]`},
|
||
{Method: "GET", Path: "/api/mem/move"},
|
||
{Files: map[string]string{"generale/doublon.md": "# A", "perso/doublon.md": "# B"},
|
||
Method: "POST", Path: "/api/mem/move", Body: `{"name":"doublon","to":"perso"}`},
|
||
// Bascule vers le projet perso : ses pages, son nom.
|
||
{Store: map[string]string{"state/active_project": "perso"}, Method: "GET", Path: "/api/skills"},
|
||
{Method: "POST", Path: "/api/mem/save", Body: `{"name":"perso-1","content":"Première\nligne"}`},
|
||
{Method: "POST", Path: "/api/mem/move", Body: `{"name":"doublon","to":"generale"}`},
|
||
{Store: map[string]string{"state/active_project": "generale"}, Method: "GET", Path: "/api/agent"},
|
||
// --- mode agent, compactage ---
|
||
{Method: "POST", Path: "/api/agent/toggle", Body: `{"on":true}`},
|
||
{Method: "GET", Path: "/api/tools"},
|
||
{Method: "POST", Path: "/api/skills/toggle", Body: `{"on":"oui"}`},
|
||
{Method: "POST", Path: "/api/tools/toggle", Body: `{"on":true}`},
|
||
{Method: "POST", Path: "/api/agent/toggle", Body: `pas du json`},
|
||
{Method: "POST", Path: "/api/agent/compact", Body: `{"on":false}`},
|
||
{Method: "GET", Path: "/api/agent"},
|
||
{Method: "POST", Path: "/api/agent/compact", Body: `{}`},
|
||
{Store: map[string]string{"config/COMPACT": " NON "}, Method: "GET", Path: "/api/agent"},
|
||
{Method: "POST", Path: "/api/agent/compact", Body: `{"on":true}`},
|
||
// --- mode mémoire ---
|
||
{Method: "GET", Path: "/api/memory"},
|
||
{Method: "POST", Path: "/api/memory", Body: `{"mode":" Search "}`},
|
||
{Method: "GET", Path: "/api/agent"},
|
||
{Method: "POST", Path: "/api/memory", Body: `pas du json`},
|
||
{Method: "POST", Path: "/api/memory", Body: `{"mode":"off"}`},
|
||
{Method: "PUT", Path: "/api/memory", Body: `{"mode":"ondemand"}`},
|
||
{Store: map[string]string{"state/active_project": "fantome"}, Method: "POST", Path: "/api/memory", Body: `{"mode":"off"}`},
|
||
{Method: "GET", Path: "/api/memory"},
|
||
{Store: map[string]string{"state/active_project": "perso"}, Method: "POST", Path: "/api/memory", Body: `{"mode":"ondemand"}`},
|
||
{Store: map[string]string{"state/active_project": "generale"}, Method: "GET", Path: "/api/memory"},
|
||
// --- trackers ---
|
||
{Method: "GET", Path: "/api/tracker"},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"Poids","when":"2024-03-10","text":" 78,2 kg "}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"Poids","when":"2024-03-09 08:15","text":"78,9"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"poids","text":"77"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"ADD","name":"Réseau NAS","when":"2023","text":"installé <ok> & fini"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"","text":"x"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"x","text":" "}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"x","when":"demain","text":"y"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"!!!","when":"2025-01","text":"z"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"Même jour","when":"2025-01","text":"m"}`},
|
||
{Method: "GET", Path: "/api/tracker?slug=poids"},
|
||
{Method: "GET", Path: "/api/tracker?slug=absent"},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"edit","slug":"poids","id":"{{ID2}}","text":" 78,0 kg "}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"edit","slug":"poids","id":"{{ID2}}","when":"2024-03-11 07:00"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"edit","slug":"poids","id":"{{ID1}}","when":"hier","text":"perdu"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"edit","slug":"poids","id":"999","text":"x"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"edit","slug":"absent","id":"{{ID1}}","text":"x"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"delete","slug":"poids","id":"{{ID1}}"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"delete","slug":"poids","id":"{{ID1}}"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"reseau-nas","name":" Réseau NAS (bureau) "}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"poids","name":"POIDS"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"tracker","name":"Poids"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"tracker","name":" "}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"tracker","name":"???"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"absent","name":"Neuf"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"move","slug":"tracker","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"move","slug":"poids","to":"inconnu"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"move","slug":"poids","to":"generale"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"move","slug":"absent","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":"Même jour","when":"2025-02","text":"perso"}`,
|
||
Store: map[string]string{"state/active_project": "perso"}},
|
||
{Store: map[string]string{"state/active_project": "generale"},
|
||
Method: "POST", Path: "/api/tracker", Body: `{"action":"move","slug":"meme-jour","to":"perso"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"delete_tracker","slug":"poids"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"delete_tracker","slug":"absent"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"bidule"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `pas du json`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `"texte"`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"add","name":3}`},
|
||
{Method: "PUT", Path: "/api/tracker"},
|
||
{Store: map[string]string{"tracker/generale/casse": "pas du json"}, Method: "GET", Path: "/api/tracker"},
|
||
{Method: "GET", Path: "/api/tracker?slug=casse"},
|
||
{Store: map[string]string{"tracker/generale/vide": `{"name":"Vide","events":null}`}, Method: "GET", Path: "/api/tracker?slug=vide"},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"rename","slug":"vide","name":"VIDE"}`},
|
||
{Method: "POST", Path: "/api/tracker", Body: `{"action":"delete","slug":"vide","id":"1"}`},
|
||
{Method: "GET", Path: "/api/tracker"},
|
||
} {
|
||
fx.Steps = append(fx.Steps, run(st))
|
||
}
|
||
|
||
got, err := parityJSON(fx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||
if err := os.MkdirAll(filepath.Dir(parityMemFile), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(parityMemFile, got, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return
|
||
}
|
||
want, err := os.ReadFile(parityMemFile)
|
||
if err != nil {
|
||
t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err)
|
||
}
|
||
if string(got) != string(want) {
|
||
t.Errorf("la mémoire ne produit plus le journal rangé dans %s.\n"+
|
||
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteMemoire,\n"+
|
||
"puis cargo test --test memory dit si le port Rust suit.\n--- obtenu ---\n%s", parityMemFile, got)
|
||
}
|
||
}
|