Add complete TUI application for monitoring Kubernetes clusters and host systems. Features include: Core features: - Collector framework with concurrent scheduling - Host collectors: disk, memory, load, network - Kubernetes collectors: pods, nodes, workloads, events with informers - Issue deduplication, state management, and resolve-after logic - Bubble Tea TUI with table view, details pane, and filtering - JSON export functionality UX improvements: - Help overlay with keybindings - Priority/category filters with visual indicators - Direct priority jump (0/1/2/3) - Bulk acknowledge (Shift+A) - Clipboard copy (y) - Theme toggle (T) - Age format toggle (d) - Wide title toggle (t) - Vi-style navigation (j/k) - Home/End jump (g/G) - Rollup drill-down in details Robustness: - Grace period for unreachable clusters - Rollups for high-volume issues - Flap suppression - RBAC error handling Files: All core application code with tests for host collectors, engine, store, model, and export packages.
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"tower/internal/model"
|
|
)
|
|
|
|
func TestStore_Upsert_DedupAndTimestamps(t *testing.T) {
|
|
now1 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
now2 := now1.Add(5 * time.Second)
|
|
|
|
s := New(30 * time.Second)
|
|
|
|
// Same ID twice in one Upsert should dedupe.
|
|
s.Upsert(now1, []model.Issue{
|
|
{ID: "i-1", Title: "first"},
|
|
{ID: "i-1", Title: "should be ignored"},
|
|
})
|
|
|
|
snap1 := s.Snapshot(now1)
|
|
if len(snap1) != 1 {
|
|
t.Fatalf("expected 1 issue, got %d", len(snap1))
|
|
}
|
|
if snap1[0].ID != "i-1" {
|
|
t.Fatalf("expected id i-1, got %q", snap1[0].ID)
|
|
}
|
|
if !snap1[0].FirstSeen.Equal(now1) {
|
|
t.Fatalf("expected FirstSeen=%v, got %v", now1, snap1[0].FirstSeen)
|
|
}
|
|
if !snap1[0].LastSeen.Equal(now1) {
|
|
t.Fatalf("expected LastSeen=%v, got %v", now1, snap1[0].LastSeen)
|
|
}
|
|
if snap1[0].State != model.StateOpen {
|
|
t.Fatalf("expected State=Open, got %q", snap1[0].State)
|
|
}
|
|
|
|
// Subsequent Upsert for same ID should preserve FirstSeen and update LastSeen.
|
|
s.Upsert(now2, []model.Issue{{ID: "i-1", Title: "updated"}})
|
|
snap2 := s.Snapshot(now2)
|
|
if len(snap2) != 1 {
|
|
t.Fatalf("expected 1 issue, got %d", len(snap2))
|
|
}
|
|
if !snap2[0].FirstSeen.Equal(now1) {
|
|
t.Fatalf("expected FirstSeen to remain %v, got %v", now1, snap2[0].FirstSeen)
|
|
}
|
|
if !snap2[0].LastSeen.Equal(now2) {
|
|
t.Fatalf("expected LastSeen=%v, got %v", now2, snap2[0].LastSeen)
|
|
}
|
|
}
|
|
|
|
func TestStore_AckPreservedWhilePresent(t *testing.T) {
|
|
now1 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
now2 := now1.Add(1 * time.Second)
|
|
|
|
s := New(30 * time.Second)
|
|
s.Upsert(now1, []model.Issue{{ID: "i-1", Title: "t"}})
|
|
|
|
s.Acknowledge("i-1")
|
|
|
|
// Upsert again while present should remain Acked.
|
|
s.Upsert(now2, []model.Issue{{ID: "i-1", Title: "t2"}})
|
|
snap := s.Snapshot(now2)
|
|
if len(snap) != 1 {
|
|
t.Fatalf("expected 1 issue, got %d", len(snap))
|
|
}
|
|
if snap[0].State != model.StateAcknowledged {
|
|
t.Fatalf("expected State=Acknowledged, got %q", snap[0].State)
|
|
}
|
|
|
|
s.Unacknowledge("i-1")
|
|
snap2 := s.Snapshot(now2)
|
|
if snap2[0].State != model.StateOpen {
|
|
t.Fatalf("expected State=Open after unack, got %q", snap2[0].State)
|
|
}
|
|
}
|
|
|
|
func TestStore_ResolvesOnlyAfterAbsenceWindow(t *testing.T) {
|
|
resolveAfter := 10 * time.Second
|
|
now0 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
s := New(resolveAfter)
|
|
s.Upsert(now0, []model.Issue{{ID: "i-1", Title: "t"}})
|
|
|
|
// Miss a tick shortly after; should not resolve due to flap suppression / window.
|
|
s.Upsert(now0.Add(1*time.Second), nil)
|
|
snap1 := s.Snapshot(now0.Add(9 * time.Second))
|
|
if len(snap1) != 1 {
|
|
t.Fatalf("expected 1 issue, got %d", len(snap1))
|
|
}
|
|
if snap1[0].State != model.StateOpen {
|
|
t.Fatalf("expected still Open before resolveAfter, got %q", snap1[0].State)
|
|
}
|
|
|
|
// Still absent beyond resolveAfter => should resolve.
|
|
snap2 := s.Snapshot(now0.Add(11 * time.Second))
|
|
if snap2[0].State != model.StateResolved {
|
|
t.Fatalf("expected Resolved after absence > resolveAfter, got %q", snap2[0].State)
|
|
}
|
|
}
|