using ClaudeDo.Worker.Usage.TokenTracker; namespace ClaudeDo.Worker.Tests.Usage.TokenTracker; public sealed class TokenTrackerAggregatorTests { private static readonly DateOnly RangeStart = new(2026, 8, 20); private static readonly DateOnly RangeEnd = new(2026, 8, 21); private static IReadOnlyList Sessions() => TokenTrackerExportParser.Parse( TokenTrackerFixtures.SessionsJson, new DateTime(2026, 8, 24, 9, 0, 0, DateTimeKind.Utc))!.Sessions; private static HashSet ClaudeDoHashes() => [ SessionHash.ForClaudeSession(TokenTrackerFixtures.ClaudeDoSessionIdA)!, SessionHash.ForClaudeSession(TokenTrackerFixtures.ClaudeDoSessionIdB)!, ]; [Fact] public void ByModel_ExcludesSessionsOutsideTheRange() { var rows = TokenTrackerAggregator.ByModel(Sessions(), RangeStart, RangeEnd, ClaudeDoHashes()); Assert.All(rows, r => Assert.InRange(r.Date, RangeStart, RangeEnd)); Assert.DoesNotContain(rows, r => r.CostUsd == 99.0); } [Fact] public void ByModel_SplitsClaudeDoFromOther() { var rows = TokenTrackerAggregator.ByModel(Sessions(), RangeStart, RangeEnd, ClaudeDoHashes()); var opus = rows.Single(r => r.Model == "claude-opus-5" && r.Scope == "claudedo"); Assert.Equal(110, opus.InputTokens); Assert.Equal(440, opus.OutputTokens); Assert.Equal(990, opus.CacheReadTokens); Assert.Equal(55, opus.CacheCreationTokens); Assert.Equal(2, opus.Sessions); Assert.Equal(3.5, opus.CostUsd, 6); var sonnet = rows.Single(r => r.Model == "claude-sonnet-5"); Assert.Equal("other", sonnet.Scope); Assert.Equal(0.25, sonnet.CostUsd, 6); } [Fact] public void ByModel_UnknownHash_CountsAsOther() { var rows = TokenTrackerAggregator.ByModel(Sessions(), RangeStart, RangeEnd, new HashSet()); Assert.All(rows, r => Assert.Equal("other", r.Scope)); } [Fact] public void ByModel_EmptyInput_ReturnsEmpty() { Assert.Empty(TokenTrackerAggregator.ByModel([], RangeStart, RangeEnd, new HashSet())); } [Fact] public void ForSessions_SumsCostAndRetriesOfMatchingRows() { var extras = TokenTrackerAggregator.ForSessions( Sessions(), [TokenTrackerFixtures.ClaudeDoSessionIdA, TokenTrackerFixtures.ClaudeDoSessionIdB]); Assert.NotNull(extras); Assert.Equal(3.5, extras!.CostUsd, 6); Assert.Equal(3, extras.Retries); Assert.True(extras.Productive); // A is productive Assert.False(extras.OneShot); // B is not one-shot } [Fact] public void ForSessions_SingleOneShotSession_ReportsOneShot() { var extras = TokenTrackerAggregator.ForSessions( Sessions(), [TokenTrackerFixtures.ClaudeDoSessionIdA]); Assert.True(extras!.OneShot); Assert.Equal(2.5, extras.CostUsd, 6); } [Fact] public void ForSessions_NoMatch_ReturnsNull() { Assert.Null(TokenTrackerAggregator.ForSessions(Sessions(), ["11111111-2222-3333-4444-555555555555"])); } [Fact] public void ForSessions_BlankIds_ReturnsNull() { Assert.Null(TokenTrackerAggregator.ForSessions(Sessions(), [null, "", " "])); } [Fact] public void Totals_SumsTokensAndCostInRange() { var totals = TokenTrackerAggregator.Totals(Sessions(), RangeStart, RangeEnd); Assert.Equal(1615, totals.TotalTokens); // 1450 + 145 + 20 Assert.Equal(3.75, totals.CostUsd, 6); } }