143 lines
5.1 KiB
C#
143 lines
5.1 KiB
C#
using System.Text.Json;
|
|
using ClaudeDo.Worker.Config;
|
|
using ClaudeDo.Worker.Usage;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Usage;
|
|
|
|
/// <summary>
|
|
/// Covers what is left of the reader after the model/scope aggregation moved to TokenTracker:
|
|
/// the per-session totals that back <c>task_runs</c>' per-run token delta.
|
|
/// </summary>
|
|
public class TranscriptUsageReaderTests : IDisposable
|
|
{
|
|
private readonly string _root;
|
|
private readonly WorkerConfig _cfg;
|
|
|
|
public TranscriptUsageReaderTests()
|
|
{
|
|
_root = Path.Combine(Path.GetTempPath(), $"tur_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_root);
|
|
_cfg = new WorkerConfig
|
|
{
|
|
CentralWorktreeRoot = Path.Combine(_root, "central-worktrees"),
|
|
SandboxRoot = Path.Combine(_root, "sandbox"),
|
|
};
|
|
}
|
|
|
|
public void Dispose() { try { Directory.Delete(_root, true); } catch { } }
|
|
|
|
private string WriteSession(string projectDir, string file, params string[] lines)
|
|
{
|
|
var dir = Path.Combine(_root, "projects", projectDir);
|
|
Directory.CreateDirectory(dir);
|
|
var path = Path.Combine(dir, file);
|
|
File.WriteAllLines(path, lines);
|
|
return path;
|
|
}
|
|
|
|
private static string AssistantLine(
|
|
string cwd, string ts, string model, long input, long output, long cacheRead, long cacheCreation,
|
|
string? requestId = null, string? messageId = null) =>
|
|
JsonSerializer.Serialize(new Dictionary<string, object?>
|
|
{
|
|
["type"] = "assistant",
|
|
["cwd"] = cwd,
|
|
["timestamp"] = ts,
|
|
["requestId"] = requestId,
|
|
["message"] = new Dictionary<string, object?>
|
|
{
|
|
["id"] = messageId,
|
|
["model"] = model,
|
|
["usage"] = new Dictionary<string, object?>
|
|
{
|
|
["input_tokens"] = input,
|
|
["output_tokens"] = output,
|
|
["cache_read_input_tokens"] = cacheRead,
|
|
["cache_creation_input_tokens"] = cacheCreation,
|
|
},
|
|
},
|
|
});
|
|
|
|
private TranscriptUsageReader MakeReader() =>
|
|
new(_cfg, Path.Combine(_root, "projects"));
|
|
|
|
[Fact]
|
|
public async Task ReadSessionTotalsAsync_Sums_All_Assistant_Messages_In_The_Session_File()
|
|
{
|
|
WriteSession("proj", "sess-1.jsonl",
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 100, 5, requestId: "r1"),
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T09:00:00Z", "claude-sonnet-5", 3, 7, 200, 0, requestId: "r2"));
|
|
|
|
var reader = MakeReader();
|
|
var totals = await reader.ReadSessionTotalsAsync("sess-1");
|
|
|
|
Assert.NotNull(totals);
|
|
Assert.Equal(13, totals!.InputTokens);
|
|
Assert.Equal(27, totals.OutputTokens);
|
|
Assert.Equal(300, totals.CacheReadTokens);
|
|
Assert.Equal(5, totals.CacheCreationTokens);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadSessionTotalsAsync_Skips_Synthetic_Model_Messages()
|
|
{
|
|
WriteSession("proj", "sess-2.jsonl",
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "r1"),
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T09:00:00Z", "<synthetic>", 999, 999, 999, 999, requestId: "r2"));
|
|
|
|
var reader = MakeReader();
|
|
var totals = await reader.ReadSessionTotalsAsync("sess-2");
|
|
|
|
Assert.NotNull(totals);
|
|
Assert.Equal(10, totals!.InputTokens);
|
|
Assert.Equal(20, totals.OutputTokens);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadSessionTotalsAsync_Dedupes_By_RequestId_Within_The_Session()
|
|
{
|
|
WriteSession("proj", "sess-3.jsonl",
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "dup"),
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:01Z", "claude-sonnet-5", 10, 20, 0, 0, requestId: "dup"));
|
|
|
|
var reader = MakeReader();
|
|
var totals = await reader.ReadSessionTotalsAsync("sess-3");
|
|
|
|
Assert.NotNull(totals);
|
|
Assert.Equal(10, totals!.InputTokens);
|
|
Assert.Equal(20, totals.OutputTokens);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadSessionTotalsAsync_Malformed_Line_Does_Not_Abort_The_Read()
|
|
{
|
|
WriteSession("proj", "sess-4.jsonl",
|
|
"this is not json",
|
|
AssistantLine(@"C:\Dev\App", "2026-06-01T08:00:00Z", "claude-sonnet-5", 5, 5, 0, 0, requestId: "r1"));
|
|
|
|
var reader = MakeReader();
|
|
var totals = await reader.ReadSessionTotalsAsync("sess-4");
|
|
|
|
Assert.NotNull(totals);
|
|
Assert.Equal(5, totals!.InputTokens);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadSessionTotalsAsync_Returns_Null_When_No_Matching_Transcript_File()
|
|
{
|
|
var reader = MakeReader();
|
|
var totals = await reader.ReadSessionTotalsAsync("does-not-exist");
|
|
|
|
Assert.Null(totals);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadSessionTotalsAsync_Returns_Null_When_ProjectsRoot_Missing()
|
|
{
|
|
var reader = new TranscriptUsageReader(_cfg, Path.Combine(_root, "does-not-exist"));
|
|
var totals = await reader.ReadSessionTotalsAsync("sess-1");
|
|
|
|
Assert.Null(totals);
|
|
}
|
|
}
|