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 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(), 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); } }