package storagescenarios import ( "context" "encoding/json" "errors" "os" "path/filepath" "runtime" "testing" "time" "github.com/itworx/pulse/internal/array" "github.com/itworx/pulse/internal/datasource" "github.com/itworx/pulse/internal/disk" "github.com/itworx/pulse/internal/pool" "github.com/itworx/pulse/internal/reconciliation" ) type scenario struct { ID string `json:"id"` Timeline []json.RawMessage `json:"timeline"` ExpectedOutcomes []json.RawMessage `json:"expectedOutcomes"` } func fixture(t *testing.T, name string) scenario { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("could not locate scenario test") } content, err := os.ReadFile(filepath.Join(filepath.Dir(file), "..", "..", "fixtures", "scenarios", name)) if err != nil { t.Fatal(err) } var got scenario if err := json.Unmarshal(content, &got); err != nil { t.Fatal(err) } if len(got.Timeline) == 0 || len(got.ExpectedOutcomes) == 0 { t.Fatalf("fixture %s has no executable expectations", name) } return got } func TestStorageFailureScenarioSuite(t *testing.T) { for _, name := range []string{"array-degraded.json", "smart-warning.json", "disk-temperature.json", "pool-capacity-pressure.json", "prometheus-stale.json"} { scenario := fixture(t, name) if scenario.ID == "" { t.Fatalf("fixture %s has no stable id", name) } } now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) arraySnapshot, err := array.Normalize(array.RawSnapshot{ Source: array.Source{ID: "fixture-array", Type: "fixture"}, State: array.StateDegraded, Parity: array.RawParity{Present: true, State: "failed"}, Members: []array.RawMember{{ID: "disk-1", Name: "Disk 1", Role: "data", State: "emulated", CapacityBytes: 100}}, ObservedAt: now, ReceivedAt: now, }, now, array.Limits{}, array.Policy{}) if err != nil || arraySnapshot.State != array.StateDegraded || arraySnapshot.Parity.State != "failed" { t.Fatalf("array scenario=%+v err=%v", arraySnapshot, err) } diskSnapshot, err := disk.Normalize(disk.RawSnapshot{ Source: disk.Source{ID: "fixture-disks", Type: "fixture"}, Disks: []disk.RawDisk{{ID: "disk-smart", Name: "Disk SMART", SizeBytes: 100, SMART: &disk.RawSMART{Available: true, Overall: disk.SMARTPassed, ObservedAt: now, Attributes: []disk.RawSMARTAttribute{{ID: "197", Name: "Current Pending Sector", RawValue: 2}}}, Temperature: &disk.RawTemperature{Available: true, Celsius: 52, ObservedAt: now}}}, ObservedAt: now, ReceivedAt: now, }, now, disk.Limits{}, disk.Policy{}) if err != nil || diskSnapshot.Disks[0].SMART.Overall != disk.SMARTAttention || diskSnapshot.Disks[0].Temperature.Status != "attention" { t.Fatalf("disk scenario=%+v err=%v", diskSnapshot, err) } poolSnapshot, err := pool.Normalize(pool.RawSnapshot{ Source: pool.Source{ID: "fixture-pools", Type: "fixture"}, Pools: []pool.RawPool{{ID: "cache", Name: "Cache", Filesystem: "btrfs", State: pool.StateHealthy, UsableBytes: 1000, UsedBytes: 950, Capabilities: pool.Capabilities{Capacity: pool.CapabilityAvailable}}}, ObservedAt: now, ReceivedAt: now, }, now, pool.Limits{}, pool.Policy{}) if err != nil || poolSnapshot.Pools[0].FreeBytes != 50 || poolSnapshot.Pools[0].UtilizationPercent != 95 { t.Fatalf("pool scenario=%+v err=%v", poolSnapshot, err) } previous := []reconciliation.Existing{{EntityID: reconciliation.StableEntityID("source", "container", "abc"), SourceID: "source", ExternalType: "container", ExternalID: "abc"}} result, err := reconciliation.Reconcile("source", datasource.HealthUnknown, nil, previous, nil, now) if err != nil || len(result.Entities) != 1 || len(result.Tombstoned) != 0 { t.Fatalf("source outage removed inventory: %+v err=%v", result, err) } } func TestStorageFalseGreenAndCancellationScenarios(t *testing.T) { now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) stale, err := disk.Normalize(disk.RawSnapshot{Source: disk.Source{ID: "fixture-disks", Type: "fixture"}, Disks: []disk.RawDisk{{ID: "disk", Name: "Disk", SizeBytes: 100, UsedBytes: 40, SMART: &disk.RawSMART{Available: true, Overall: disk.SMARTPassed, ObservedAt: now.Add(-2 * time.Hour)}}}, ObservedAt: now.Add(-2 * time.Hour), ReceivedAt: now.Add(-2 * time.Hour)}, now, disk.Limits{}, disk.Policy{FreshnessMaxAge: time.Minute, SMARTFreshnessMaxAge: time.Hour}) if err != nil || stale.Source.State != disk.StateUnknown || stale.Disks[0].SMART.State != disk.SMARTUnknown { t.Fatalf("stale data became green: %+v err=%v", stale, err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := (array.Adapter{}).Snapshot(ctx); !errors.Is(err, context.Canceled) { t.Fatalf("array cancellation err=%v", err) } if _, err := (disk.Adapter{}).Snapshot(ctx); !errors.Is(err, context.Canceled) { t.Fatalf("disk cancellation err=%v", err) } if _, err := (pool.Adapter{}).Snapshot(ctx); !errors.Is(err, context.Canceled) { t.Fatalf("pool cancellation err=%v", err) } }