mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
parity_engine_test.go appelle lastErrorLine (sys_service_rust_linux.go) et suppose la supervision par fichier PID du conteneur ; la façade ne tourne qu'en conteneur Linux. La CI macOS ne compilait plus les tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
297 lines
13 KiB
Go
297 lines
13 KiB
Go
//go:build linux
|
|
|
|
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// parity_system_test.go — journal de référence des interrupteurs système
|
|
// (lot 17 de la migration Rust) : accès internet et Crawl4AI (moteur, URL,
|
|
// clé jamais rendue, joignabilité et son cache), pilotage du navigateur et
|
|
// vision, clé de complétion (génération, saisie, effacement, repli sur
|
|
// config.env, adresse à coller dans un client tiers, refus pendant une
|
|
// optimisation) et écoute réseau du moteur. Après chaque appel : les clés
|
|
// touchées en base et les en-têtes reçus par les faux serveurs Crawl4AI.
|
|
// Rangé dans rust/loki-rs/tests/parity_system/system.json ; tests/system.rs
|
|
// le rejoue par la façade.
|
|
//
|
|
// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteSysteme
|
|
|
|
const paritySystemFile = "../../rust/loki-rs/tests/parity_system/system.json"
|
|
|
|
type paritySystemStep struct {
|
|
Name string `json:"name"`
|
|
Config map[string]string `json:"config,omitempty"` // posé avant l'appel ; "" = supprimé
|
|
State map[string]string `json:"state,omitempty"`
|
|
Tune bool `json:"tune,omitempty"`
|
|
Method string `json:"method"`
|
|
Path string `json:"path"`
|
|
Body string `json:"body,omitempty"`
|
|
Headers map[string]string `json:"headers,omitempty"`
|
|
Status int `json:"status"`
|
|
Out any `json:"out"`
|
|
After map[string]string `json:"after"`
|
|
Probes []string `json:"probes"` // « chemin Authorization » reçus depuis l'appel précédent
|
|
}
|
|
|
|
// paritySystemKeys : ce que les routes touchent, relu après chaque appel.
|
|
var paritySystemKeys = []string{
|
|
"config/WEB_ENGINE", "config/CRAWL4AI_URL", "config/CRAWL4AI_KEY", "config/API_KEY", "config/HOST",
|
|
"state/internet", "state/computer", "state/crawl_key", "state/api_key", "state/public_url",
|
|
}
|
|
|
|
var (
|
|
parityKeyRe = regexp.MustCompile(`sk-loki-[0-9a-f]{48}`)
|
|
parityMaskedRe = regexp.MustCompile(`sk-loki-[0-9a-f]…[0-9a-f]{4}`)
|
|
)
|
|
|
|
func TestPariteSysteme(t *testing.T) {
|
|
home := testHome(t)
|
|
t.Setenv("LOKI_CONTAINER", "1")
|
|
t.Setenv("LOKI_CHROME", "/opt/faux/chrome")
|
|
|
|
var mu sync.Mutex
|
|
var probes []string
|
|
fake := func(code int) *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
mu.Lock()
|
|
probes = append(probes, strings.TrimSpace(r.URL.Path+" "+r.Header.Get("Authorization")))
|
|
mu.Unlock()
|
|
w.WriteHeader(code)
|
|
}))
|
|
}
|
|
ok, down, notFound := fake(200), fake(500), fake(404)
|
|
defer ok.Close()
|
|
defer down.Close()
|
|
defer notFound.Close()
|
|
|
|
mask := func(s string) string {
|
|
s = strings.ReplaceAll(s, ok.URL, "{{OK}}")
|
|
s = strings.ReplaceAll(s, down.URL, "{{DOWN}}")
|
|
s = strings.ReplaceAll(s, notFound.URL, "{{NOTFOUND}}")
|
|
s = strings.ReplaceAll(s, home, "{{HOME}}")
|
|
if ip := localIP(); ip != "" {
|
|
s = strings.ReplaceAll(s, "http://"+ip+":", "http://{{IP}}:")
|
|
}
|
|
s = parityKeyRe.ReplaceAllString(s, "{{KEY}}")
|
|
return parityMaskedRe.ReplaceAllString(s, "{{MASKED}}")
|
|
}
|
|
unmask := func(s string) string {
|
|
s = strings.ReplaceAll(s, "{{OK}}", ok.URL)
|
|
s = strings.ReplaceAll(s, "{{DOWN}}", down.URL)
|
|
return strings.ReplaceAll(s, "{{NOTFOUND}}", notFound.URL)
|
|
}
|
|
handlers := map[string]http.HandlerFunc{
|
|
"/api/internet": handleInternet,
|
|
"/api/computer": handleComputer,
|
|
"/api/apikey": handleAPIKey,
|
|
"/api/network": handleNetwork,
|
|
}
|
|
run := func(st paritySystemStep) paritySystemStep {
|
|
for k, v := range st.Config {
|
|
if err := SetConfigKey(k, unmask(v)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for k, v := range st.State {
|
|
if err := putStr(bkState, k, unmask(v)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var lock *tuneLock
|
|
if st.Tune {
|
|
l, err := tuneLockAcquire("web", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lock = l
|
|
}
|
|
mu.Lock()
|
|
probes = nil
|
|
mu.Unlock()
|
|
req := httptest.NewRequest(st.Method, st.Path, strings.NewReader(unmask(st.Body)))
|
|
req.Host = "loki.local:8090"
|
|
for k, v := range st.Headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
handlers[st.Path](rec, req)
|
|
if lock != nil {
|
|
lock.release()
|
|
}
|
|
st.Status = rec.Code
|
|
var out any
|
|
if err := json.Unmarshal([]byte(mask(rec.Body.String())), &out); err != nil {
|
|
t.Fatalf("%s : corps illisible %q", st.Name, rec.Body.String())
|
|
}
|
|
st.Out = out
|
|
st.After = map[string]string{}
|
|
for _, k := range paritySystemKeys {
|
|
b, key, _ := strings.Cut(k, "/")
|
|
if v := getStr(b, key); v != "" {
|
|
st.After[k] = mask(v)
|
|
}
|
|
}
|
|
mu.Lock()
|
|
st.Probes = append([]string{}, probes...)
|
|
mu.Unlock()
|
|
for i := range st.Probes {
|
|
st.Probes[i] = mask(st.Probes[i])
|
|
}
|
|
return st
|
|
}
|
|
|
|
var steps []paritySystemStep
|
|
for _, st := range []paritySystemStep{
|
|
// --- accès internet ---
|
|
{Name: "internet par défaut", Method: "GET", Path: "/api/internet"},
|
|
{Name: "allumé, moteur intégré", Method: "POST", Path: "/api/internet", Body: `{"enabled":true}`},
|
|
{Name: "moteur crawl4ai sans URL", Method: "POST", Path: "/api/internet", Body: `{"engine":" CRAWL4AI "}`},
|
|
{Name: "moteur inconnu", Method: "POST", Path: "/api/internet", Body: `{"engine":"bing"}`},
|
|
{Name: "moteur inconnu vide", Method: "POST", Path: "/api/internet", Body: `{"engine":" "}`},
|
|
{Name: "URL joignable", Method: "POST", Path: "/api/internet", Body: `{"url":" {{OK}}/// "}`},
|
|
{Name: "relu dans le cache", Method: "GET", Path: "/api/internet"},
|
|
{Name: "clé posée", Method: "POST", Path: "/api/internet", Body: `{"key":" secret-clé-crawl "}`},
|
|
{Name: "clé courte", Method: "POST", Path: "/api/internet", Body: `{"key":"abcd"}`},
|
|
{Name: "clé d'un caractère", Method: "POST", Path: "/api/internet", Body: `{"key":"é"}`},
|
|
{Name: "clé résiduelle en config", Config: map[string]string{"CRAWL4AI_KEY": "vieille"},
|
|
Method: "POST", Path: "/api/internet", Body: `{"key":"neuve12345"}`},
|
|
{Name: "URL en panne", Method: "POST", Path: "/api/internet", Body: `{"url":"{{DOWN}}"}`},
|
|
{Name: "URL 404", Method: "POST", Path: "/api/internet", Body: `{"url":"{{NOTFOUND}}"}`},
|
|
{Name: "URL injoignable", Method: "POST", Path: "/api/internet", Body: `{"url":"http://127.0.0.1:1"}`},
|
|
{Name: "URL sans schéma", Method: "POST", Path: "/api/internet", Body: `{"url":"exemple"}`},
|
|
{Name: "type faux, le reste passe", Method: "POST", Path: "/api/internet", Body: `{"enabled":"oui","url":"{{OK}}"}`},
|
|
{Name: "corps illisible", Method: "POST", Path: "/api/internet", Body: `{"enabled":false`},
|
|
{Name: "éteint, clé effacée", Method: "POST", Path: "/api/internet", Body: `{"enabled":false,"key":""}`},
|
|
{Name: "null ne touche à rien", Method: "POST", Path: "/api/internet", Body: `{"enabled":null,"url":null,"engine":null}`},
|
|
{Name: "URL vidée, moteur choisi gardé", Method: "POST", Path: "/api/internet", Body: `{"url":"","enabled":true}`},
|
|
{Name: "moteur go", Method: "POST", Path: "/api/internet", Body: `{"engine":"Go"}`},
|
|
{Name: "moteur non choisi, URL héritée", Config: map[string]string{"WEB_ENGINE": "", "CRAWL4AI_URL": "{{OK}}"},
|
|
Method: "GET", Path: "/api/internet"},
|
|
{Name: "moteur illisible en config", Config: map[string]string{"WEB_ENGINE": "autre", "CRAWL4AI_URL": ""},
|
|
Method: "GET", Path: "/api/internet"},
|
|
{Name: "URL en config avec slashs", Config: map[string]string{"WEB_ENGINE": "crawl4ai", "CRAWL4AI_URL": " {{DOWN}}// "},
|
|
Method: "GET", Path: "/api/internet"},
|
|
{Name: "internet en PUT", Method: "PUT", Path: "/api/internet", Body: `{"enabled":true}`},
|
|
|
|
// --- navigateur ---
|
|
{Name: "navigateur par défaut", Method: "GET", Path: "/api/computer"},
|
|
{Name: "navigateur allumé", Method: "POST", Path: "/api/computer", Body: `{"on":true}`},
|
|
{Name: "navigateur, corps illisible", Method: "POST", Path: "/api/computer", Body: `oui`},
|
|
{Name: "vision par projecteur", Config: map[string]string{"MMPROJ": " mmproj.gguf "},
|
|
Method: "GET", Path: "/api/computer"},
|
|
{Name: "vision externe", Config: map[string]string{"MMPROJ": "", "EXTERNAL": "1", "EXTERNAL_VISION": "1"},
|
|
Method: "GET", Path: "/api/computer"},
|
|
{Name: "externe sans vision", Config: map[string]string{"EXTERNAL_VISION": "0", "MMPROJ": "m.gguf"},
|
|
Method: "GET", Path: "/api/computer"},
|
|
{Name: "vision strata", Config: map[string]string{"EXTERNAL": "", "ENGINE": "strata"},
|
|
Method: "GET", Path: "/api/computer"},
|
|
{Name: "strata sans vision", Config: map[string]string{"STRATA_VISION": "0", "MMPROJ": ""},
|
|
Method: "GET", Path: "/api/computer"},
|
|
{Name: "navigateur éteint", Config: map[string]string{"ENGINE": "", "STRATA_VISION": ""},
|
|
Method: "POST", Path: "/api/computer", Body: `{"on":false}`},
|
|
|
|
// --- clé de complétion ---
|
|
{Name: "clé par défaut", Method: "GET", Path: "/api/apikey"},
|
|
{Name: "clé générée", Method: "POST", Path: "/api/apikey", Body: `{"action":"generate"}`},
|
|
{Name: "clé relue", Method: "GET", Path: "/api/apikey", Headers: map[string]string{"X-Forwarded-Proto": "HTTPS, http"}},
|
|
{Name: "clé saisie", Method: "POST", Path: "/api/apikey", Body: `{"action":"set","key":" ma-clé-perso-é "}`},
|
|
{Name: "clé saisie courte", Method: "POST", Path: "/api/apikey", Body: `{"action":"set","key":"abc"}`},
|
|
{Name: "clé de 13 octets", Method: "POST", Path: "/api/apikey", Body: `{"action":"set","key":"abcdefghijklm"}`},
|
|
{Name: "clé effacée", Method: "POST", Path: "/api/apikey", Body: `{"action":"clear"}`},
|
|
{Name: "repli sur config.env", Config: map[string]string{"API_KEY": " résiduelle-config "},
|
|
Method: "GET", Path: "/api/apikey", Headers: map[string]string{"X-Forwarded-Proto": "ftp"}},
|
|
{Name: "adresse publique", State: map[string]string{"public_url": "https://loki.exemple.fr"},
|
|
Method: "POST", Path: "/api/apikey", Body: `{"action":"set","key":"k"}`},
|
|
{Name: "action inconnue", Method: "POST", Path: "/api/apikey", Body: `{"action":"rotate"}`},
|
|
{Name: "action vide", Method: "POST", Path: "/api/apikey", Body: `{}`},
|
|
{Name: "corps vide", Method: "POST", Path: "/api/apikey", Body: ``},
|
|
{Name: "corps illisible", Method: "POST", Path: "/api/apikey", Body: `{"action":`},
|
|
{Name: "type faux", Method: "POST", Path: "/api/apikey", Body: `{"action":1}`},
|
|
{Name: "optimisation en cours", Tune: true, Method: "POST", Path: "/api/apikey", Body: `{"action":"clear"}`},
|
|
{Name: "lecture pendant une optimisation", Tune: true, Method: "GET", Path: "/api/apikey"},
|
|
|
|
// --- réseau ---
|
|
{Name: "réseau par défaut", Method: "GET", Path: "/api/network"},
|
|
{Name: "réseau fermé", Method: "POST", Path: "/api/network", Body: `{"exposed":false}`},
|
|
{Name: "réseau ouvert", Method: "POST", Path: "/api/network", Body: `{"exposed":true}`},
|
|
{Name: "réseau sans champ", Method: "POST", Path: "/api/network", Body: `{}`},
|
|
{Name: "réseau illisible", Method: "POST", Path: "/api/network", Body: `{"exposed":1}`},
|
|
{Name: "réseau corps vide", Method: "POST", Path: "/api/network", Body: ``},
|
|
{Name: "HOST localhost", Config: map[string]string{"HOST": " LocalHost ", "PORT": "9090"}, Method: "GET", Path: "/api/network"},
|
|
{Name: "HOST ::1", Config: map[string]string{"HOST": "::1", "PORT": "abc"}, Method: "GET", Path: "/api/network"},
|
|
{Name: "HOST d'une carte", Config: map[string]string{"HOST": "192.168.1.5", "PORT": ""}, Method: "GET", Path: "/api/network"},
|
|
} {
|
|
steps = append(steps, run(st))
|
|
}
|
|
|
|
// --- le Chromium de Playwright, le plus récent ---
|
|
type chromiumCase struct {
|
|
Files []string `json:"files"` // relatifs à la racine ; « / » final = dossier
|
|
Out string `json:"out"`
|
|
}
|
|
var chromium []chromiumCase
|
|
for _, files := range [][]string{
|
|
{"chromium-1000/chrome-linux/chrome", "chromium-1200/chrome-linux/chrome"},
|
|
{"chromium-1000/chrome-linux/chrome", "chromium-1200/chrome-linux/headless_shell"},
|
|
{"chromium-1200/chrome-linux/headless_shell", "chromium-1200/chrome-linux/chrome"},
|
|
{"chromium-1000/chrome-linux/chrome", "chromium_headless_shell-1100/chrome-linux/headless_shell"},
|
|
{"chromium-1000/chrome-linux/chrome", "chromium-1300/chrome-linux/chrome/"},
|
|
{"chromiumX/chrome-linux/chrome", "chromium-1/chrome-linux/chrome"},
|
|
{"firefox-1/chrome-linux/chrome", "chromium-/chrome-linux/chrome"},
|
|
{"chromium-9/chrome-linux/chrome", "chromium-10/chrome-linux/chrome"},
|
|
} {
|
|
root := t.TempDir()
|
|
for _, f := range files {
|
|
p := filepath.Join(root, f)
|
|
if strings.HasSuffix(f, "/") {
|
|
if err := os.MkdirAll(p, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
continue
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, nil, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
t.Setenv("PLAYWRIGHT_BROWSERS_PATH", root)
|
|
chromium = append(chromium, chromiumCase{Files: files,
|
|
Out: strings.TrimPrefix(playwrightChromium(), root+"/")})
|
|
}
|
|
|
|
got, err := parityJSON(map[string]any{"steps": steps, "chromium": chromium})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if os.Getenv("LOKI_PARITY_UPDATE") == "1" {
|
|
if err := os.MkdirAll(filepath.Dir(paritySystemFile), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(paritySystemFile, got, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
want, err := os.ReadFile(paritySystemFile)
|
|
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("les interrupteurs système ne produisent plus le journal rangé dans %s.\n"+
|
|
"Si c'est voulu : LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteSysteme,\n"+
|
|
"puis cargo test --test system dit si le port Rust suit.\n--- obtenu ---\n%s", paritySystemFile, got)
|
|
}
|
|
}
|