mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 19:41:17 +00:00
300 lines
8.3 KiB
Go
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)
|
|
}
|
|
}
|