Files
ClaudeDo/tests/ClaudeDo.Data.Tests/UsageGateAndRunModelTests.cs
T
mika kuns 7d3d6d7b54 fix(worker): record real raw token usage per run, not the uncached remainder
task_runs.tokens_in only ever held the API's uncached "input_tokens" field
(off by a factor of ~400,000 on a resumed session), and tokens_out summed
only the last result event instead of the whole session. TaskRunner now
reads each run's cache-read/cache-write/input/output totals from the
session transcript via a new ITranscriptUsageReader.ReadSessionTotalsAsync,
storing the delta against prior runs on the same session so a --resume
doesn't double-count. New task_runs.cache_read_tokens/cache_write_tokens
columns; the Session tab now shows the raw total (what actually counts
against the 5h/7d usage limit) with a breakdown tooltip.
2026-08-05 15:44:20 +02:00

175 lines
6.0 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Data.Tests;
public sealed class UsageGateAndRunModelTests : IDisposable
{
private readonly string _dbPath;
private readonly ClaudeDoDbContext _ctx;
public UsageGateAndRunModelTests()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_usagegate_{Guid.NewGuid():N}.db");
var options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={_dbPath}")
.Options;
_ctx = new ClaudeDoDbContext(options);
_ctx.Database.EnsureCreated();
}
public void Dispose()
{
_ctx.Dispose();
foreach (var suffix in new[] { "", "-wal", "-shm" })
try { File.Delete(_dbPath + suffix); } catch { }
}
[Fact]
public async Task Fresh_db_seeds_usage_gate_defaults()
{
var settings = await _ctx.AppSettings.AsNoTracking().FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
Assert.Equal(80, settings.UsageGateFiveHourPct);
Assert.Equal(90, settings.UsageGateSevenDayPct);
}
[Fact]
public async Task UpdateAsync_clamps_out_of_range_values()
{
var repo = new AppSettingsRepository(_ctx);
var current = await repo.GetAsync();
current.UsageGateFiveHourPct = -5;
current.UsageGateSevenDayPct = 150;
await repo.UpdateAsync(current);
var reloaded = await repo.GetAsync();
Assert.Equal(0, reloaded.UsageGateFiveHourPct);
Assert.Equal(100, reloaded.UsageGateSevenDayPct);
}
[Fact]
public async Task UpdateAsync_writes_through_valid_values()
{
var repo = new AppSettingsRepository(_ctx);
var current = await repo.GetAsync();
current.UsageGateFiveHourPct = 60;
current.UsageGateSevenDayPct = 95;
await repo.UpdateAsync(current);
var reloaded = await repo.GetAsync();
Assert.Equal(60, reloaded.UsageGateFiveHourPct);
Assert.Equal(95, reloaded.UsageGateSevenDayPct);
}
[Fact]
public async Task TaskRun_model_roundtrips_when_set()
{
var list = new ListEntity { Id = "l1", Name = "Test", CreatedAt = DateTime.UtcNow };
var task = new TaskEntity { Id = "t1", ListId = "l1", Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
var run = new TaskRunEntity
{
Id = "r1",
TaskId = "t1",
RunNumber = 1,
IsRetry = false,
Prompt = "do the thing",
Model = "sonnet",
};
_ctx.Lists.Add(list);
_ctx.Tasks.Add(task);
_ctx.TaskRuns.Add(run);
await _ctx.SaveChangesAsync();
await using var freshCtx = new ClaudeDoDbContext(
new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r1");
Assert.Equal("sonnet", reloaded.Model);
}
[Fact]
public async Task TaskRun_model_stays_null_when_not_set()
{
var list = new ListEntity { Id = "l2", Name = "Test", CreatedAt = DateTime.UtcNow };
var task = new TaskEntity { Id = "t2", ListId = "l2", Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
var run = new TaskRunEntity
{
Id = "r2",
TaskId = "t2",
RunNumber = 1,
IsRetry = false,
Prompt = "do the thing",
};
_ctx.Lists.Add(list);
_ctx.Tasks.Add(task);
_ctx.TaskRuns.Add(run);
await _ctx.SaveChangesAsync();
await using var freshCtx = new ClaudeDoDbContext(
new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r2");
Assert.Null(reloaded.Model);
}
[Fact]
public async Task TaskRun_cache_token_fields_roundtrip_when_set()
{
var list = new ListEntity { Id = "l3", Name = "Test", CreatedAt = DateTime.UtcNow };
var task = new TaskEntity { Id = "t3", ListId = "l3", Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
var run = new TaskRunEntity
{
Id = "r3",
TaskId = "t3",
RunNumber = 1,
IsRetry = false,
Prompt = "do the thing",
CacheReadTokens = 12345,
CacheWriteTokens = 678,
};
_ctx.Lists.Add(list);
_ctx.Tasks.Add(task);
_ctx.TaskRuns.Add(run);
await _ctx.SaveChangesAsync();
await using var freshCtx = new ClaudeDoDbContext(
new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r3");
Assert.Equal(12345, reloaded.CacheReadTokens);
Assert.Equal(678, reloaded.CacheWriteTokens);
}
[Fact]
public async Task TaskRun_cache_token_fields_stay_null_when_not_set()
{
var list = new ListEntity { Id = "l4", Name = "Test", CreatedAt = DateTime.UtcNow };
var task = new TaskEntity { Id = "t4", ListId = "l4", Title = "T", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow };
var run = new TaskRunEntity
{
Id = "r4",
TaskId = "t4",
RunNumber = 1,
IsRetry = false,
Prompt = "do the thing",
};
_ctx.Lists.Add(list);
_ctx.Tasks.Add(task);
_ctx.TaskRuns.Add(run);
await _ctx.SaveChangesAsync();
await using var freshCtx = new ClaudeDoDbContext(
new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={_dbPath}").Options);
var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r4");
Assert.Null(reloaded.CacheReadTokens);
Assert.Null(reloaded.CacheWriteTokens);
}
}