Files
glpi-neural-brain/internal/web/server_test.go
jbergner 440423c5b6
All checks were successful
release-tag / release-image (push) Successful in 2m43s
RC-3
2026-08-09 11:29:13 +02:00

222 lines
8.2 KiB
Go

package web
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/local/glpi-neural-brain/internal/activity"
"github.com/local/glpi-neural-brain/internal/config"
"github.com/local/glpi-neural-brain/internal/engine"
"github.com/local/glpi-neural-brain/internal/graph"
"github.com/local/glpi-neural-brain/internal/model"
"github.com/local/glpi-neural-brain/internal/research"
)
func TestRuntimeSettingsAndSourcesAPI(t *testing.T) {
data := t.TempDir()
g, err := graph.Open(data)
if err != nil {
t.Fatal(err)
}
g.UpsertNode(model.Node{ID: "n1", Kind: "knowledge", Label: "GLPI", Origin: "test", Categories: []string{"GLPI"}, Metadata: map[string]any{"source": "GLPI Knowledge Base"}})
g.UpsertNode(model.Node{ID: "n2", Kind: "knowledge", Label: "Ollama", Origin: "test", Categories: []string{"Ollama"}, Metadata: map[string]any{"source": "internal-kb"}})
g.UpsertNode(model.Node{ID: "n3", Kind: "knowledge", Label: "Ohne Kategorie", Origin: "test"})
broker := activity.New(20)
eng := engine.New(config.Config{
DataDir: data,
PersistInterval: time.Minute,
RuntimeDefaultsConfigured: true,
LearningEnabled: true,
ThinkingEnabled: true,
DefaultView: "neural",
GLPIKBSource: "Configured GLPI Source",
}, g, broker)
h := (&Server{Engine: eng, Graph: g, Broker: broker}).Handler()
body := `{"learning_enabled":false,"thinking_enabled":false,"learning_sources":["GLPI Knowledge Base"],"display_sources":["internal-kb"],"thinking_sources":["GLPI Knowledge Base"],"view_mode":"constellation","max_display_nodes":1500,"low_power_mode":true}`
req := httptest.NewRequest(http.MethodPut, "/api/runtime-settings", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
res := httptest.NewRecorder()
h.ServeHTTP(res, req)
if res.Code != http.StatusOK {
t.Fatalf("PUT runtime settings returned %d: %s", res.Code, res.Body.String())
}
var settings engine.RuntimeSettings
if err := json.NewDecoder(res.Body).Decode(&settings); err != nil {
t.Fatal(err)
}
if settings.LearningEnabled || settings.ThinkingEnabled || settings.ViewMode != "constellation" || settings.MaxDisplayNodes != 1500 || !settings.LowPowerMode {
t.Fatalf("unexpected settings: %+v", settings)
}
res = httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/api/sources", nil))
if res.Code != http.StatusOK {
t.Fatalf("GET sources returned %d", res.Code)
}
var sources struct {
Sources []engine.SourceInfo `json:"sources"`
}
if err := json.NewDecoder(res.Body).Decode(&sources); err != nil {
t.Fatal(err)
}
foundConfiguredGLPI := false
foundInternal := false
for _, source := range sources.Sources {
if source.Name == "Configured GLPI Source" && source.Count == 0 {
foundConfiguredGLPI = true
}
if source.Name == "internal-kb" && source.Count == 1 {
foundInternal = true
}
}
if !foundConfiguredGLPI || !foundInternal {
t.Fatalf("exact source options or configured GLPI source missing: %+v", sources.Sources)
}
res = httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodPost, "/api/enrich?async=1", strings.NewReader(`{}`)))
if res.Code != http.StatusConflict {
t.Fatalf("disabled thinking should return 409, got %d: %s", res.Code, res.Body.String())
}
}
func TestResearchDiagnosticAPI(t *testing.T) {
searx := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"results":[{"title":"Btrfs documentation","url":"https://example.test/btrfs","content":"Snapshot evidence"}]}`))
}))
defer searx.Close()
broker := activity.New(20)
eng := &engine.Engine{
Cfg: config.Config{ResearchEnabled: true},
Research: research.New(searx.URL),
Broker: broker,
}
h := (&Server{Engine: eng, Broker: broker}).Handler()
res := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/research/test", strings.NewReader(`{"query":"btrfs snapshots","limit":4}`))
req.Header.Set("Content-Type", "application/json")
h.ServeHTTP(res, req)
if res.Code != http.StatusOK {
t.Fatalf("research test returned %d: %s", res.Code, res.Body.String())
}
var result engine.ResearchTestResult
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
t.Fatal(err)
}
if !result.OK || !result.Diagnostic.OK || len(result.Results) != 1 {
t.Fatalf("unexpected result: %+v", result)
}
res = httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/api/research/status", nil))
if res.Code != http.StatusOK {
t.Fatalf("research status returned %d: %s", res.Code, res.Body.String())
}
var status map[string]any
if err := json.NewDecoder(res.Body).Decode(&status); err != nil {
t.Fatal(err)
}
if status["ok"] != true {
t.Fatalf("unexpected status: %#v", status)
}
}
func TestSettingsPanelContentIsScrollable(t *testing.T) {
broker := activity.New(4)
h := (&Server{Broker: broker}).Handler()
res := httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/", nil))
if res.Code != http.StatusOK {
t.Fatalf("GET / returned %d: %s", res.Code, res.Body.String())
}
body := res.Body.String()
if !strings.Contains(body, `class="settings-scroll"`) {
t.Fatal("settings panel is missing its scroll container")
}
if !strings.Contains(body, `class="settings-footer"`) {
t.Fatal("settings footer is missing")
}
res = httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/app.css", nil))
if res.Code != http.StatusOK {
t.Fatalf("GET /app.css returned %d: %s", res.Code, res.Body.String())
}
css := res.Body.String()
for _, expected := range []string{
".settings-scroll{flex:1;min-height:0;overflow-y:auto",
"overscroll-behavior:contain",
".settings-footer{flex:0 0 auto",
} {
if !strings.Contains(css, expected) {
t.Fatalf("scroll CSS is missing %q", expected)
}
}
}
func TestAnalysisDashboardPageAndAPI(t *testing.T) {
data := t.TempDir()
g, err := graph.Open(data)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = g.Close() })
broker := activity.New(20)
eng := engine.New(config.Config{DataDir: data, RuntimeDefaultsConfigured: true, LearningEnabled: true, ThinkingEnabled: true, PersistInterval: time.Minute}, g, broker)
g.UpsertNode(model.Node{ID: "n1", Kind: "knowledge", Label: "Analyse", Origin: "test", Metadata: map[string]any{"source": "internal"}})
broker.Publish(model.Activity{Type: "graph.updated", Source: "brain", Message: "Graph aktualisiert", Metadata: map[string]any{"nodes": 1, "edges": 0}})
h := (&Server{Engine: eng, Graph: g, Broker: broker}).Handler()
page := httptest.NewRecorder()
h.ServeHTTP(page, httptest.NewRequest(http.MethodGet, "/analysis.html", nil))
if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "BRAIN ANALYSIS CENTER") || !strings.Contains(page.Body.String(), "ÄNDERUNGSJOURNAL") {
t.Fatalf("analysis page missing: %d %s", page.Code, page.Body.String())
}
deadline := time.Now().Add(3 * time.Second)
for {
res := httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/api/analysis/dashboard?hours=24&limit=50", nil))
if res.Code != http.StatusOK {
t.Fatalf("analysis API returned %d: %s", res.Code, res.Body.String())
}
var payload map[string]any
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
t.Fatal(err)
}
history, _ := payload["history"].(map[string]any)
if count, _ := history["raw_event_count"].(float64); count > 0 {
break
}
if time.Now().After(deadline) {
t.Fatal("analysis event was not visible through API")
}
time.Sleep(20 * time.Millisecond)
}
}
func TestStaticUIAssetsAreNeverServedFromStaleBrowserCache(t *testing.T) {
broker := activity.New(4)
h := (&Server{Broker: broker}).Handler()
for _, path := range []string{"/analysis.html", "/analysis.js", "/analysis.css"} {
res := httptest.NewRecorder()
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, path, nil))
if res.Code != http.StatusOK {
t.Fatalf("GET %s returned %d", path, res.Code)
}
if got := res.Header().Get("Cache-Control"); got != "no-store" {
t.Fatalf("GET %s Cache-Control=%q, want no-store", path, got)
}
}
}