124 lines
6.4 KiB
Go
124 lines
6.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/local/glpi-neural-brain/internal/config"
|
|
"github.com/local/glpi-neural-brain/internal/model"
|
|
"github.com/local/glpi-neural-brain/internal/ollama"
|
|
)
|
|
|
|
func TestGenerateThenReviewUsesSeparateModels(t *testing.T) {
|
|
var requested []string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/tags":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"models": []map[string]any{
|
|
{"name": "qwen3:8b", "digest": "review"},
|
|
{"name": "gemma3:12b", "digest": "author"},
|
|
{"name": "embeddinggemma:latest", "digest": "embed"},
|
|
}})
|
|
case "/api/chat":
|
|
var body map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
modelName, _ := body["model"].(string)
|
|
requested = append(requested, modelName)
|
|
content := ""
|
|
if modelName == "gemma3:12b" {
|
|
content = `{"title":"VPN prüfen","problem_description":"Der VPN-Tunnel wird nicht aufgebaut.","scope":"Dokumentierter VPN-Client.","symptoms":["Tunnel bleibt getrennt."],"key_points":[],"decision_criteria":[],"prerequisites":["Fehlermeldung liegt vor."],"solution_steps":["Prüfen Sie die Gateway-Adresse."],"validation_steps":["Der Tunnel wird aufgebaut."],"troubleshooting":[],"categories":["VPN"],"keywords":["VPN","Gateway"],"open_questions":[],"research_needed":false,"research_queries":[],"freshness_sensitive":false,"research_reason":""}`
|
|
} else {
|
|
content = `{"accepted":true,"confidence":0.94,"coverage_complete":true,"coverage_score":0.92,"missing_topics":[],"coverage_issues":[],"research_use_justification":"Die bereitgestellten Quellen decken die Testaussagen vollständig ab.","meta_content_detected":false,"unsupported_claims":[],"issues":[],"claim_reviews":[{"claim":"Gateway-Adresse prüfen","verdict":"supported","source_refs":["SOURCE-1"],"reason":"Direkt im internen Beleg genannt."}],"missing_evidence_queries":[],"rewrite_instructions":[]}`
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": content}})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := ollama.NewPool(ollama.PoolConfig{Nodes: []ollama.NodeConfig{{Name: "only", URL: srv.URL}}, NodeMaxInflight: 1, HealthInterval: time.Minute, RequestTimeout: time.Second, RequireSameModelDigest: true, RequireEmbeddingModel: true}, "qwen3:8b", "embeddinggemma")
|
|
if err := client.Ping(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e := &Engine{Cfg: config.Config{ArticleLanguage: "de-DE", ArticleSynthesisModel: "gemma3:12b", ArticleReviewModel: "qwen3:8b", MaxContextChars: 8000}, Ollama: client}
|
|
sources := []articleSource{{Node: model.Node{ID: "SOURCE-1", Kind: "knowledge", Status: "production", Label: "VPN Gateway", Origin: "knowledge-production"}, Content: "Prüfen Sie die konfigurierte Gateway-Adresse und anschließend den Tunnelaufbau."}}
|
|
plan := model.ArticlePlanDecision{Action: "create", ArticleType: "troubleshooting", SourceNodeIDs: []string{"SOURCE-1"}}
|
|
brief := model.KnowledgeBrief{Topic: "VPN Gateway", Purpose: "VPN-Verbindung wiederherstellen", ReadyForArticle: false}
|
|
content, _, err := e.generateArticleContent(context.Background(), "test-article-run", sources, plan, brief, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
draft := articleContentToDraft(content, plan.SourceNodeIDs, plan.ArticleType)
|
|
quality, _, err := e.reviewArticleContent(context.Background(), draft, plan.ArticleType, sources, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !quality.Accepted || len(quality.ClaimReviews) != 1 {
|
|
t.Fatalf("unexpected quality decision: %+v", quality)
|
|
}
|
|
if !reflect.DeepEqual(requested, []string{"gemma3:12b", "qwen3:8b"}) {
|
|
t.Fatalf("model sequence=%v", requested)
|
|
}
|
|
}
|
|
|
|
func TestArticleClaimReviewCounts(t *testing.T) {
|
|
got := articleClaimReviewCounts([]model.ArticleClaimReview{
|
|
{Verdict: "supported"}, {Verdict: "supported"}, {Verdict: "partially_supported"}, {Verdict: "unsupported"}, {Verdict: "contradicted"},
|
|
})
|
|
want := map[string]int{"supported": 2, "partially_supported": 1, "unsupported": 1, "contradicted": 1}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("got=%v want=%v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestReviewedResearchEvidenceUsesOnlySupportedReferences(t *testing.T) {
|
|
results := []model.ResearchResult{
|
|
{Title: "A", URL: "https://example.com/a"},
|
|
{Title: "B", URL: "https://example.com/b"},
|
|
{Title: "C", URL: "https://example.com/c"},
|
|
}
|
|
reviews := []model.ArticleClaimReview{
|
|
{Claim: "supported by first", Verdict: "supported", SourceRefs: []string{"R1"}},
|
|
{Claim: "supported by URL", Verdict: "partially_supported", SourceRefs: []string{"https://example.com/c"}},
|
|
{Claim: "bad claim", Verdict: "unsupported", SourceRefs: []string{"R2"}},
|
|
}
|
|
got := reviewedResearchEvidence(results, reviews)
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 grounded results, got %d: %#v", len(got), got)
|
|
}
|
|
if got[0].URL != "https://example.com/a" || got[1].URL != "https://example.com/c" {
|
|
t.Fatalf("unexpected grounded results: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestResearchResultValidationStateDistinguishesMaterialFromEvaluatedEvidence(t *testing.T) {
|
|
material := model.ResearchResult{AssessmentReason: "Volltextmaterial für die Artikelsynthese gesammelt; die fachliche Belegprüfung erfolgt anschließend am generierten Artikel."}
|
|
if got := researchResultValidationState(material); got != "pending_article_review" {
|
|
t.Fatalf("expected pending_article_review, got %q", got)
|
|
}
|
|
evaluated := model.ResearchResult{AssessmentReason: "Die Quelle schließt die Wissenslücke belastbar."}
|
|
if got := researchResultValidationState(evaluated); got != "evaluated" {
|
|
t.Fatalf("expected evaluated, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestArticleAuthorBriefExcludesPredraftReadinessGates(t *testing.T) {
|
|
brief := model.KnowledgeBrief{Topic: "VPN", ReadyForArticle: false, MissingInformation: []string{"fehlend"}, CriticalGaps: []model.KnowledgeGap{{ID: "KG-1", Description: "Gap"}}, ResearchQueries: []string{"query"}}
|
|
got := articleAuthorBrief(brief)
|
|
for _, forbidden := range []string{"ready_for_article", "missing_information", "critical_gaps", "optional_gaps", "research_queries"} {
|
|
if _, ok := got[forbidden]; ok {
|
|
t.Fatalf("author brief must not contain pre-draft gate %q", forbidden)
|
|
}
|
|
}
|
|
if got["topic"] != "VPN" {
|
|
t.Fatalf("expected factual topic to remain, got %#v", got["topic"])
|
|
}
|
|
}
|