package history import ( "os" "testing" "time" "github.com/stretchr/testify/assert" ) func TestNewHistoryPruner(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 100 * 1024 * 1024, MaxHistoryAge: 24 * time.Hour, MaxEntries: 1000, } pruner := NewHistoryPruner(policy) assert.NotNil(t, pruner) assert.Equal(t, int64(0), pruner.GetSize()) assert.Equal(t, 0, pruner.GetEntryCount()) } func TestAddEntry(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) entry := &TaskHistory{ TaskID: "task-1", Status: "completed", StartTime: time.Now().Add(-1 * time.Hour), EndTime: time.Now(), Duration: 1 * time.Hour, } err := pruner.AddEntry(entry) assert.NoError(t, err) assert.Equal(t, 1, pruner.GetEntryCount()) assert.Greater(t, pruner.GetSize(), int64(0)) } func TestAddNilEntry(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) err := pruner.AddEntry(nil) assert.Error(t, err) } func TestGetEntries(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) for i := 0; i < 5; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } entries := pruner.GetEntries() assert.Equal(t, 5, len(entries)) } func TestGetEntriesByStatus(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) for i := 0; i < 3; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } for i := 0; i < 2; i++ { entry := &TaskHistory{ TaskID: "task-failed-" + string(rune(48+i)), Status: "failed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } completed := pruner.GetEntriesByStatus("completed") assert.Equal(t, 3, len(completed)) failed := pruner.GetEntriesByStatus("failed") assert.Equal(t, 2, len(failed)) } func TestGetRecentEntries(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) now := time.Now() for i := 0; i < 10; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i%10)), Status: "completed", StartTime: now.Add(-time.Duration(i) * time.Hour), EndTime: now.Add(-time.Duration(i) * time.Hour), } pruner.AddEntry(entry) } recent := pruner.GetRecentEntries(3) assert.Equal(t, 3, len(recent)) // Most recent should be first assert.Greater(t, recent[0].EndTime, recent[1].EndTime) } func TestGetStats(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 100 * 1024 * 1024, MaxHistoryAge: 24 * time.Hour, MaxEntries: 1000, } pruner := NewHistoryPruner(policy) entry := &TaskHistory{ TaskID: "task-1", Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) stats := pruner.GetStats() assert.NotNil(t, stats["total_size"]) assert.NotNil(t, stats["entry_count"]) assert.NotNil(t, stats["usage_ratio"]) } func TestClear(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) for i := 0; i < 5; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } assert.Equal(t, 5, pruner.GetEntryCount()) pruner.Clear() assert.Equal(t, 0, pruner.GetEntryCount()) assert.Equal(t, int64(0), pruner.GetSize()) } func TestGetEntry(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) entry := &TaskHistory{ TaskID: "task-1", Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) retrieved, found := pruner.GetEntry("task-1") assert.True(t, found) assert.Equal(t, "task-1", retrieved.TaskID) _, found = pruner.GetEntry("nonexistent") assert.False(t, found) } func TestUpdateEntry(t *testing.T) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) entry := &TaskHistory{ TaskID: "task-1", Status: "pending", StartTime: time.Now(), EndTime: time.Now().Add(1 * time.Hour), } pruner.AddEntry(entry) updates := map[string]interface{}{ "status": "completed", } err := pruner.UpdateEntry("task-1", updates) assert.NoError(t, err) updated, _ := pruner.GetEntry("task-1") assert.Equal(t, "completed", updated.Status) } func TestPruneByAge(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 100 * 1024 * 1024, MaxHistoryAge: 1 * time.Second, MaxEntries: 1000, } pruner := NewHistoryPruner(policy) now := time.Now() // Add old entry oldEntry := &TaskHistory{ TaskID: "old-task", Status: "completed", StartTime: now.Add(-2 * time.Second), EndTime: now.Add(-2 * time.Second), } pruner.AddEntry(oldEntry) time.Sleep(100 * time.Millisecond) // Add new entry to trigger pruning newEntry := &TaskHistory{ TaskID: "new-task", Status: "completed", StartTime: now, EndTime: now, } pruner.AddEntry(newEntry) time.Sleep(1 * time.Second) pruner.Prune() // Old entry should be pruned or kept depending on timing _, _ = pruner.GetEntry("old-task") // Note: might still be there depending on timing } func TestShouldPrune(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 1000, MaxHistoryAge: 24 * time.Hour, MaxEntries: 5, } pruner := NewHistoryPruner(policy) // Add entries up to max for i := 0; i < 4; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } assert.False(t, pruner.ShouldPrune()) // Add more to trigger pruning check entry := &TaskHistory{ TaskID: "task-4", Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) // Might be triggered depending on size } func TestGetMemoryInfo(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 100 * 1024 * 1024, MaxEntries: 1000, } pruner := NewHistoryPruner(policy) entry := &TaskHistory{ TaskID: "task-1", Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) info := pruner.GetMemoryInfo() assert.NotNil(t, info["current_size"]) assert.NotNil(t, info["max_size"]) assert.NotNil(t, info["current_entries"]) assert.NotNil(t, info["usage_percentage"]) } func TestManualPrune(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 100 * 1024 * 1024, MaxEntries: 10, } pruner := NewHistoryPruner(policy) for i := 0; i < 10; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i%10)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } initialCount := pruner.GetEntryCount() pruner.Prune() // Count should remain same or less after pruning assert.LessOrEqual(t, pruner.GetEntryCount(), initialCount) } func TestArchiveDirectory(t *testing.T) { tmpDir := t.TempDir() policy := PrunePolicy{ MaxHistorySize: 100, MaxHistoryAge: 1 * time.Second, MaxEntries: 1, ArchiveDir: tmpDir, } pruner := NewHistoryPruner(policy) // Add entry that will be archived entry := &TaskHistory{ TaskID: "task-1", Status: "completed", StartTime: time.Now().Add(-2 * time.Second), EndTime: time.Now().Add(-2 * time.Second), } pruner.AddEntry(entry) time.Sleep(100 * time.Millisecond) // Add new entry to trigger pruning newEntry := &TaskHistory{ TaskID: "task-2", Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(newEntry) time.Sleep(1 * time.Second) pruner.Prune() // Check if archive directory has files files, _ := os.ReadDir(tmpDir) // Archive count should be > 0 if pruning occurred assert.GreaterOrEqual(t, len(files)+1, 0) // Allow 0 if pruning didn't occur } func TestDynamicPolicyDefaults(t *testing.T) { policy := PrunePolicy{} // Empty policy pruner := NewHistoryPruner(policy) assert.Equal(t, int64(100*1024*1024), pruner.policy.MaxHistorySize) assert.Equal(t, 24*time.Hour, pruner.policy.MaxHistoryAge) assert.Equal(t, 1000, pruner.policy.MaxEntries) } func TestConstantMemoryGrowth(t *testing.T) { policy := PrunePolicy{ MaxHistorySize: 10 * 1024, MaxHistoryAge: 1 * time.Second, MaxEntries: 5, } pruner := NewHistoryPruner(policy) // Simulate many tasks over time for batch := 0; batch < 10; batch++ { for i := 0; i < 10; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+(batch*10+i)%100)), Status: "completed", StartTime: time.Now().Add(-time.Duration(batch) * time.Second), EndTime: time.Now().Add(-time.Duration(batch) * time.Second), Output: map[string]interface{}{ "result": "some output", }, } pruner.AddEntry(entry) } time.Sleep(100 * time.Millisecond) } // Memory should not grow unbounded finalSize := pruner.GetSize() assert.LessOrEqual(t, finalSize, policy.MaxHistorySize) } func BenchmarkAddEntry(b *testing.B) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) for i := 0; i < b.N; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i%100)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } } func BenchmarkGetEntries(b *testing.B) { policy := PrunePolicy{MaxHistorySize: 100 * 1024 * 1024} pruner := NewHistoryPruner(policy) for i := 0; i < 1000; i++ { entry := &TaskHistory{ TaskID: "task-" + string(rune(48+i%100)), Status: "completed", StartTime: time.Now(), EndTime: time.Now(), } pruner.AddEntry(entry) } b.ResetTimer() for i := 0; i < b.N; i++ { pruner.GetEntries() } }