using ClaudeDo.Worker.Usage.TokenTracker; namespace ClaudeDo.Worker.Tests.Usage.TokenTracker; public sealed class TokenTrackerExportParserTests { private static readonly DateTime FetchedAt = new(2026, 8, 24, 9, 0, 0, DateTimeKind.Utc); [Fact] public void Parse_ReadsAllSessions() { var export = TokenTrackerExportParser.Parse(TokenTrackerFixtures.SessionsJson, FetchedAt); Assert.NotNull(export); Assert.Equal(4, export!.Sessions.Count); Assert.Equal(11, export.FormatVersion); Assert.Equal(FetchedAt, export.FetchedAtUtc); } [Fact] public void Parse_MapsTokenFieldsAndCost() { var export = TokenTrackerExportParser.Parse(TokenTrackerFixtures.SessionsJson, FetchedAt)!; var first = export.Sessions.Single(s => s.SessionHash == "b24babcbf2b615730459773f"); Assert.Equal("claude-opus-5", first.Model); Assert.Equal(100, first.InputTokens); Assert.Equal(400, first.OutputTokens); Assert.Equal(900, first.CacheReadTokens); Assert.Equal(50, first.CacheCreationTokens); Assert.Equal(2.5, first.CostUsd); Assert.Equal(4, first.Turns); Assert.Equal(1, first.RetryTurns); Assert.True(first.Productive); Assert.True(first.OneShot); } [Fact] public void Parse_UnsupportedVersion_StillParsesButReportsIt() { var export = TokenTrackerExportParser.Parse(TokenTrackerFixtures.UnsupportedVersionJson, FetchedAt); Assert.NotNull(export); Assert.Equal(99, export!.FormatVersion); } [Fact] public void Parse_MissingFields_DefaultToZero() { const string json = """ { "sessions": [ { "version": 11, "session_hash": "abc", "source": "claude", "model": "claude-opus-5", "started_at": "2026-08-20T10:00:00.000Z" } ] } """; var session = TokenTrackerExportParser.Parse(json, FetchedAt)!.Sessions.Single(); Assert.Equal(0, session.InputTokens); Assert.Equal(0, session.CostUsd); Assert.Equal(0, session.RetryTurns); Assert.False(session.Productive); } [Fact] public void Parse_SkipsRowsWithoutHashOrTimestamp() { const string json = """ { "sessions": [ { "version": 11, "source": "claude", "model": "m" }, { "version": 11, "session_hash": "abc", "source": "claude", "model": "m" }, { "version": 11, "session_hash": "def", "source": "claude", "model": "m", "started_at": "2026-08-20T10:00:00.000Z" } ] } """; var export = TokenTrackerExportParser.Parse(json, FetchedAt); Assert.Single(export!.Sessions); Assert.Equal("def", export.Sessions[0].SessionHash); } [Fact] public void Parse_SkipsNonClaudeSources() { const string json = """ { "sessions": [ { "version": 11, "session_hash": "abc", "source": "codex", "model": "gpt", "started_at": "2026-08-20T10:00:00.000Z" } ] } """; Assert.Empty(TokenTrackerExportParser.Parse(json, FetchedAt)!.Sessions); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData("not json at all")] [InlineData("[1,2,3]")] [InlineData("{\"available\": false}")] public void Parse_Garbage_ReturnsNull(string json) { Assert.Null(TokenTrackerExportParser.Parse(json, FetchedAt)); } }