mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
test: add memory regression coverage for AI stores
This commit is contained in:
parent
ee0e89871d
commit
8bb89c4031
5 changed files with 419 additions and 0 deletions
108
internal/ai/approval/approval_memory_regression_test.go
Normal file
108
internal/ai/approval/approval_memory_regression_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package approval
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestApprovalStoreMemoryStability(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping memory regression in short mode")
|
||||
}
|
||||
|
||||
store, err := NewStore(StoreConfig{
|
||||
DataDir: t.TempDir(),
|
||||
DefaultTimeout: 2 * time.Minute,
|
||||
MaxApprovals: 50,
|
||||
DisablePersistence: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewStore: %v", err)
|
||||
}
|
||||
|
||||
approvalsPerCycle := 120
|
||||
executionsPerCycle := 60
|
||||
warmupCycles := 5
|
||||
measureCycles := 12
|
||||
|
||||
runCycle := func(offset int) {
|
||||
for i := 0; i < approvalsPerCycle; i++ {
|
||||
idx := offset + i
|
||||
req := &ApprovalRequest{
|
||||
ExecutionID: fmt.Sprintf("exec-%d", idx),
|
||||
ToolID: "pulse_exec",
|
||||
Command: "systemctl status nginx",
|
||||
TargetType: "vm",
|
||||
TargetID: fmt.Sprintf("vm-%02d", idx%20),
|
||||
Context: "status check",
|
||||
}
|
||||
if err := store.CreateApproval(req); err != nil {
|
||||
t.Fatalf("CreateApproval: %v", err)
|
||||
}
|
||||
|
||||
var updated *ApprovalRequest
|
||||
if idx%2 == 0 {
|
||||
approved, err := store.Approve(req.ID, "admin")
|
||||
if err != nil {
|
||||
t.Fatalf("Approve: %v", err)
|
||||
}
|
||||
updated = approved
|
||||
} else {
|
||||
denied, err := store.Deny(req.ID, "admin", "not needed")
|
||||
if err != nil {
|
||||
t.Fatalf("Deny: %v", err)
|
||||
}
|
||||
updated = denied
|
||||
}
|
||||
if updated != nil {
|
||||
decidedAt := time.Now().Add(-48 * time.Hour)
|
||||
updated.DecidedAt = &decidedAt
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < executionsPerCycle; i++ {
|
||||
idx := offset + i
|
||||
state := &ExecutionState{
|
||||
ID: fmt.Sprintf("exec-state-%d", idx),
|
||||
CreatedAt: time.Now().Add(-2 * time.Hour),
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour),
|
||||
Messages: []map[string]interface{}{{"role": "user", "content": "status"}},
|
||||
OriginalRequest: map[string]interface{}{"prompt": "status"},
|
||||
}
|
||||
if err := store.StoreExecution(state); err != nil {
|
||||
t.Fatalf("StoreExecution: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
store.CleanupExpired()
|
||||
}
|
||||
|
||||
for i := 0; i < warmupCycles; i++ {
|
||||
runCycle(i * approvalsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var baseline runtime.MemStats
|
||||
runtime.ReadMemStats(&baseline)
|
||||
|
||||
for i := 0; i < measureCycles; i++ {
|
||||
runCycle(100000 + i*approvalsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var after runtime.MemStats
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
if baseline.HeapAlloc > 0 {
|
||||
allowed := baseline.HeapAlloc + 5*1024*1024
|
||||
growthRatio := float64(after.HeapAlloc) / float64(baseline.HeapAlloc)
|
||||
if after.HeapAlloc > allowed && growthRatio > 1.25 {
|
||||
t.Fatalf("heap allocation grew too much: baseline=%d final=%d ratio=%.2f", baseline.HeapAlloc, after.HeapAlloc, growthRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
92
internal/ai/chat/session_memory_regression_test.go
Normal file
92
internal/ai/chat/session_memory_regression_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSessionStoreMemoryStability(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping memory regression in short mode")
|
||||
}
|
||||
|
||||
store, err := NewSessionStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("NewSessionStore: %v", err)
|
||||
}
|
||||
|
||||
sessionsPerCycle := 40
|
||||
messagesPerSession := 6
|
||||
warmupCycles := 4
|
||||
measureCycles := 10
|
||||
|
||||
runCycle := func(offset int) {
|
||||
for i := 0; i < sessionsPerCycle; i++ {
|
||||
session, err := store.Create()
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
for j := 0; j < messagesPerSession; j++ {
|
||||
msg := Message{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf("message %d", offset+j),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
if err := store.AddMessage(session.ID, msg); err != nil {
|
||||
t.Fatalf("AddMessage: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
store.SetToolSet(session.ID, map[string]bool{"pulse_query": true})
|
||||
|
||||
res := &ResolvedResource{
|
||||
Kind: "vm",
|
||||
ProviderUID: fmt.Sprintf("vm-%02d", i),
|
||||
ResourceID: fmt.Sprintf("vm:%02d", i),
|
||||
Name: fmt.Sprintf("vm-%02d", i),
|
||||
ResourceType: "vm",
|
||||
}
|
||||
store.AddResolvedResource(session.ID, res.Name, res)
|
||||
|
||||
ka := store.GetKnowledgeAccumulator(session.ID)
|
||||
ka.SetTurn(1)
|
||||
ka.AddFact(FactCategoryResource, fmt.Sprintf("vm:%02d:status", i), "running")
|
||||
|
||||
store.ClearSessionState(session.ID, false)
|
||||
|
||||
if err := store.Delete(session.ID); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < warmupCycles; i++ {
|
||||
runCycle(i * sessionsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var baseline runtime.MemStats
|
||||
runtime.ReadMemStats(&baseline)
|
||||
|
||||
for i := 0; i < measureCycles; i++ {
|
||||
runCycle(100000 + i*sessionsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var after runtime.MemStats
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
if baseline.HeapAlloc > 0 {
|
||||
allowed := baseline.HeapAlloc + 5*1024*1024
|
||||
growthRatio := float64(after.HeapAlloc) / float64(baseline.HeapAlloc)
|
||||
if after.HeapAlloc > allowed && growthRatio > 1.25 {
|
||||
t.Fatalf("heap allocation grew too much: baseline=%d final=%d ratio=%.2f", baseline.HeapAlloc, after.HeapAlloc, growthRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
77
internal/ai/memory/change_detector_memory_regression_test.go
Normal file
77
internal/ai/memory/change_detector_memory_regression_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestChangeDetectorMemoryStability(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping memory regression in short mode")
|
||||
}
|
||||
|
||||
detector := NewChangeDetector(ChangeDetectorConfig{
|
||||
MaxChanges: 50,
|
||||
})
|
||||
|
||||
resourcesPerCycle := 120
|
||||
warmupCycles := 4
|
||||
measureCycles := 12
|
||||
|
||||
buildSnapshots := func(offset int, status string) []ResourceSnapshot {
|
||||
now := time.Now()
|
||||
snapshots := make([]ResourceSnapshot, 0, resourcesPerCycle)
|
||||
for i := 0; i < resourcesPerCycle; i++ {
|
||||
snapshots = append(snapshots, ResourceSnapshot{
|
||||
ID: fmt.Sprintf("vm-%03d", i),
|
||||
Name: fmt.Sprintf("vm-%03d", i),
|
||||
Type: "vm",
|
||||
Status: status,
|
||||
Node: fmt.Sprintf("node-%d", i%3),
|
||||
CPUCores: 2 + ((i + offset) % 4),
|
||||
MemoryBytes: int64(2+(i+offset)%4) * 1024 * 1024 * 1024,
|
||||
DiskBytes: int64(20+i%10) * 1024 * 1024 * 1024,
|
||||
LastBackup: now.Add(-time.Duration((i+offset)%5) * time.Hour),
|
||||
SnapshotTime: now,
|
||||
})
|
||||
}
|
||||
return snapshots
|
||||
}
|
||||
|
||||
for i := 0; i < warmupCycles; i++ {
|
||||
status := "running"
|
||||
if i%2 == 1 {
|
||||
status = "stopped"
|
||||
}
|
||||
detector.DetectChanges(buildSnapshots(i*resourcesPerCycle, status))
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var baseline runtime.MemStats
|
||||
runtime.ReadMemStats(&baseline)
|
||||
|
||||
for i := 0; i < measureCycles; i++ {
|
||||
status := "running"
|
||||
if i%2 == 1 {
|
||||
status = "stopped"
|
||||
}
|
||||
detector.DetectChanges(buildSnapshots(100000+i*resourcesPerCycle, status))
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var after runtime.MemStats
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
if baseline.HeapAlloc > 0 {
|
||||
allowed := baseline.HeapAlloc + 5*1024*1024
|
||||
growthRatio := float64(after.HeapAlloc) / float64(baseline.HeapAlloc)
|
||||
if after.HeapAlloc > allowed && growthRatio > 1.25 {
|
||||
t.Fatalf("heap allocation grew too much: baseline=%d final=%d ratio=%.2f", baseline.HeapAlloc, after.HeapAlloc, growthRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
73
internal/ai/memory/remediation_memory_regression_test.go
Normal file
73
internal/ai/memory/remediation_memory_regression_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRemediationLogMemoryStability(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping memory regression in short mode")
|
||||
}
|
||||
|
||||
log := NewRemediationLog(RemediationLogConfig{
|
||||
MaxRecords: 80,
|
||||
})
|
||||
|
||||
recordsPerCycle := 200
|
||||
warmupCycles := 4
|
||||
measureCycles := 12
|
||||
|
||||
recordCycle := func(offset int) {
|
||||
for i := 0; i < recordsPerCycle; i++ {
|
||||
idx := offset + i
|
||||
err := log.Log(RemediationRecord{
|
||||
ResourceID: fmt.Sprintf("vm-%02d", idx%20),
|
||||
ResourceType: "vm",
|
||||
ResourceName: fmt.Sprintf("vm-%02d", idx%20),
|
||||
Problem: "memory spike",
|
||||
Summary: "restart service",
|
||||
Action: "systemctl restart app",
|
||||
Output: "ok",
|
||||
Outcome: OutcomeResolved,
|
||||
Duration: 2 * time.Second,
|
||||
Automatic: idx%2 == 0,
|
||||
Timestamp: time.Now().Add(-time.Duration(idx%60) * time.Second),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Log: %v", err)
|
||||
}
|
||||
}
|
||||
_ = log.GetForResource("vm-01", 5)
|
||||
_ = log.GetSimilar("memory spike", 5)
|
||||
}
|
||||
|
||||
for i := 0; i < warmupCycles; i++ {
|
||||
recordCycle(i * recordsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var baseline runtime.MemStats
|
||||
runtime.ReadMemStats(&baseline)
|
||||
|
||||
for i := 0; i < measureCycles; i++ {
|
||||
recordCycle(100000 + i*recordsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var after runtime.MemStats
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
if baseline.HeapAlloc > 0 {
|
||||
allowed := baseline.HeapAlloc + 5*1024*1024
|
||||
growthRatio := float64(after.HeapAlloc) / float64(baseline.HeapAlloc)
|
||||
if after.HeapAlloc > allowed && growthRatio > 1.25 {
|
||||
t.Fatalf("heap allocation grew too much: baseline=%d final=%d ratio=%.2f", baseline.HeapAlloc, after.HeapAlloc, growthRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
internal/ai/patrol_history_memory_regression_test.go
Normal file
69
internal/ai/patrol_history_memory_regression_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package ai
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPatrolRunHistoryMemoryStability(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping memory regression in short mode")
|
||||
}
|
||||
|
||||
store := NewPatrolRunHistoryStore(60)
|
||||
|
||||
runsPerCycle := 150
|
||||
warmupCycles := 4
|
||||
measureCycles := 12
|
||||
|
||||
addCycle := func(offset int) {
|
||||
for i := 0; i < runsPerCycle; i++ {
|
||||
idx := offset + i
|
||||
started := time.Now().Add(-time.Duration(idx%120) * time.Second)
|
||||
completed := started.Add(2 * time.Second)
|
||||
store.Add(PatrolRunRecord{
|
||||
ID: fmt.Sprintf("run-%d", idx),
|
||||
StartedAt: started,
|
||||
CompletedAt: completed,
|
||||
Duration: completed.Sub(started),
|
||||
DurationMs: int64(completed.Sub(started) / time.Millisecond),
|
||||
Type: "patrol",
|
||||
TriggerReason: "scheduled",
|
||||
ResourcesChecked: 10,
|
||||
NewFindings: idx % 3,
|
||||
Status: "healthy",
|
||||
FindingsSummary: "All healthy",
|
||||
})
|
||||
}
|
||||
_ = store.GetRecent(10)
|
||||
}
|
||||
|
||||
for i := 0; i < warmupCycles; i++ {
|
||||
addCycle(i * runsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var baseline runtime.MemStats
|
||||
runtime.ReadMemStats(&baseline)
|
||||
|
||||
for i := 0; i < measureCycles; i++ {
|
||||
addCycle(100000 + i*runsPerCycle)
|
||||
}
|
||||
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
var after runtime.MemStats
|
||||
runtime.ReadMemStats(&after)
|
||||
|
||||
if baseline.HeapAlloc > 0 {
|
||||
allowed := baseline.HeapAlloc + 5*1024*1024
|
||||
growthRatio := float64(after.HeapAlloc) / float64(baseline.HeapAlloc)
|
||||
if after.HeapAlloc > allowed && growthRatio > 1.25 {
|
||||
t.Fatalf("heap allocation grew too much: baseline=%d final=%d ratio=%.2f", baseline.HeapAlloc, after.HeapAlloc, growthRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue