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() .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().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().UseSqlite($"Data Source={_dbPath}").Options); var reloaded = await freshCtx.TaskRuns.AsNoTracking().FirstAsync(r => r.Id == "r2"); Assert.Null(reloaded.Model); } }