107 lines
3.5 KiB
C#
107 lines
3.5 KiB
C#
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<TokenTrackerSession> Sessions() =>
|
|
TokenTrackerExportParser.Parse(
|
|
TokenTrackerFixtures.SessionsJson,
|
|
new DateTime(2026, 8, 24, 9, 0, 0, DateTimeKind.Utc))!.Sessions;
|
|
|
|
private static HashSet<string> 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<string>());
|
|
|
|
Assert.All(rows, r => Assert.Equal("other", r.Scope));
|
|
}
|
|
|
|
[Fact]
|
|
public void ByModel_EmptyInput_ReturnsEmpty()
|
|
{
|
|
Assert.Empty(TokenTrackerAggregator.ByModel([], RangeStart, RangeEnd, new HashSet<string>()));
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|