All checks were successful
release-tag / release-image (push) Successful in 2m32s
91 lines
4.6 KiB
Go
91 lines
4.6 KiB
Go
package engine
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/local/glpi-neural-brain/internal/model"
|
||
"github.com/local/glpi-neural-brain/internal/ollama"
|
||
)
|
||
|
||
func TestNormalizeRelationResearchQueryRemovesInternalNodeIDs(t *testing.T) {
|
||
a := model.Node{ID: "8c68b454ce45705357ae1831", Label: "AI Safety Guardrails – sicher gestalten und härten"}
|
||
b := model.Node{ID: "a0808bae9ec114a144632262", Label: "AI Security Guardrails – sicher gestalten und härten"}
|
||
decision := model.RelationDecision{RelationType: "same_topic", TopicLabel: "AI Guardrails", ResearchQuery: "Überprüfung der Einträge 8c68b454ce45705357ae1831 und a0808bae9ec114a144632262 auf Duplikat oder unterschiedliche Quellen"}
|
||
query, rebuilt, reason := normalizeRelationResearchQuery(a, b, decision)
|
||
if !rebuilt || reason != "internal_node_id_removed" {
|
||
t.Fatalf("expected deterministic rebuild, rebuilt=%v reason=%q query=%q", rebuilt, reason, query)
|
||
}
|
||
if strings.Contains(query, a.ID) || strings.Contains(query, b.ID) || internalNodeIDPattern.MatchString(query) {
|
||
t.Fatalf("internal IDs leaked into rebuilt query: %q", query)
|
||
}
|
||
for _, want := range []string{"AI Safety Guardrails", "AI Security Guardrails"} {
|
||
if !strings.Contains(query, want) {
|
||
t.Fatalf("rebuilt query lost visible topic %q: %q", want, query)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestNormalizeRelationResearchQueryRebuildsGenericQueryWithoutTopicAnchor(t *testing.T) {
|
||
a := model.Node{Label: "Kubernetes Restore Testing"}
|
||
b := model.Node{Label: "Kubernetes Backup Validation"}
|
||
decision := model.RelationDecision{RelationType: "supports", ResearchQuery: "Einträge prüfen und Unterschiede validieren"}
|
||
query, rebuilt, reason := normalizeRelationResearchQuery(a, b, decision)
|
||
if !rebuilt || reason != "missing_topic_anchor" || !strings.Contains(strings.ToLower(query), "kubernetes") {
|
||
t.Fatalf("expected topic-anchored rebuild, got rebuilt=%v reason=%q query=%q", rebuilt, reason, query)
|
||
}
|
||
}
|
||
|
||
func TestRelationSnippetGateRejectsUnrelatedDuplicateTools(t *testing.T) {
|
||
question := model.ResearchQuestion{GapID: "relation-evidence", Question: "AI Safety Guardrails und AI Security Guardrails fachlich vergleichen"}
|
||
results := []model.ResearchResult{
|
||
{Title: "IBAN auf Fehler prüfen und Bankverbindung identifizieren", URL: "https://example.org/iban", Snippet: "IBAN prüfen und Duplikate finden"},
|
||
{Title: "Duplikate in Excel finden", URL: "https://example.org/excel", Snippet: "Doppelte Einträge über mehrere Spalten"},
|
||
{Title: "AI Security Guardrails technical guidance", URL: "https://docs.example.org/ai-security/guardrails", Snippet: "AI safety and security guardrails, controls and validation"},
|
||
}
|
||
ranked := rankResearchCandidatesHeuristic(question, results, false)
|
||
selection := selectResearchCandidates(question, ranked, map[string]bool{}, 3, 1, .35, .60, .55)
|
||
for _, candidate := range selection.Selected {
|
||
if strings.Contains(strings.ToLower(candidate.Result.Title), "iban") || strings.Contains(strings.ToLower(candidate.Result.Title), "excel") {
|
||
t.Fatalf("unrelated duplicate-tool result passed relation topic gate: %+v", candidate.Result)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestReconsiderOperationalArticleTypeCanChooseReference(t *testing.T) {
|
||
mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
switch r.URL.Path {
|
||
case "/api/tags":
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"models": []map[string]any{{"name": "qwen3:8b", "digest": "chat"}, {"name": "embeddinggemma", "digest": "embed"}}})
|
||
case "/api/chat":
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": `{"action":"reclassify","article_type":"reference","reason":"Die Evidenz trägt technische Zuordnungen, aber keinen belastbaren Lösungsablauf."}`}})
|
||
default:
|
||
http.NotFound(w, r)
|
||
}
|
||
}))
|
||
defer mock.Close()
|
||
client := ollama.New(mock.URL, "qwen3:8b", "embeddinggemma")
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
client.Start(ctx)
|
||
deadline := time.Now().Add(time.Second)
|
||
for client.PoolStatus()["healthy_nodes"].(int) < 1 && time.Now().Before(deadline) {
|
||
time.Sleep(10 * time.Millisecond)
|
||
}
|
||
e := &Engine{Ollama: client}
|
||
plan := model.ArticlePlanDecision{ArticleType: "troubleshooting", ExpectedValue: "ATT&CK-Techniken einordnen"}
|
||
content := model.KnowledgeArticleContent{Title: "ATT&CK-Techniken", TechnicalDetails: []string{"T1018"}, Mappings: []string{"Profil → T1018"}}
|
||
result, err := e.reconsiderOperationalArticleType(context.Background(), plan, content, nil, nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if result.Action != "reclassify" || result.ArticleType != "reference" {
|
||
t.Fatalf("unexpected reconsideration: %+v", result)
|
||
}
|
||
}
|