using ClawdDotNet.Core.Accounting; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Deploymentcenter.Watchdog; using Shouldly; namespace ClawdDotNet.Core.Tests.Deploymentcenter; public sealed class InstanceHealthProviderTests { /// Delivers a fixed daily consumption, without a 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 Without_API_key_reports_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 Exhausted_cost_budget_reports_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(); } /// /// A heartbeat only proves that a thread is running. If the scanner stands still, the /// instance no longer works through anything — and exactly that is what the check should make visible. /// [Fact] public async Task Stalled_scanner_reports_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 Normal_operation_reports_ok_with_metrics() { 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); } /// /// Metrics are only evaluated server-side if they are numbers — everything else /// the history silently discards. Descriptive things belong in the message. /// [Fact] public async Task Instance_name_is_in_the_message_not_in_the_metrics() { var provider = new InstanceHealthProvider( "Produktion", agentsEnabled: true, InstanceBudget.Unlimited, usage: null, () => 1, () => 0); var health = await provider.GetAsync(default); health.Message.ShouldNotBeNull().ShouldContain("Produktion"); health.Metrics.Keys.ShouldNotContain("instanceName"); } }