using ClawdDotNet.Core.Deploymentcenter.Watchdog; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Core.Tests.Deploymentcenter; public sealed class WatchdogHeartbeatServiceTests { /// Merkt sich, was gesendet wurde, ohne Netzwerk. private sealed class RecordingClient : IWatchdogClient { public List<(string Status, string? Message)> Heartbeats { get; } = []; public List<(string Kind, string? Message)> Events { get; } = []; public Task SendHeartbeatAsync( InstanceHealth health, int intervalSeconds, CancellationToken ct) { Heartbeats.Add((health.Status, health.Message)); return Task.FromResult(new WatchdogPingResult("up", [])); } public Task SendEventAsync( string kind, string severity, string? message, object? meta, CancellationToken ct) { Events.Add((kind, message)); return Task.CompletedTask; } } private sealed class FixedHealth(InstanceHealth health) : IInstanceHealthProvider { public Task GetAsync(CancellationToken ct) => Task.FromResult(health); } private static WatchdogHeartbeatService Build(RecordingClient client) => new(client, new FixedHealth(InstanceHealth.Ok("läuft")), 3600, NullLogger.Instance); [Fact] public async Task Regulaeres_Ende_meldet_stopped() { var client = new RecordingClient(); var service = Build(client); service.Start(); await service.DisposeAsync(); client.Heartbeats[^1].Status.ShouldBe(WatchdogStatus.Stopped); client.Events[^1].Kind.ShouldBe(WatchdogEventKind.StoppedGraceful); } /// /// Beim Update ist die Instanz gleich weg, kommt aber wieder. stopped wäre /// die falsche Auskunft: Der Monitor bliebe liegen, bis jemand ihn anfasst. /// [Fact] public async Task Angekuendigte_Wartung_meldet_maintenance() { var client = new RecordingClient(); var service = Build(client); service.Start(); service.AnnounceMaintenance("Update auf 0.1.1 wird eingespielt."); await service.DisposeAsync(); var last = client.Heartbeats[^1]; last.Status.ShouldBe(WatchdogStatus.Maintenance); last.Message.ShouldContain("0.1.1"); client.Events[^1].Kind.ShouldBe(WatchdogEventKind.MaintenanceStart); } /// Der erste Beat geht sofort raus, nicht erst nach einem vollen Intervall. [Fact] public async Task Erster_Heartbeat_kommt_sofort() { var client = new RecordingClient(); var service = Build(client); service.Start(); await service.DisposeAsync(); // Start-Ereignis, mindestens ein Beat, dann die Abmeldung. client.Events[0].Kind.ShouldBe(WatchdogEventKind.Started); client.Heartbeats.Count.ShouldBeGreaterThanOrEqualTo(2); client.Heartbeats[0].Status.ShouldBe(WatchdogStatus.Ok); } }