diff --git a/tests/Vantage.App.Tests/OverviewAndAlertWiringTests.cs b/tests/Vantage.App.Tests/OverviewAndAlertWiringTests.cs new file mode 100644 index 0000000..79281b3 --- /dev/null +++ b/tests/Vantage.App.Tests/OverviewAndAlertWiringTests.cs @@ -0,0 +1,132 @@ +using System.Collections.ObjectModel; +using Vantage.App.ViewModels; +using Vantage.Core.Alerts; +using Vantage.Core.Configuration; +using Vantage.Core.Docker; +using Vantage.Core.Health; +using Vantage.Core.History; +using Vantage.Core.Metrics; +using Vantage.Core.Models; +using Xunit; + +namespace Vantage.App.Tests; + +public class OverviewAndAlertWiringTests +{ + private sealed class FixedMetricsProvider : IMetricsProvider + { + private readonly double cpu; + + public FixedMetricsProvider(double cpu) + { + this.cpu = cpu; + } + + public Task GetMetricsAsync(HostConfig host, CancellationToken cancellationToken) => + Task.FromResult(new HostMetrics + { + CpuUsagePercent = cpu, + MemoryUsedBytes = 90, + MemoryTotalBytes = 100, + DiskUsedBytes = 10, + DiskTotalBytes = 100, + Load1 = 1, + CollectedAt = new DateTimeOffset(2026, 8, 26, 12, 0, 0, TimeSpan.Zero) + }); + } + + private static async Task SeededStore(AlertRule rule) + { + var store = new JsonConfigStore( + Path.Combine(Path.GetTempPath(), "vantage-wire-" + Guid.NewGuid().ToString("N"), "config.json")); + await store.SaveAsync(new VantageConfig + { + Hosts = { new HostConfig { Id = "h1", Name = "vps01" } }, + AlertRules = { rule } + }); + return store; + } + + [Fact] + public void Overview_AggregatesOnlineOfflineAndPeaks() + { + var hosts = new ObservableCollection + { + new(new HostConfig { Name = "a" }, 10), + new(new HostConfig { Name = "b" }, 10), + new(new HostConfig { Name = "c" }, 10) + }; + hosts[0].ApplyMetrics(new HostMetrics { CpuUsagePercent = 20, MemoryTotalBytes = 100, MemoryUsedBytes = 30 }); + hosts[1].ApplyMetrics(new HostMetrics { CpuUsagePercent = 80, MemoryTotalBytes = 100, MemoryUsedBytes = 60 }); + hosts[2].ApplyError("down"); + + var overview = new OverviewViewModel(hosts); + overview.Refresh(); + + Assert.Equal(3, overview.TotalHosts); + Assert.Equal(2, overview.OnlineHosts); + Assert.Equal(1, overview.OfflineHosts); + Assert.Equal(50, overview.AverageCpuPercent, 1); + Assert.Equal(80, overview.PeakCpuPercent, 1); + Assert.Equal("b", overview.PeakCpuHost); + } + + [Fact] + public async Task Refresh_RecordsHistoryAndFiresAlerts() + { + await using var store = SqliteMetricsStore.InMemory(); + var sink = new CollectingSink(); + var rule = new AlertRule { Name = "CPU", Metric = "cpu", Comparison = AlertComparison.GreaterOrEqual, Threshold = 80, For = TimeSpan.Zero }; + var engine = new AlertEngine(Array.Empty(), new IAlertSink[] { sink }); + + var viewModel = new MainWindowViewModel( + new FixedMetricsProvider(95), + new MockDockerProvider(), + new HealthChecker(new HttpClient()), + await SeededStore(rule), + store, + engine); + + await viewModel.InitializeAsync(); + await viewModel.RefreshSelectedHostAsync(CancellationToken.None); + + Assert.True(await store.CountAsync() >= 6); + Assert.Contains(sink.Received, n => n.Kind == AlertTransitionKind.Fired); + Assert.Equal(1, viewModel.FiringAlertCount); + Assert.NotEmpty(viewModel.Alerts); + } + + [Fact] + public async Task Refresh_BelowThreshold_DoesNotFire() + { + await using var store = SqliteMetricsStore.InMemory(); + var sink = new CollectingSink(); + var rule = new AlertRule { Name = "CPU", Metric = "cpu", Comparison = AlertComparison.GreaterOrEqual, Threshold = 80, For = TimeSpan.Zero }; + var engine = new AlertEngine(Array.Empty(), new IAlertSink[] { sink }); + + var viewModel = new MainWindowViewModel( + new FixedMetricsProvider(15), + new MockDockerProvider(), + new HealthChecker(new HttpClient()), + await SeededStore(rule), + store, + engine); + + await viewModel.InitializeAsync(); + await viewModel.RefreshSelectedHostAsync(CancellationToken.None); + + Assert.Empty(sink.Received); + Assert.Equal(0, viewModel.FiringAlertCount); + } + + private sealed class CollectingSink : IAlertSink + { + public List Received { get; } = new(); + + public Task NotifyAsync(AlertNotification notification, CancellationToken cancellationToken = default) + { + Received.Add(notification); + return Task.CompletedTask; + } + } +}