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