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) } }