feat(ui): add usage pill to footer and mission control header
Adds the IWorkerClient/WorkerClient usage surface (GetUsageSnapshot, GetModelUsage, GetTaskUsage, UsageUpdated event) and a shared UsagePillViewModel hosted once in IslandsShellViewModel (footer) and once in MissionControlViewModel (header), showing "5h X% · 7d Y%" with warn/blocked/stale states via existing design tokens. The OpenMonitorCommand is wired but currently a no-op, pending the usage monitor modal.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class UsagePillViewModelTests
|
||||
{
|
||||
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)
|
||||
=> new(
|
||||
fiveHourPercent, fiveHourResetsAt, sevenDayPercent, sevenDayResetsAt,
|
||||
Array.Empty<UsageLimitDto>(), fiveHourThresholdPct, sevenDayThresholdPct,
|
||||
isGateBlocked, gateReason, fetchedAtUtc ?? DateTime.UtcNow, isStale, lastError);
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user