using ClaudeDo.Worker.Usage.TokenTracker; namespace ClaudeDo.Worker.Tests.Usage.TokenTracker; public sealed class TokenTrackerServiceTests { private static readonly DateTime Now = new(2026, 8, 24, 9, 0, 0, DateTimeKind.Utc); private static (TokenTrackerService Service, FakeTokenTrackerClient Client) Build(DateTime? now = null) { var client = new FakeTokenTrackerClient(); var clock = now ?? Now; return (new TokenTrackerService(client, new TokenTrackerState(), () => clock), client); } [Fact] public async Task OpenDashboardAsync_ReturnsTheUrlWhenTheCliIsInstalled() { var (service, client) = Build(); var dashboard = await service.OpenDashboardAsync(); Assert.True(dashboard.Ok); Assert.Equal("http://127.0.0.1:7680/", dashboard.Url); Assert.Equal(1, client.DashboardCalls); } [Fact] public async Task OpenDashboardAsync_MissingCli_ReportsTheProbeErrorWithoutSpawning() { var (service, client) = Build(); client.Probe = new TokenTrackerProbe(false, null, true, "v22.1.0", "'tokentracker' not found on PATH."); var dashboard = await service.OpenDashboardAsync(); Assert.False(dashboard.Ok); Assert.Null(dashboard.Url); Assert.Equal("'tokentracker' not found on PATH.", dashboard.Error); Assert.Equal(0, client.DashboardCalls); } [Fact] public async Task RefreshAsync_StoresParsedExport() { var (service, client) = Build(); await service.RefreshAsync(); Assert.Equal(1, client.ExportCalls); Assert.Equal(4, service.State.Export!.Sessions.Count); Assert.Null(service.State.LastError); } [Fact] public async Task RefreshAsync_RequestsTheConfiguredWindow() { var (service, client) = Build(); await service.RefreshAsync(); Assert.Equal(DateOnly.FromDateTime(Now.ToLocalTime()), client.LastTo); Assert.Equal( DateOnly.FromDateTime(Now.ToLocalTime()).AddDays(-TokenTrackerService.WindowDays), client.LastFrom); } [Fact] public async Task RefreshAsync_Failure_KeepsLastGoodExport() { var (service, client) = Build(); await service.RefreshAsync(); client.ExportResult = new TokenTrackerRunResult(false, "", "tokentracker exited with code 1"); await service.RefreshAsync(); Assert.Equal(4, service.State.Export!.Sessions.Count); Assert.Equal("tokentracker exited with code 1", service.State.LastError); } [Fact] public async Task RefreshAsync_UnparseableOutput_IsAnError() { var (service, client) = Build(); client.ExportResult = new TokenTrackerRunResult(true, "not json", null); await service.RefreshAsync(); Assert.Null(service.State.Export); Assert.NotNull(service.State.LastError); } [Fact] public async Task RefreshAsync_UnsupportedFormatVersion_IsAnErrorAndKeepsNoData() { var (service, client) = Build(); client.ExportResult = new TokenTrackerRunResult(true, TokenTrackerFixtures.UnsupportedVersionJson, null); await service.RefreshAsync(); Assert.Null(service.State.Export); Assert.Contains("99", service.State.LastError); } [Fact] public async Task EnsureFreshAsync_FreshExport_DoesNotFetchAgain() { var (service, client) = Build(); await service.RefreshAsync(); await service.EnsureFreshAsync(TimeSpan.FromMinutes(15)); Assert.Equal(1, client.ExportCalls); } [Fact] public async Task EnsureFreshAsync_NoExportYet_Fetches() { var (service, client) = Build(); await service.EnsureFreshAsync(TimeSpan.FromMinutes(15)); Assert.Equal(1, client.ExportCalls); } [Fact] public async Task ConcurrentRefresh_RunsTheCliOnce() { var (service, client) = Build(); client.BlockExport = true; var first = service.RefreshAsync(); var second = service.RefreshAsync(); client.Release(); await Task.WhenAll(first, second); Assert.Equal(1, client.ExportCalls); } [Fact] public async Task ProbeAsync_CachesUntilForced() { var (service, client) = Build(); await service.ProbeAsync(); await service.ProbeAsync(); Assert.Equal(1, client.ProbeCalls); await service.ProbeAsync(force: true); Assert.Equal(2, client.ProbeCalls); } [Fact] public async Task InstallAsync_ReprobesAfterwards() { var (service, client) = Build(); await service.ProbeAsync(); var result = await service.InstallAsync(); Assert.True(result.Ok); Assert.Equal(1, client.InstallCalls); Assert.Equal(2, client.ProbeCalls); } }