Files
ClaudeDo/tests/ClaudeDo.Ui.Tests/ViewModels/UsagePillViewModelTests.cs
T
mika kuns a201d3f43d chore(claude-do): UsageGate: Parallelitaet stufenweise drosseln statt erst bei
## Kontext: Limits sind Fenster, nicht Summen

Die Runs laufen ueber das Claude-Abo. Limits greifen pro 5h-Fenster und pro 7 Tage. Nicht die Wochensumme tut weh, sondern dass ein Agent-Burst ein Fenster leerraeumt, in dem Mika selbst interaktiv arbeiten will.

## Messgrundlage (alle Transcripts unter ~/.claude/projects)

Agent-Runs sind ueber die ganze Historie nur **18,4 %** des Account-Verbrauch

ClaudeDo-Task: 87105f5e-c4f4-4af4-ae60-89cd2e153e3c
2026-08-05 15:53:15 +02:00

148 lines
5.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using ClaudeDo.Localization;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels;
using Xunit;
namespace ClaudeDo.Ui.Tests.ViewModels;
public class UsagePillViewModelTests
{
// Loc.Current is ambient static state defaulting to a key-echo localizer, so these
// assertions on real strings must install a real one instead of relying on whichever
// other test class happened to run first.
public UsagePillViewModelTests()
{
var dir = AppContext.BaseDirectory;
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
dir = Path.GetDirectoryName(dir);
Loc.Current = new Localizer(
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
}
private sealed class FakeWorker : StubWorkerClient
{
public UsageSnapshotDto? InitialSnapshot;
public override Task<UsageSnapshotDto?> GetUsageSnapshotAsync() => Task.FromResult(InitialSnapshot);
}
private static UsageSnapshotDto Snapshot(
double? fiveHourPercent = null,
DateTimeOffset? fiveHourResetsAt = null,
double? sevenDayPercent = null,
DateTimeOffset? sevenDayResetsAt = null,
int fiveHourThresholdPct = 80,
int sevenDayThresholdPct = 90,
bool isGateBlocked = false,
string? gateReason = null,
DateTime? fetchedAtUtc = null,
bool isStale = false,
string? lastError = null,
int configuredSlots = 1,
int effectiveSlots = 1,
string? throttleBucket = null)
=> new(
fiveHourPercent, fiveHourResetsAt, sevenDayPercent, sevenDayResetsAt,
Array.Empty<UsageLimitDto>(), fiveHourThresholdPct, sevenDayThresholdPct,
isGateBlocked, gateReason, fetchedAtUtc ?? DateTime.UtcNow, isStale, lastError,
configuredSlots, effectiveSlots, throttleBucket);
// ── Text formatting ─────────────────────────────────────────────────────
[Fact]
public void Text_BothBucketsKnown_ShowsBoth()
{
var worker = new FakeWorker { InitialSnapshot = Snapshot(fiveHourPercent: 30, sevenDayPercent: 57) };
var vm = new UsagePillViewModel(worker);
Assert.Contains("5h", vm.Text);
Assert.Contains("30", vm.Text);
Assert.Contains("7d", vm.Text);
Assert.Contains("57", vm.Text);
}
[Fact]
public void Text_OnlyFiveHourKnown_ShowsOnlyFiveHour()
{
var worker = new FakeWorker { InitialSnapshot = Snapshot(fiveHourPercent: 42) };
var vm = new UsagePillViewModel(worker);
Assert.Contains("5h", vm.Text);
Assert.Contains("42", vm.Text);
Assert.DoesNotContain("7d", vm.Text);
}
[Fact]
public void Text_NoSnapshot_ShowsEmptyPlaceholder()
{
var worker = new FakeWorker();
var vm = new UsagePillViewModel(worker);
Assert.Equal("Usage ", vm.Text);
}
// ── IsWarn / IsBlocked threshold logic ──────────────────────────────────
[Fact]
public void IsWarn_BelowThresholdMinusTen_IsFalse()
{
var worker = new FakeWorker { InitialSnapshot = Snapshot(fiveHourPercent: 69, fiveHourThresholdPct: 80) };
var vm = new UsagePillViewModel(worker);
Assert.False(vm.IsWarn);
Assert.False(vm.IsBlocked);
}
[Fact]
public void IsWarn_AtThresholdMinusTen_IsTrue()
{
var worker = new FakeWorker { InitialSnapshot = Snapshot(fiveHourPercent: 70, fiveHourThresholdPct: 80) };
var vm = new UsagePillViewModel(worker);
Assert.True(vm.IsWarn);
Assert.False(vm.IsBlocked);
}
[Fact]
public void IsBlocked_WhenGateReportsBlocked_IsTrue()
{
var worker = new FakeWorker
{
InitialSnapshot = Snapshot(fiveHourPercent: 80, fiveHourThresholdPct: 80, isGateBlocked: true, gateReason: "5h-Limit 80% >= 80%")
};
var vm = new UsagePillViewModel(worker);
Assert.True(vm.IsBlocked);
}
// ── Stale ────────────────────────────────────────────────────────────────
[Fact]
public void Stale_Snapshot_SetsIsStale_AndTooltipMentionsStamp()
{
var fetchedAt = new DateTime(2026, 8, 5, 9, 41, 0, DateTimeKind.Utc);
var worker = new FakeWorker { InitialSnapshot = Snapshot(isStale: true, fetchedAtUtc: fetchedAt, lastError: "timeout") };
var vm = new UsagePillViewModel(worker);
Assert.True(vm.IsStale);
Assert.Contains(fetchedAt.ToLocalTime().ToString("HH:mm"), vm.Tooltip);
Assert.Contains("timeout", vm.Tooltip);
}
// ── UsageUpdated event ───────────────────────────────────────────────────
[Fact]
public void UsageUpdated_Event_UpdatesText()
{
var worker = new FakeWorker();
var vm = new UsagePillViewModel(worker);
Assert.Equal("Usage ", vm.Text);
worker.RaiseUsageUpdated(Snapshot(fiveHourPercent: 12));
Assert.Contains("12", vm.Text);
}
}