Lot 17 : interrupteurs système tenus par la façade (LOKI_SYSTEM=rust)

La façade sert /api/internet (moteur web, serveur Crawl4AI et sa clé
jamais rendue, joignabilité avec son cache de 30 s), /api/computer
(interrupteur du navigateur, binaire trouvé, vision), /api/apikey (clé de
complétion, repli sur config.env, adresse /v1 à coller dans un client
tiers) et /api/network (écoute du moteur) quand LOKI_SYSTEM=rust
(expérimental ; le Go reste le défaut).

Restent au Go derrière son API interne (facade_system.go) : le cache de
joignabilité de ses outils web (/internal/v1/web/reach/reset), le
Chromium des outils browser_* (/internal/v1/cdp/shutdown) et le
redémarrage qui applique une clé neuve quand la façade ne pilote pas le
moteur (/internal/v1/service/action). Le relais du tunnel garde l'hôte
et X-Forwarded-Proto du client.

Correction en passant : maskAPIKey paniquait sur une clé d'un octet, et
/api/apikey tombait à chaque lecture.

Parité : parity_system_test.go tire du Go 61 appels de route avec la
base et les sondes reçues par trois faux Crawl4AI après chacun, et huit
dispositions du dossier de Playwright, rejoués par tests/system.rs ; ce
qui reste au Go vérifié de bout en bout ; TestFacadeSysteme,
TestFacadeRelaisGardeHote et TestMaskAPIKeyCourte 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:
Claude committed 2026-10-15 18:51:58 +00:00
1 parent 2da2efc1fd
commit a4f8bd3ec4
19 files changed
+3110 -3

No files matched your search

