test: add memory regression coverage for AI stores

This commit is contained in:
rcourtman 2026-02-04 19:56:12 +00:00
parent ee0e89871d
commit 8bb89c4031
5 changed files with 419 additions and 0 deletions

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

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

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

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

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