Files
glpi-neural-brain/internal/engine/research_work_test.go
2026-08-07 17:05:03 +02:00

121 lines
5.4 KiB
Go

package engine
import (
"context"
"errors"
"testing"
"time"
"github.com/local/glpi-neural-brain/internal/config"
"github.com/local/glpi-neural-brain/internal/model"
)
func TestResearchIntentDedupeReusesWithinNamespace(t *testing.T) {
e := &Engine{Cfg: config.Config{ResearchDedupeThreshold: .8, ResearchDedupeTTL: time.Hour}, researchDedupe: map[string]*researchDedupeEntry{}}
owner, _, err := e.beginResearchIntent(context.Background(), "evidence", "Azure MFA ransomware prevention")
if err != nil || !owner.owner {
t.Fatalf("expected owner lease, lease=%+v err=%v", owner, err)
}
e.completeResearchIntent(owner, []model.ResearchResult{{URL: "https://example.com/azure-mfa", Title: "Azure MFA"}}, nil)
reusedLease, reused, err := e.beginResearchIntent(context.Background(), "evidence", "Azure MFA ransomware prevention")
if err != nil {
t.Fatal(err)
}
if reusedLease.owner || len(reused) != 1 {
t.Fatalf("expected cached evidence reuse, lease=%+v results=%+v", reusedLease, reused)
}
relationLease, relationReuse, err := e.beginResearchIntent(context.Background(), "relation", "Azure MFA ransomware prevention")
if err != nil {
t.Fatal(err)
}
if !relationLease.owner || len(relationReuse) != 0 {
t.Fatalf("dedupe namespaces must not cross, lease=%+v results=%+v", relationLease, relationReuse)
}
e.completeResearchIntent(relationLease, nil, nil)
}
func TestResearchIntentWaiterRetriesAfterOwnerFailure(t *testing.T) {
e := &Engine{Cfg: config.Config{ResearchDedupeThreshold: .8, ResearchDedupeTTL: time.Hour}, researchDedupe: map[string]*researchDedupeEntry{}}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
owner, _, err := e.beginResearchIntent(ctx, "evidence", "forensic evidence preservation")
if err != nil || !owner.owner {
t.Fatalf("expected owner lease, lease=%+v err=%v", owner, err)
}
type result struct {
lease researchIntentLease
err error
}
waiter := make(chan result, 1)
go func() {
lease, _, err := e.beginResearchIntent(ctx, "evidence", "forensic evidence preservation")
waiter <- result{lease: lease, err: err}
}()
// Give the duplicate enough time to enter the in-flight wait path.
time.Sleep(20 * time.Millisecond)
e.completeResearchIntent(owner, nil, errors.New("temporary search failure"))
got := <-waiter
if got.err != nil {
t.Fatalf("waiter should retry instead of inheriting owner failure: %v", got.err)
}
if !got.lease.owner {
t.Fatalf("waiter should become the new owner after failed owner, lease=%+v", got.lease)
}
e.completeResearchIntent(got.lease, nil, nil)
}
func TestResearchIntentGuardRejectsAuthenticationVsEvidencePreservation(t *testing.T) {
evidence := classifyResearchIntent("Welche Schritte sind notwendig, um flüchtige Daten bei Mobile Authentication vor Neustarts zu sichern?")
auth := classifyResearchIntent("Wie kann Mobile Authentication in Microsoft Entra konfiguriert werden?")
if evidence.Primary != "evidence-preservation" || auth.Primary != "authentication" {
t.Fatalf("unexpected intent profiles: evidence=%+v auth=%+v", evidence, auth)
}
if ok, reason := researchIntentProfilesCompatible(evidence, auth); ok || reason != "primary_intent_mismatch" {
t.Fatalf("forensic preservation must not dedupe to authentication/configuration: ok=%t reason=%s", ok, reason)
}
}
func TestResearchIntentGuardRejectsDifferentForensicFocus(t *testing.T) {
hash := classifyResearchIntent("How are hash checksums created in forensic evidence preservation for Mobile Authentication?")
volatile := classifyResearchIntent("What steps preserve volatile evidence before reboot for Mobile Authentication?")
if hash.Primary != "evidence-preservation" || volatile.Primary != "evidence-preservation" {
t.Fatalf("expected same high-level intent: hash=%+v volatile=%+v", hash, volatile)
}
if ok, reason := researchIntentProfilesCompatible(hash, volatile); ok || reason != "technical_focus_mismatch" {
t.Fatalf("different forensic sub-focus must not dedupe: ok=%t reason=%s hash=%+v volatile=%+v", ok, reason, hash, volatile)
}
}
func TestResearchIntentGuardKeepsGermanEnglishParaphraseCompatible(t *testing.T) {
de := classifyResearchIntent("Welche Schritte sind notwendig, um flüchtige Daten bei Mobile Authentication vor Neustarts zu sichern?")
en := classifyResearchIntent("What steps are necessary to preserve volatile data in Mobile Authentication before reboots?")
if ok, reason := researchIntentProfilesCompatible(de, en); !ok {
t.Fatalf("DE/EN paraphrase should remain dedupe-compatible: reason=%s de=%+v en=%+v", reason, de, en)
}
}
func TestResearchIntentDedupeDoesNotReuseIncompatibleIntentEvenAtLowThreshold(t *testing.T) {
e := &Engine{Cfg: config.Config{ResearchDedupeThreshold: .1, ResearchDedupeTTL: time.Hour}, researchDedupe: map[string]*researchDedupeEntry{}}
owner, _, err := e.beginResearchIntent(context.Background(), "evidence", "Mobile Authentication forensic evidence preservation")
if err != nil || !owner.owner {
t.Fatalf("expected owner: lease=%+v err=%v", owner, err)
}
e.completeResearchIntent(owner, []model.ResearchResult{{URL: "https://example.com/evidence", Title: "Evidence"}}, nil)
other, reused, err := e.beginResearchIntent(context.Background(), "evidence", "Mobile Authentication configuration")
if err != nil {
t.Fatal(err)
}
if !other.owner || len(reused) != 0 {
t.Fatalf("intent guard should force a fresh owner despite lexical overlap: lease=%+v reused=%+v", other, reused)
}
e.completeResearchIntent(other, nil, nil)
}