+6 -1
View File
@@ -73,11 +73,16 @@ func writeAPIKey(key string) error {
}
// maskAPIKey renders a key for display: keep the "sk-loki-" prefix and last 4
// chars, elide the middle. Empty in → empty out.
// chars, elide the middle. Empty in → empty out. Une clé d'un ou deux octets
// n'est pas montrée du tout : k[len(k)-2:] paniquait sur un seul octet, et
// /api/apikey tombait à chaque lecture tant que la clé restait en place.
func maskAPIKey(k string) string {
if k == "" {
return ""
}
if len(k) <= 2 {
return "…"
}
if len(k) <= 13 {
return "…" + k[len(k)-2:]
}
+9 -1
View File
@@ -145,7 +145,7 @@ 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).
// moteur (facade_engine.go), les interrupteurs système (facade_system.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 {
@@ -168,6 +168,8 @@ func withFacadeChat(path string, h http.HandlerFunc) http.HandlerFunc {
held, unavailable = facadePresets, "presets indisponibles : "
case facadeEngineRoutes[path]:
held, unavailable = facadeEngine, "pilotage du moteur indisponible : "
case facadeSystemRoutes[path]:
held, unavailable = facadeSystem, "réglages système indisponibles : "
default:
return h
}
@@ -186,6 +188,12 @@ func facadeRelayAs(w http.ResponseWriter, r *http.Request, unavailable string) {
rp := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(&url.URL{Scheme: "http", Host: facadeAddr()})
// L'hôte et le schéma du client, pas ceux du relais : la clé de
// complétion affiche l'adresse à coller dans un client tiers.
pr.Out.Host = pr.In.Host
if p := pr.In.Header.Get("X-Forwarded-Proto"); p != "" {
pr.Out.Header.Set("X-Forwarded-Proto", p)
}
pr.Out.Header.Set(internalTokenHeader, internalToken())
},
FlushInterval: -1, // SSE : chaque trame part aussitôt
+113
View File
@@ -645,3 +645,116 @@ func TestFacadeMoteur(t *testing.T) {
t.Fatalf("arrêt de la dictée : %d", w.Code)
}
}
// Une clé de complétion d'un ou deux octets ne fait plus tomber /api/apikey.
func TestMaskAPIKeyCourte(t *testing.T) {
for k, want := range map[string]string{
"": "",
"k": "…",
"ab": "…",
"abc": "…bc",
"abcdefghijklm": "…lm",
"sk-loki-0123456789abcdef": "sk-loki-0…cdef",
} {
if got := maskAPIKey(k); got != want {
t.Errorf("maskAPIKey(%q) = %q, attendu %q", k, got, want)
}
}
}
// Les interrupteurs système (LOKI_SYSTEM=rust, lot 17) partent à la façade ;
// l'API interne lui rend ce qui vit ici : cache de joignabilité des outils
// web, navigateur piloté, redémarrage du moteur.
func TestFacadeSysteme(t *testing.T) {
testHome(t)
t.Setenv("LOKI_CONTAINER", "1")
f := demarrerFacade(t)
t.Setenv(facadeChatEnv, "")
local := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "go") }
for p := range facadeSystemRoutes {
w := httptest.NewRecorder()
withFacadeChat(p, local)(w, httptest.NewRequest("GET", p, nil))
if w.Body.String() != "go" {
t.Fatalf("%s servi ailleurs sans LOKI_SYSTEM : %q", p, w.Body.String())
}
}
t.Setenv(facadeSystemEnv, "rust")
for p := range facadeSystemRoutes {
w := httptest.NewRecorder()
withFacadeChat(p, local)(w, httptest.NewRequest("POST", p, strings.NewReader("{}")))
if w.Body.String() != f.reply {
t.Fatalf("%s : réponse %q", p, w.Body.String())
}
}
if n := len(f.recu()); n != len(facadeSystemRoutes) {
t.Fatalf("reçu par la façade : %d appels", n)
}
// Cache de joignabilité vidé.
reachMu.Lock()
reachURL = "http://ancien"
reachMu.Unlock()
w := httptest.NewRecorder()
handleInternalReachReset(w, httptest.NewRequest("POST", "/internal/v1/web/reach/reset", nil))
reachMu.Lock()
vide := reachURL == ""
reachMu.Unlock()
if w.Code != 204 || !vide {
t.Fatalf("reach/reset : %d, cache vidé : %v", w.Code, vide)
}
w = httptest.NewRecorder()
handleInternalCDPShutdown(w, httptest.NewRequest("POST", "/internal/v1/cdp/shutdown", nil))
if w.Code != 204 {
t.Fatalf("cdp/shutdown : %d", w.Code)
}
// Redémarrage : seulement si le moteur tourne quand if_active est posé.
var actions []string
old := internalServiceAction
t.Cleanup(func() { internalServiceAction = old })
internalServiceAction = func(a string) error {
actions = append(actions, a)
if a == "start" {
return errTuneBusy
}
return nil
}
for _, c := range []struct {
body, want string
code int
}{
{`{"action":"restart","if_active":true}`, `{"error":"","ran":false}`, 200},
{`{"action":"restart"}`, `{"error":"","ran":true}`, 200},
{`{"action":"start"}`, `{"error":"` + errTuneBusy.Error() + `","ran":true}`, 200},
{`{"action":"reload"}`, `{"error":"action inconnue : reload"}`, 400},
{`{"action":`, `{"error":"unexpected EOF"}`, 400},
} {
w := httptest.NewRecorder()
handleInternalServiceAction(w, httptest.NewRequest("POST", "/internal/v1/service/action", strings.NewReader(c.body)))
if got := strings.TrimSpace(w.Body.String()); w.Code != c.code || got != c.want {
t.Fatalf("%s : %d %s", c.body, w.Code, got)
}
}
if strings.Join(actions, ",") != "restart,start" {
t.Fatalf("actions : %v", actions)
}
}
// Le relais vers la façade garde l'hôte et le schéma du client : la clé de
// complétion y calcule l'adresse à coller dans un client tiers.
func TestFacadeRelaisGardeHote(t *testing.T) {
var host, proto string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, proto = r.Host, r.Header.Get("X-Forwarded-Proto")
}))
defer srv.Close()
t.Setenv(internalTokenEnv, jetonTest)
t.Setenv(facadeAddrEnv, strings.TrimPrefix(srv.URL, "http://"))
r := httptest.NewRequest("GET", "/api/apikey", nil)
r.Host = "loki.exemple.fr"
r.Header.Set("X-Forwarded-Proto", "https")
facadeRelayAs(httptest.NewRecorder(), r, "")
if host != "loki.exemple.fr" || proto != "https" {
t.Fatalf("hôte %q, schéma %q", host, proto)
}
}
+98
View File
@@ -0,0 +1,98 @@
package loki
import (
"encoding/json"
"net/http"
"os"
"strings"
)
// facade_system.go — interrupteurs système tenus par la façade Rust
// (LOKI_SYSTEM=rust, lot 17 de la migration ; expérimental, le Go reste le
// défaut) : accès internet (/api/internet), pilotage du navigateur
// (/api/computer), clé de complétion (/api/apikey) et écoute réseau du moteur
// (/api/network).
//
// Les réglages restent des clés de la base. Ce qui vit dans ce processus reste
// ici, derrière l'API interne :
//
// - le cache de joignabilité de Crawl4AI des outils web, vidé quand l'URL, la
// clé ou le moteur changent (/internal/v1/web/reach/reset) ;
// - le Chromium piloté par les outils browser_*, fermé quand l'interrupteur
// se coupe (/internal/v1/cdp/shutdown) ;
// - le redémarrage du moteur qui applique une nouvelle clé, quand la façade
// ne pilote pas le moteur (/internal/v1/service/action).
const facadeSystemEnv = "LOKI_SYSTEM"
// facadeSystem : la façade tient-elle les interrupteurs système ?
func facadeSystem() bool {
return strings.EqualFold(strings.TrimSpace(os.Getenv(facadeSystemEnv)), "rust") &&
facadeAddr() != "" && internalToken() != ""
}
// facadeSystemRoutes : les routes que la façade sert alors (system.rs).
var facadeSystemRoutes = map[string]bool{
"/api/internet": true,
"/api/computer": true,
"/api/apikey": true,
"/api/network": true,
}
// handleInternalReachReset : POST → 204 — le prochain tour reteste Crawl4AI.
func handleInternalReachReset(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
return
}
reachMu.Lock()
reachURL = ""
reachMu.Unlock()
w.WriteHeader(http.StatusNoContent)
}
// handleInternalCDPShutdown : POST → 204 — ferme le navigateur piloté.
func handleInternalCDPShutdown(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
return
}
cdpShutdown()
w.WriteHeader(http.StatusNoContent)
}
// internalServiceAction : serviceAction, variable pour les tests.
var internalServiceAction = serviceAction
// handleInternalServiceAction : POST {action, if_active} → 200 {ran, error}.
// if_active : seulement si le moteur tourne (une clé neuve ne s'applique qu'au
// redémarrage, inutile de démarrer un moteur arrêté pour elle).
func handleInternalServiceAction(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "POST attendu"})
return
}
var in struct {
Action string `json:"action"`
IfActive bool `json:"if_active"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
sendJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
return
}
switch in.Action {
case "start", "stop", "restart":
default:
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "action inconnue : " + in.Action})
return
}
if in.IfActive && !serviceIsActive() {
sendJSON(w, http.StatusOK, map[string]any{"ran": false, "error": ""})
return
}
msg := ""
if err := internalServiceAction(in.Action); err != nil {
msg = err.Error()
}
sendJSON(w, http.StatusOK, map[string]any{"ran": true, "error": msg})
}
+294
View File
@@ -0,0 +1,294 @@
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)
}
}
+6
View File
@@ -119,6 +119,12 @@ func withInternalAPI(next http.Handler) http.Handler {
handleInternalChatGenerating(w, r)
case "/internal/v1/dictate/stop":
handleInternalDictateStop(w, r)
case "/internal/v1/web/reach/reset":
handleInternalReachReset(w, r)
case "/internal/v1/cdp/shutdown":
handleInternalCDPShutdown(w, r)
case "/internal/v1/service/action":
handleInternalServiceAction(w, r)
default:
http.NotFound(w, r)
}