Pulse/internal/ai/memory/context_test.go
2026-01-25 21:08:44 +00:00

300 lines
8.3 KiB
Go

package memory
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)
func TestNewContextStore_Defaults(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
if store.config.MaxMemoriesPerType != 1000 {
t.Fatalf("expected MaxMemoriesPerType default, got %d", store.config.MaxMemoriesPerType)
}
if store.config.MaxResourceNotes != 20 {
t.Fatalf("expected MaxResourceNotes default, got %d", store.config.MaxResourceNotes)
}
if store.config.RetentionDays != 90 {
t.Fatalf("expected RetentionDays default, got %d", store.config.RetentionDays)
}
if store.config.RelevanceDecayDays != 7 {
t.Fatalf("expected RelevanceDecayDays default, got %d", store.config.RelevanceDecayDays)
}
}
func TestRemember_AddsMemoryAndResourceNote(t *testing.T) {
store := NewContextStore(ContextStoreConfig{MaxResourceNotes: 2})
id1 := store.Remember("vm-1", "note-1", "user", MemoryTypeResource)
id2 := store.Remember("vm-1", "note-1", "user", MemoryTypeResource)
if id1 == id2 {
t.Fatalf("expected unique memory IDs for separate remembers")
}
mem := store.GetResourceMemory("vm-1")
if mem == nil || len(mem.Notes) != 1 {
t.Fatalf("expected duplicate notes to be ignored")
}
store.AddResourceNote("vm-1", "", "", "note-2")
store.AddResourceNote("vm-1", "", "", "note-3")
mem = store.GetResourceMemory("vm-1")
if len(mem.Notes) != 2 {
t.Fatalf("expected notes trimmed to max, got %d", len(mem.Notes))
}
if mem.Notes[0] != "note-2" {
t.Fatalf("expected oldest note to be trimmed")
}
}
func TestAddResourceNote_UpdatesMetadata(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
store.AddResourceNote("vm-1", "web-1", "vm", "note")
mem := store.GetResourceMemory("vm-1")
if mem == nil {
t.Fatalf("expected resource memory to exist")
}
if mem.ResourceName != "web-1" || mem.ResourceType != "vm" {
t.Fatalf("expected metadata to be stored")
}
}
func TestAddIncidentMemory_CreatesMemory(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
store.AddIncidentMemory(&IncidentMemory{
ResourceID: "vm-1",
Summary: "high cpu",
RootCause: "runaway process",
Resolution: "restarted service",
})
incidents := store.GetRecentIncidents(10)
if len(incidents) != 1 {
t.Fatalf("expected incident to be stored")
}
store.mu.RLock()
defer store.mu.RUnlock()
if len(store.memories[MemoryTypeIncident]) != 1 {
t.Fatalf("expected incident memory to be created")
}
for _, mem := range store.memories[MemoryTypeIncident] {
if mem.Content == "" || mem.ResourceID != "vm-1" {
t.Fatalf("expected incident memory content to be populated")
}
}
}
func TestAddPatternMemory_Deduplicates(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
store.AddPatternMemory(&PatternMemory{
Pattern: "cpu spike at backup",
Description: "backup time spikes CPU",
Occurrences: 2,
})
store.AddPatternMemory(&PatternMemory{
Pattern: "cpu spike at backup",
Description: "same pattern",
Occurrences: 1,
})
patterns := store.GetPatterns(0)
if len(patterns) != 1 {
t.Fatalf("expected pattern to deduplicate")
}
if patterns[0].Occurrences != 3 {
t.Fatalf("expected occurrences to be incremented, got %d", patterns[0].Occurrences)
}
if patterns[0].Confidence == 0 {
t.Fatalf("expected confidence to be set")
}
}
func TestRecallAndRecallByType_SortsAndMarksUsed(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
store.mu.Lock()
store.memories[MemoryTypeResource]["m1"] = &Memory{
ID: "m1",
Type: MemoryTypeResource,
ResourceID: "vm-1",
Relevance: 0.5,
UseCount: 1,
LastUsed: time.Now().Add(-24 * time.Hour),
}
store.memories[MemoryTypeResource]["m2"] = &Memory{
ID: "m2",
Type: MemoryTypeResource,
ResourceID: "vm-1",
Relevance: 0.9,
UseCount: 1,
LastUsed: time.Now().Add(-24 * time.Hour),
}
store.mu.Unlock()
memories := store.Recall("vm-1")
if len(memories) != 2 {
t.Fatalf("expected 2 memories, got %d", len(memories))
}
if memories[0].ID != "m2" {
t.Fatalf("expected higher relevance memory first")
}
store.mu.RLock()
defer store.mu.RUnlock()
if store.memories[MemoryTypeResource]["m1"].UseCount != 2 {
t.Fatalf("expected use count increment")
}
if store.memories[MemoryTypeResource]["m1"].Relevance <= 0.5 {
t.Fatalf("expected relevance to increase")
}
}
func TestGetPatterns_FilterAndSort(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
store.mu.Lock()
store.patternMemories["p1"] = &PatternMemory{ID: "p1", Pattern: "a", Confidence: 0.4}
store.patternMemories["p2"] = &PatternMemory{ID: "p2", Pattern: "b", Confidence: 0.9}
store.patternMemories["p3"] = &PatternMemory{ID: "p3", Pattern: "c", Confidence: 0.7}
store.mu.Unlock()
patterns := store.GetPatterns(0.5)
if len(patterns) != 2 {
t.Fatalf("expected 2 patterns above threshold, got %d", len(patterns))
}
if patterns[0].Confidence < patterns[1].Confidence {
t.Fatalf("expected patterns sorted by confidence desc")
}
}
func TestDecayRelevance(t *testing.T) {
store := NewContextStore(ContextStoreConfig{RelevanceDecayDays: 1})
store.mu.Lock()
store.memories[MemoryTypeResource]["m1"] = &Memory{
ID: "m1",
Type: MemoryTypeResource,
ResourceID: "vm-1",
Relevance: 0.2,
LastUsed: time.Now().Add(-30 * 24 * time.Hour),
}
store.mu.Unlock()
store.DecayRelevance()
store.mu.RLock()
defer store.mu.RUnlock()
if store.memories[MemoryTypeResource]["m1"].Relevance != 0.1 {
t.Fatalf("expected relevance to decay to minimum, got %.2f", store.memories[MemoryTypeResource]["m1"].Relevance)
}
}
func TestCleanup_RemovesOldAndTrims(t *testing.T) {
store := NewContextStore(ContextStoreConfig{
MaxMemoriesPerType: 1,
RetentionDays: 1,
})
store.mu.Lock()
store.memories[MemoryTypeResource]["old"] = &Memory{
ID: "old",
Type: MemoryTypeResource,
CreatedAt: time.Now().Add(-48 * time.Hour),
Relevance: 0.9,
}
store.memories[MemoryTypeResource]["low"] = &Memory{
ID: "low",
Type: MemoryTypeResource,
CreatedAt: time.Now(),
Relevance: 0.05,
}
store.memories[MemoryTypeResource]["new"] = &Memory{
ID: "new",
Type: MemoryTypeResource,
CreatedAt: time.Now(),
Relevance: 0.8,
}
store.mu.Unlock()
removed := store.Cleanup()
if removed == 0 {
t.Fatalf("expected cleanup to remove memories")
}
store.mu.RLock()
defer store.mu.RUnlock()
if len(store.memories[MemoryTypeResource]) != 1 {
t.Fatalf("expected memories trimmed to max")
}
}
func TestFormatForPatrolAndResource(t *testing.T) {
store := NewContextStore(ContextStoreConfig{})
store.AddResourceNote("vm-1", "vm-one", "vm", "note-1")
store.AddPatternMemory(&PatternMemory{
Pattern: "pattern",
Description: "pattern desc",
Occurrences: 5,
})
store.AddIncidentMemory(&IncidentMemory{
ResourceID: "vm-1",
Summary: "disk full",
})
resource := store.GetResourceMemory("vm-1")
resource.Patterns = []string{"daily spike"}
store.mu.Lock()
store.resourceMemories["vm-1"] = resource
store.mu.Unlock()
context := store.FormatForPatrol()
if context == "" || !containsStr(context, "Resource Notes") || !containsStr(context, "Recent Incidents") {
t.Fatalf("expected patrol context to include sections")
}
resourceContext := store.FormatForResource("vm-1")
if resourceContext == "" || !containsStr(resourceContext, "Observed patterns") {
t.Fatalf("expected resource context to include patterns")
}
}
func TestContextStore_SaveLoad(t *testing.T) {
dir := t.TempDir()
store := NewContextStore(ContextStoreConfig{DataDir: dir})
store.Remember("vm-1", "note-1", "user", MemoryTypeResource)
store.AddPatternMemory(&PatternMemory{
Pattern: "pattern",
Description: "pattern desc",
Occurrences: 3,
})
if err := store.ForceSave(); err != nil {
t.Fatalf("force save failed: %v", err)
}
loaded := NewContextStore(ContextStoreConfig{DataDir: dir})
if loaded == nil {
t.Fatalf("expected store to load from disk")
}
if len(loaded.memories[MemoryTypeResource]) == 0 {
t.Fatalf("expected memories to load")
}
if len(loaded.patternMemories) == 0 {
t.Fatalf("expected patterns to load")
}
// Validate saved file exists and is JSON.
data, err := os.ReadFile(filepath.Join(dir, "ai_context.json"))
if err != nil {
t.Fatalf("expected context file to exist: %v", err)
}
var decoded map[string]interface{}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("expected valid json, got %v", err)
}
}