Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Usage/UsageGateTests.cs
T
mika kuns 8f8c2a65b2 feat(claude-do): Worker: UsageGate — Queue ab Schwelle pausieren
> **Stand 2026-08-05 (List-Handler):** Der Roadblock aus dem letzten Lauf ist erledigt. Beide Voraussetzungen sind jetzt auf `main` gemerged: die `app_settings`-Schwellen `UsageGateFiveHourPct`/`UsageGateSevenDayPct` (Merge-Commit `b1efcdc`) und `UsageState`/`IUsageClient`/`UsageMonitorService` unter `src/ClaudeDo.Worker/Usage/` (Merge-Commit `b126a21`). Dein Worktree ist frisch von diesem `main`

ClaudeDo-Task: 06a7cc32-6ab7-4758-98f4-bee77149b2bf
2026-08-05 11:10:30 +02:00

106 lines
3.3 KiB
C#

using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Usage;
using Microsoft.Extensions.Logging.Abstractions;
namespace ClaudeDo.Worker.Tests.Usage;
public sealed class UsageGateTests : IDisposable
{
private readonly DbFixture _db = new();
public void Dispose() => _db.Dispose();
private async Task SetThresholdsAsync(int fiveHourPct, int sevenDayPct)
{
using var ctx = _db.CreateContext();
var repo = new AppSettingsRepository(ctx);
var settings = await repo.GetAsync();
settings.UsageGateFiveHourPct = fiveHourPct;
settings.UsageGateSevenDayPct = sevenDayPct;
await repo.UpdateAsync(settings);
}
private UsageGate CreateGate(UsageState state) =>
new(_db.CreateFactory(), state, NullLogger<UsageGate>.Instance);
private static UsageState StateWithSnapshot(double fiveHourPct, double sevenDayPct)
{
var state = new UsageState();
state.ReportSuccess(new UsageSnapshot(
new UsageBucket(fiveHourPct, null),
new UsageBucket(sevenDayPct, null),
Array.Empty<UsageLimitRow>(),
DateTime.UtcNow));
return state;
}
[Fact]
public async Task BothUnderThreshold_NotBlocked()
{
await SetThresholdsAsync(80, 90);
var decision = await CreateGate(StateWithSnapshot(50, 60)).EvaluateAsync();
Assert.False(decision.IsBlocked);
}
[Fact]
public async Task FiveHourAtOrOverThreshold_Blocked()
{
await SetThresholdsAsync(80, 90);
var decision = await CreateGate(StateWithSnapshot(85, 60)).EvaluateAsync();
Assert.True(decision.IsBlocked);
Assert.Contains("5h", decision.Reason);
}
[Fact]
public async Task SevenDayAtOrOverThreshold_Blocked()
{
await SetThresholdsAsync(80, 90);
var decision = await CreateGate(StateWithSnapshot(50, 95)).EvaluateAsync();
Assert.True(decision.IsBlocked);
Assert.Contains("7d", decision.Reason);
}
[Fact]
public async Task ExactlyOnThreshold_Blocked()
{
await SetThresholdsAsync(80, 90);
var decision = await CreateGate(StateWithSnapshot(80, 60)).EvaluateAsync();
Assert.True(decision.IsBlocked);
}
[Fact]
public async Task ThresholdZero_ThatBucketNeverGates()
{
await SetThresholdsAsync(0, 90);
var decision = await CreateGate(StateWithSnapshot(100, 60)).EvaluateAsync();
Assert.False(decision.IsBlocked);
}
[Fact]
public async Task BothThresholdsZero_AlwaysFree()
{
await SetThresholdsAsync(0, 0);
var decision = await CreateGate(StateWithSnapshot(100, 100)).EvaluateAsync();
Assert.False(decision.IsBlocked);
}
[Fact]
public async Task NoSnapshotYet_FailsOpen()
{
await SetThresholdsAsync(80, 90);
var decision = await CreateGate(new UsageState()).EvaluateAsync();
Assert.False(decision.IsBlocked);
}
[Fact]
public async Task LastPollFailed_FailsOpen()
{
await SetThresholdsAsync(80, 90);
var state = StateWithSnapshot(95, 95);
state.ReportFailure("boom", DateTime.UtcNow);
var decision = await CreateGate(state).EvaluateAsync();
Assert.False(decision.IsBlocked);
}
}