mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 22:48:11 +02:00
Lot 18 : hub des projets tenu par la façade (LOKI_PROJECTS=rust)
La façade sert /api/projects quand LOKI_PROJECTS=rust (expérimental ; le Go reste le défaut) : liste avec discussions, pages et trackers comptés, création (slug translittéré et unique, index amorcé), renommage, description, suppression (discussions, trackers et mémoire du projet, actif réassigné, dernier projet indestructible) et bascule, refusées pendant une génération. Le projet actif passe toujours par le Go, qui en garde une copie en RAM. Les discussions sont à qui tient la conversation : la façade bascule et supprime elle-même quand elle la tient, sinon le Go garde /internal/v1/project/switch et /internal/v1/project/drop-convs (facade_projects.go). Parité : parity_projects_test.go tire du Go 40 appels sur une base amorcée (pages, discussions, trackers piégés), avec le registre, l'actif, memory/, les trackers et l'index après chacun, rejoués par tests/projects_api.rs contre un faux Go qui bascule et supprime comme le vrai ; bascule et suppression par la façade de bout en bout ; TestFacadeProjets côté Go. 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
+5707
-4
No files matched your search
@@ -145,7 +145,8 @@ var facadeChatRoutes = map[string]bool{
|
||||
// (facade_dictate.go), les réglages partagés (facade_settings.go), les fichiers
|
||||
// de la discussion (facade_files.go), les notifications (facade_push.go), la
|
||||
// mémoire (facade_mem.go), les presets (facade_presets.go), le pilotage du
|
||||
// moteur (facade_engine.go), les interrupteurs système (facade_system.go).
|
||||
// moteur (facade_engine.go), les interrupteurs système (facade_system.go), les
|
||||
// projets (facade_projects.go).
|
||||
// Posé APRÈS requireWebAuth : seule une requête authentifiée ici part, avec le
|
||||
// jeton interne qui en tient lieu là-bas.
|
||||
func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc {
|
||||
@@ -170,6 +171,8 @@ func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc {
|
||||
held, unavailable = facadeEngine, "pilotage du moteur indisponible : "
|
||||
case facadeSystemRoutes[path]:
|
||||
held, unavailable = facadeSystem, "réglages système indisponibles : "
|
||||
case facadeProjectsRoutes[path]:
|
||||
held, unavailable = facadeProjects, "projets indisponibles : "
|
||||
default:
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -758,3 +758,72 @@ func TestFacadeRelaisGardeHote(t *testing.T) {
|
||||
t.Fatalf("hôte %q, schéma %q", host, proto)
|
||||
}
|
||||
}
|
||||
|
||||
// Le hub des projets (LOKI_PROJECTS=rust, lot 18) part à la façade ; quand le
|
||||
// Go tient la conversation, la bascule et les discussions d'un projet
|
||||
// supprimé restent ici (API interne).
|
||||
func TestFacadeProjets(t *testing.T) {
|
||||
testHome(t)
|
||||
// Le projet actif est gardé en RAM d'un test à l'autre.
|
||||
activeProjMu.Lock()
|
||||
activeProjCache = ""
|
||||
activeProjMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
activeProjMu.Lock()
|
||||
activeProjCache = ""
|
||||
activeProjMu.Unlock()
|
||||
})
|
||||
ensureDefaultProject()
|
||||
f := demarrerFacade(t)
|
||||
t.Setenv(facadeChatEnv, "")
|
||||
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
|
||||
w := httptest.NewRecorder()
|
||||
withFacadeChat("/api/projects", local)(w, httptest.NewRequest("GET", "/api/projects", nil))
|
||||
if w.Body.String() != "go" {
|
||||
t.Fatalf("servi ailleurs sans LOKI_PROJECTS : %q", w.Body.String())
|
||||
}
|
||||
t.Setenv(facadeProjectsEnv, "rust")
|
||||
w = httptest.NewRecorder()
|
||||
withFacadeChat("/api/projects", local)(w, httptest.NewRequest("POST", "/api/projects", strings.NewReader("{}")))
|
||||
if w.Body.String() != f.reply || len(f.recu()) != 1 {
|
||||
t.Fatalf("relais : %q, %d appels", w.Body.String(), len(f.recu()))
|
||||
}
|
||||
|
||||
if _, err := createProject("Alpha"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
convIndexSave([]convMeta{{ID: "a1", Project: "alpha", Updated: 1}, {ID: "g1", Project: "generale", Updated: 2}})
|
||||
_ = putBytes(bkChat, convKey("a1"), []byte(`{"id":"a1"}`))
|
||||
_ = putBytes(bkChat, convKey("g1"), []byte(`{"id":"g1"}`))
|
||||
post := func(h http.HandlerFunc, body string) (int, string) {
|
||||
w := httptest.NewRecorder()
|
||||
h(w, httptest.NewRequest("POST", "/internal/v1/project/x", strings.NewReader(body)))
|
||||
return w.Code, strings.TrimSpace(w.Body.String())
|
||||
}
|
||||
if c, b := post(handleInternalProjectSwitch, `{"slug":"nope"}`); c != 400 || b != `{"error":"projet introuvable"}` {
|
||||
t.Fatalf("bascule inconnue : %d %s", c, b)
|
||||
}
|
||||
if c, _ := post(handleInternalProjectSwitch, `{"slug":" alpha "}`); c != 204 || activeProjectSlug() != "alpha" {
|
||||
t.Fatalf("bascule : %d, actif %q", c, activeProjectSlug())
|
||||
}
|
||||
conv.mu.Lock()
|
||||
conv.Generating = true
|
||||
conv.mu.Unlock()
|
||||
c1, _ := post(handleInternalProjectSwitch, `{"slug":"generale"}`)
|
||||
c2, _ := post(handleInternalProjectDropConvs, `{"slug":"alpha"}`)
|
||||
conv.mu.Lock()
|
||||
conv.Generating = false
|
||||
conv.mu.Unlock()
|
||||
if c1 != 409 || c2 != 409 {
|
||||
t.Fatalf("pendant une génération : %d, %d", c1, c2)
|
||||
}
|
||||
if c, b := post(handleInternalProjectDropConvs, `{"slug":"alpha"}`); c != 200 || b != `{"dropped":1}` {
|
||||
t.Fatalf("discussions supprimées : %d %s", c, b)
|
||||
}
|
||||
if n := len(convIndexForProject("alpha")); n != 0 || len(convIndexForProject("generale")) == 0 {
|
||||
t.Fatalf("index après : %d d'Alpha", n)
|
||||
}
|
||||
if c, _ := post(handleInternalProjectDropConvs, `nimp`); c != 400 {
|
||||
t.Fatalf("corps illisible : %d", c)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// facade_projects.go — hub des projets tenu par la façade Rust
|
||||
// (LOKI_PROJECTS=rust, lot 18 de la migration ; expérimental, le Go reste le
|
||||
// défaut) : liste avec ses comptes, création, renommage, description,
|
||||
// suppression, bascule (/api/projects).
|
||||
//
|
||||
// Le registre reste dans la base (state/projects) et la mémoire de chaque
|
||||
// projet dans memory/<slug>. Le projet actif passe toujours par le Go
|
||||
// (/internal/v1/project/activate), qui en garde une copie en RAM. Quand le Go
|
||||
// tient la conversation, ce qui la touche reste ici :
|
||||
//
|
||||
// - la bascule : discussion du projet quitté enregistrée, la plus récente
|
||||
// du projet rejoint ouverte (/internal/v1/project/switch) ;
|
||||
// - les discussions d'un projet supprimé (/internal/v1/project/drop-convs).
|
||||
//
|
||||
// Quand la façade tient la conversation, elle fait les deux elle-même.
|
||||
|
||||
const facadeProjectsEnv = "LOKI_PROJECTS"
|
||||
|
||||
// facadeProjects : la façade tient-elle le hub des projets ?
|
||||
func facadeProjects() bool {
|
||||
return strings.EqualFold(strings.TrimSpace(os.Getenv(facadeProjectsEnv)), "rust") &&
|
||||
facadeAddr() != "" && internalToken() != ""
|
||||
}
|
||||
|
||||
// facadeProjectsRoutes : les routes que la façade sert alors (projects_api.rs).
|
||||
var facadeProjectsRoutes = map[string]bool{
|
||||
"/api/projects": true,
|
||||
}
|
||||
|
||||
// handleInternalProjectSwitch : POST {slug} → 204, ou 400/409 + {error}.
|
||||
func handleInternalProjectSwitch(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
if conv.isGenerating() {
|
||||
sendJSON(w, http.StatusConflict, map[string]any{"error": "une génération est en cours"})
|
||||
return
|
||||
}
|
||||
if err := projectSwitch(strings.TrimSpace(in.Slug)); err != nil {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// handleInternalProjectDropConvs : POST {slug} → 200 {dropped} — supprime les
|
||||
// discussions du projet, comme deleteProject quand le Go tient la conversation.
|
||||
func handleInternalProjectDropConvs(w http.ResponseWriter, r *http.Request) {
|
||||
var in struct {
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
if !readInternalJSON(w, r, &in) {
|
||||
return
|
||||
}
|
||||
if conv.isGenerating() {
|
||||
sendJSON(w, http.StatusConflict, map[string]any{"error": "une génération est en cours"})
|
||||
return
|
||||
}
|
||||
n := 0
|
||||
for _, m := range convIndexForProject(strings.TrimSpace(in.Slug)) {
|
||||
if convDelete(m.ID) == nil {
|
||||
n++
|
||||
}
|
||||
}
|
||||
sendJSON(w, http.StatusOK, map[string]any{"dropped": n})
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
//go:build linux
|
||||
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// parity_projects_test.go — journal de référence du hub des projets (lot 18
|
||||
// de la migration Rust) : liste et comptes (discussions, pages, trackers),
|
||||
// création (slug translittéré, unique, tronqué), renommage, description,
|
||||
// bascule et suppression (discussions, trackers et mémoire du projet, actif
|
||||
// réassigné, dernier projet indestructible), refus pendant une génération,
|
||||
// erreurs JSON. Après chaque appel : le registre, le projet actif, l'arbre
|
||||
// memory/, les trackers et l'index des discussions. La conversation est tenue
|
||||
// par le Go (le Rust le rejoue contre le faux Go, qui fait de même).
|
||||
// Rangé dans rust/loki-rs/tests/parity_projects/projects.json ;
|
||||
// tests/projects_api.rs le rejoue par la façade.
|
||||
//
|
||||
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteProjets
|
||||
|
||||
const parityProjectsFile = "../../rust/loki-rs/tests/parity_projects/projects.json"
|
||||
|
||||
type parityProjectStep struct {
|
||||
Name string `json:"name"`
|
||||
Generating bool `json:"generating,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Status int `json:"status"`
|
||||
Out any `json:"out"`
|
||||
After map[string]any `json:"after"`
|
||||
}
|
||||
|
||||
// parityZeroTimes : les dates de création, propres à chaque exécution.
|
||||
func parityZeroTimes(v any) any {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
for k, x := range t {
|
||||
if k == "created" || k == "created_at" || k == "updated" {
|
||||
t[k] = 0
|
||||
} else {
|
||||
t[k] = parityZeroTimes(x)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for i := range t {
|
||||
t[i] = parityZeroTimes(t[i])
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func TestPariteProjets(t *testing.T) {
|
||||
home := testHome(t)
|
||||
ensureDefaultProject()
|
||||
// Le projet actif est gardé en RAM d'un test à l'autre.
|
||||
activeProjMu.Lock()
|
||||
activeProjCache = ""
|
||||
activeProjMu.Unlock()
|
||||
t.Cleanup(func() {
|
||||
activeProjMu.Lock()
|
||||
activeProjCache = ""
|
||||
activeProjMu.Unlock()
|
||||
})
|
||||
// Date de création fixe : la graine est rangée telle quelle.
|
||||
if err := saveProjects([]Project{{Slug: defaultProjectSlug, Name: defaultProjectName, CreatedAt: 1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Pages de Générale : trois comptées, le reste non.
|
||||
for _, f := range []string{"a.md", "b.MD", "z.md", ".cache.md", "notes.txt", "sous/page.md"} {
|
||||
p := filepath.Join(projectMemoryDir(defaultProjectSlug), f)
|
||||
_ = os.MkdirAll(filepath.Dir(p), 0o755)
|
||||
if err := os.WriteFile(p, []byte("# "+f+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// Discussions : deux de Générale (dont une d'avant les projets), trois
|
||||
// d'Alpha (créé plus loin), une d'un projet disparu.
|
||||
idx := []convMeta{
|
||||
{ID: "g1", Title: "g1", Created: 1, Updated: 10, Project: "generale"},
|
||||
{ID: "o1", Title: "o1", Created: 2, Updated: 20},
|
||||
{ID: "a1", Title: "a1", Created: 3, Updated: 30, Project: "alpha"},
|
||||
{ID: "a2", Title: "a2", Created: 4, Updated: 50, Project: "alpha"},
|
||||
{ID: "a3", Title: "a3", Created: 5, Updated: 40, Project: "alpha"},
|
||||
{ID: "x1", Title: "x1", Created: 6, Updated: 60, Project: "ailleurs"},
|
||||
}
|
||||
convIndexSave(idx)
|
||||
for _, m := range idx {
|
||||
_ = putBytes(bkChat, convKey(m.ID), []byte(`{"id":"`+m.ID+`","log":[]}`))
|
||||
}
|
||||
// La discussion ouverte, comme au démarrage du Go (convEnsureActive).
|
||||
_ = putStr(bkChat, ckActive, "g1")
|
||||
convActivate("g1", getBytes(bkChat, convKey("g1")))
|
||||
seen := map[string]bool{}
|
||||
for _, m := range idx {
|
||||
seen[m.ID] = true
|
||||
}
|
||||
for _, k := range []string{"generale/poids", "alpha/sommeil", "alpha/pas", "alphabet/x"} {
|
||||
_ = putStr(bkTracker, k, `{"name":"`+k+`","events":[]}`)
|
||||
}
|
||||
|
||||
snapshot := func() map[string]any {
|
||||
st := map[string]any{}
|
||||
var reg any
|
||||
_ = json.Unmarshal(getBytes(bkState, stProjectsKey), ®)
|
||||
st["projects"] = parityZeroTimes(reg)
|
||||
st["active"] = getStr(bkState, stActiveProjectKey)
|
||||
var tree []string
|
||||
_ = filepath.Walk(filepath.Join(home, "memory"), func(p string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(home, p)
|
||||
if fi.IsDir() {
|
||||
rel += "/"
|
||||
}
|
||||
tree = append(tree, rel)
|
||||
return nil
|
||||
})
|
||||
sort.Strings(tree)
|
||||
st["tree"] = tree
|
||||
var trk []string
|
||||
for k := range allKV(bkTracker) {
|
||||
trk = append(trk, k)
|
||||
}
|
||||
sort.Strings(trk)
|
||||
st["trackers"] = trk
|
||||
var convs []string
|
||||
for _, m := range convIndex() {
|
||||
id := m.ID
|
||||
if !seen[id] {
|
||||
id = "{{NEW}}" // discussion créée par la bascule, identifiant tiré au hasard
|
||||
}
|
||||
convs = append(convs, id+":"+m.Project)
|
||||
}
|
||||
sort.Strings(convs)
|
||||
st["convs"] = convs
|
||||
return st
|
||||
}
|
||||
seed := snapshot()
|
||||
seedMem := map[string]string{}
|
||||
_ = filepath.Walk(filepath.Join(home, "memory"), func(p string, fi os.FileInfo, err error) error {
|
||||
if err == nil && !fi.IsDir() {
|
||||
b, _ := os.ReadFile(p)
|
||||
rel, _ := filepath.Rel(home, p)
|
||||
seedMem[rel] = string(b)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
seedStore := map[string]string{
|
||||
"state/projects": string(getBytes(bkState, stProjectsKey)),
|
||||
"state/active_project": getStr(bkState, stActiveProjectKey),
|
||||
"chat/index": string(getBytes(bkChat, ckIndex)),
|
||||
}
|
||||
for _, m := range idx {
|
||||
seedStore["chat/"+convKey(m.ID)] = string(getBytes(bkChat, convKey(m.ID)))
|
||||
}
|
||||
for k, v := range allKV(bkTracker) {
|
||||
seedStore["tracker/"+k] = string(v)
|
||||
}
|
||||
|
||||
long := strings.Repeat("é", 2100) + " fin"
|
||||
var steps []parityProjectStep
|
||||
for _, st := range []parityProjectStep{
|
||||
{Name: "liste", Method: "GET"},
|
||||
{Name: "création", Method: "POST", Body: `{"action":"create","name":"Alpha"}`},
|
||||
{Name: "création, même nom", Method: "POST", Body: `{"action":"create","name":"Alpha"}`},
|
||||
{Name: "création translittérée", Method: "POST", Body: `{"action":" CREATE ","name":" Réseau NAS — ça ñ "}`},
|
||||
{Name: "création sans lettres", Method: "POST", Body: `{"action":"create","name":"!!!"}`},
|
||||
{Name: "création sans lettres bis", Method: "POST", Body: `{"action":"create","name":"???"}`},
|
||||
{Name: "création longue", Method: "POST",
|
||||
Body: `{"action":"create","name":"` + strings.Repeat("Ab-", 30) + `"}`},
|
||||
{Name: "création très longue", Method: "POST",
|
||||
Body: `{"action":"create","name":"` + strings.Repeat("ü", 70) + `"}`},
|
||||
{Name: "création vide", Method: "POST", Body: `{"action":"create","name":" "}`},
|
||||
{Name: "renommage", Method: "POST", Body: `{"action":"rename","slug":" alpha-2 ","name":" Bêta "}`},
|
||||
{Name: "renommage vide", Method: "POST", Body: `{"action":"rename","slug":"alpha-2","name":""}`},
|
||||
{Name: "renommage inconnu", Method: "POST", Body: `{"action":"rename","slug":"nope","name":"X"}`},
|
||||
{Name: "description", Method: "POST", Body: `{"action":"desc","slug":"alpha","desc":" Le projet Alpha. "}`},
|
||||
{Name: "description longue", Method: "POST", Body: `{"action":"desc","slug":"alpha-2","desc":"` + long + `"}`},
|
||||
{Name: "description coupée sur une espace", Method: "POST",
|
||||
Body: `{"action":"desc","slug":"projet","desc":"` + strings.Repeat("é", 1999) + ` suite"}`},
|
||||
{Name: "description effacée", Method: "POST", Body: `{"action":"desc","slug":"alpha-2","desc":" "}`},
|
||||
{Name: "description inconnue", Method: "POST", Body: `{"action":"desc","slug":"nope","desc":"x"}`},
|
||||
{Name: "bascule", Method: "POST", Body: `{"action":"switch","slug":"alpha"}`},
|
||||
{Name: "bascule sur l'actif", Method: "POST", Body: `{"action":"switch","slug":"alpha"}`},
|
||||
{Name: "bascule inconnue", Method: "POST", Body: `{"action":"switch","slug":"nope"}`},
|
||||
{Name: "bascule pendant une génération", Generating: true, Method: "POST", Body: `{"action":"switch","slug":"generale"}`},
|
||||
{Name: "suppression pendant une génération", Generating: true, Method: "POST", Body: `{"action":"delete","slug":"alpha"}`},
|
||||
{Name: "suppression inconnue pendant une génération", Generating: true, Method: "POST", Body: `{"action":"delete","slug":"nope"}`},
|
||||
{Name: "bascule inconnue pendant une génération", Generating: true, Method: "POST", Body: `{"action":"switch","slug":"nope"}`},
|
||||
{Name: "suppression de l'actif", Method: "POST", Body: `{"action":"delete","slug":"alpha"}`},
|
||||
{Name: "suppression inconnue", Method: "POST", Body: `{"action":"delete","slug":"nope"}`},
|
||||
{Name: "suppression", Method: "POST", Body: `{"action":"delete","slug":"projet"}`},
|
||||
{Name: "bascule vers un projet sans discussion", Method: "POST", Body: `{"action":"switch","slug":"projet-2"}`},
|
||||
{Name: "suppression bis", Method: "POST", Body: `{"action":"delete","slug":"alpha-2"}`},
|
||||
{Name: "suppression ter", Method: "POST", Body: `{"action":"delete","slug":"reseau-nas-ca-n"}`},
|
||||
{Name: "liste après", Method: "GET"},
|
||||
{Name: "action inconnue", Method: "POST", Body: `{"action":"Nuke","slug":"generale"}`},
|
||||
{Name: "action vide", Method: "POST", Body: `{}`},
|
||||
{Name: "corps vide", Method: "POST", Body: ``},
|
||||
{Name: "corps illisible", Method: "POST", Body: `{"action":"create"`},
|
||||
{Name: "type faux", Method: "POST", Body: `{"action":"create","name":7}`},
|
||||
{Name: "méthode refusée", Method: "PUT", Body: `{}`},
|
||||
} {
|
||||
conv.mu.Lock()
|
||||
conv.Generating = st.Generating
|
||||
conv.mu.Unlock()
|
||||
rec := httptest.NewRecorder()
|
||||
handleProjects(rec, httptest.NewRequest(st.Method, "/api/projects", strings.NewReader(st.Body)))
|
||||
conv.mu.Lock()
|
||||
conv.Generating = false
|
||||
conv.mu.Unlock()
|
||||
st.Status = rec.Code
|
||||
var out any
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("%s : %q", st.Name, rec.Body.String())
|
||||
}
|
||||
st.Out = parityZeroTimes(out)
|
||||
st.After = snapshot()
|
||||
steps = append(steps, st)
|
||||
}
|
||||
// Les dernières suppressions, jusqu'au dernier projet.
|
||||
for _, p := range listProjects() {
|
||||
if p.Slug == defaultProjectSlug {
|
||||
continue
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
handleProjects(rec, httptest.NewRequest("POST", "/api/projects",
|
||||
strings.NewReader(`{"action":"delete","slug":"`+p.Slug+`"}`)))
|
||||
var out any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
||||
steps = append(steps, parityProjectStep{Name: "suppression de " + p.Slug, Method: "POST",
|
||||
Body: `{"action":"delete","slug":"` + p.Slug + `"}`, Status: rec.Code, Out: parityZeroTimes(out), After: snapshot()})
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
handleProjects(rec, httptest.NewRequest("POST", "/api/projects", strings.NewReader(`{"action":"delete","slug":"generale"}`)))
|
||||
var out any
|
||||
_ = json.Unmarshal(rec.Body.Bytes(), &out)
|
||||
steps = append(steps, parityProjectStep{Name: "dernier projet", Method: "POST",
|
||||
Body: `{"action":"delete","slug":"generale"}`, Status: rec.Code, Out: parityZeroTimes(out), After: snapshot()})
|
||||
|
||||
got, err := parityJSON(map[string]any{"seed": seed, "seed_store": seedStore, "seed_mem": seedMem, "steps": steps})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got = []byte(strings.ReplaceAll(string(got), home, "{{HOME}}"))
|
||||
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
||||
if err := os.MkdirAll(filepath.Dir(parityProjectsFile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(parityProjectsFile, got, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(parityProjectsFile)
|
||||
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("le hub des projets ne produit plus le journal rangé dans %s.\n"+
|
||||
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteProjets,\n"+
|
||||
"puis cargo test --test projects_api dit si le port Rust suit.\n--- obtenu ---\n%s", parityProjectsFile, got)
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,10 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
handleInternalCDPShutdown(w, r)
|
||||
case "/internal/v1/service/action":
|
||||
handleInternalServiceAction(w, r)
|
||||
case "/internal/v1/project/switch":
|
||||
handleInternalProjectSwitch(w, r)
|
||||
case "/internal/v1/project/drop-convs":
|
||||
handleInternalProjectDropConvs(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user