using ClawdDotNet.Core.Accounting; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Deploymentcenter.Watchdog; using Shouldly; namespace ClawdDotNet.Core.Tests.Deploymentcenter; public sealed class InstanceHealthProviderTests { /// Liefert einen festen Tagesverbrauch, ohne DB. private sealed class FakeUsage(DailyUsage daily) : IUsageRepository { public Task RecordAsync(RunUsage usage, CancellationToken ct) => Task.CompletedTask; public Task GetDailyAsync(DateOnly date, string agentId, CancellationToken ct) => Task.FromResult(daily); public Task> GetRecentAsync(int limit, CancellationToken ct) => Task.FromResult((IReadOnlyList)Array.Empty()); public Task PurgeBeforeAsync(DateOnly date, CancellationToken ct) => Task.FromResult(0); } private static DailyUsage Usage(decimal cost, int tokens = 0) => new(DateOnly.FromDateTime(DateTime.Now), tokens, 0, cost, true, 1); [Fact] public async Task Ohne_API_Key_meldet_error() { var provider = new InstanceHealthProvider( "Test", agentsEnabled: false, InstanceBudget.Unlimited, usage: null, () => 2, () => 0); var health = await provider.GetAsync(default); health.Status.ShouldBe(WatchdogStatus.Error); health.Metrics["agentCount"].ShouldBe(2); health.Checks["agents"].Ok.ShouldBeFalse(); } [Fact] public async Task Erschoepftes_Kostenbudget_meldet_warning() { var budget = new InstanceBudget { DailyCostUsd = 5m }; var provider = new InstanceHealthProvider( "Test", agentsEnabled: true, budget, new FakeUsage(Usage(cost: 5m)), () => 1, () => 0); var health = await provider.GetAsync(default); health.Status.ShouldBe(WatchdogStatus.Warning); health.Checks["budget"].Ok.ShouldBeFalse(); health.Message.ShouldNotBeNull(); } /// /// Ein Heartbeat beweist nur, dass ein Faden läuft. Steht der Scanner, arbeitet die /// Instanz nichts mehr ab — und genau das soll die Prüfung sichtbar machen. /// [Fact] public async Task Stehender_Scanner_meldet_warning() { var provider = new InstanceHealthProvider( "Test", agentsEnabled: true, InstanceBudget.Unlimited, new FakeUsage(Usage(cost: 0m)), () => 1, () => 0, schedulerRunning: () => false); var health = await provider.GetAsync(default); health.Status.ShouldBe(WatchdogStatus.Warning); health.Checks["scheduler"].Ok.ShouldBeFalse(); } [Fact] public async Task Normalbetrieb_meldet_ok_mit_Metriken() { var provider = new InstanceHealthProvider( "Test", agentsEnabled: true, InstanceBudget.Unlimited, new FakeUsage(Usage(cost: 0.5m, tokens: 1200)), () => 3, () => 1, schedulerRunning: () => true); var health = await provider.GetAsync(default); health.Status.ShouldBe(WatchdogStatus.Ok); health.Metrics["runningChats"].ShouldBe(1); health.Metrics.Keys.ShouldContain("todayCostUsd"); health.Metrics.Keys.ShouldContain("todayTokens"); health.Checks.Values.ShouldAllBe(c => c.Ok); } /// /// Metriken werden serverseitig nur ausgewertet, wenn sie Zahlen sind — alles andere /// verwirft der Verlauf stillschweigend. Beschreibendes gehört in die Meldung. /// [Fact] public async Task Instanzname_steht_in_der_Meldung_nicht_in_den_Metriken() { var provider = new InstanceHealthProvider( "Produktion", agentsEnabled: true, InstanceBudget.Unlimited, usage: null, () => 1, () => 0); var health = await provider.GetAsync(default); health.Message.ShouldContain("Produktion"); health.Metrics.Keys.ShouldNotContain("instanceName"); } }