The ctor-time LoadAsync races WorkerClient.StartAsync, so the SignalR call fails silently and the pill only fills on the next worker poll (up to 900s). Subscribe to ConnectionRestoredEvent, which also fires on the first successful connect, to retry the cached snapshot.
164 lines
5.9 KiB
C#
164 lines
5.9 KiB
C#
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);
|
||
}
|
||
|
||
// ── ConnectionRestored event ─────────────────────────────────────────────
|
||
|
||
[Fact]
|
||
public void ConnectionRestored_Event_LoadsSnapshot()
|
||
{
|
||
var worker = new FakeWorker();
|
||
var vm = new UsagePillViewModel(worker);
|
||
Assert.Null(vm.Snapshot);
|
||
|
||
worker.InitialSnapshot = Snapshot(fiveHourPercent: 25);
|
||
worker.RaiseConnectionRestored();
|
||
|
||
Assert.NotNull(vm.Snapshot);
|
||
Assert.Contains("25", vm.Text);
|
||
}
|
||
}
|