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 TaskNumberAllocatorTests : IDisposable { private readonly string _dbPath; private readonly DbContextOptions _options; private readonly ClaudeDoDbContext _ctx; public TaskNumberAllocatorTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_tasknum_{Guid.NewGuid():N}.db"); _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 { } } private async Task SeedListAsync(string id = "l1") { _ctx.Lists.Add(new ListEntity { Id = id, Name = "Test List", CreatedAt = DateTime.UtcNow }); await _ctx.SaveChangesAsync(); return id; } private static TaskEntity NewTask(string listId, string title = "T") => new() { Id = Guid.NewGuid().ToString(), ListId = listId, Title = title, Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, }; [Fact] public async Task AddAsync_assigns_increasing_unique_numbers() { await SeedListAsync(); var repo = new TaskRepository(_ctx); var first = NewTask("l1"); await repo.AddAsync(first); var second = NewTask("l1"); await repo.AddAsync(second); Assert.True(first.Number > 0); Assert.True(second.Number > first.Number); } [Fact] public async Task CreateChildAsync_also_allocates_a_number() { await SeedListAsync(); var parent = NewTask("l1"); await new TaskRepository(_ctx).AddAsync(parent); var child = await new TaskRepository(_ctx).CreateChildAsync(parent.Id, "Child", null, null); Assert.True(child.Number > 0); Assert.NotEqual(parent.Number, child.Number); } [Fact] public async Task Deleting_the_highest_numbered_task_does_not_recycle_its_number() { await SeedListAsync(); var repo = new TaskRepository(_ctx); var t1 = NewTask("l1"); await repo.AddAsync(t1); var t2 = NewTask("l1"); await repo.AddAsync(t2); Assert.True(t2.Number > t1.Number); await repo.DeleteAsync(t2.Id); var t3 = NewTask("l1"); await repo.AddAsync(t3); Assert.True(t3.Number > t2.Number); Assert.NotEqual(t2.Number, t3.Number); } [Fact] public async Task Repeated_creation_never_assigns_the_same_number_twice() { var listId = await SeedListAsync(); var numbers = new List(); for (var i = 0; i < 20; i++) { await using var ctx = new ClaudeDoDbContext(_options); var task = NewTask(listId, $"T{i}"); await new TaskRepository(ctx).AddAsync(task); numbers.Add(task.Number); } Assert.Equal(numbers.Count, numbers.Distinct().Count()); } [Fact] public async Task Concurrent_creation_never_assigns_the_same_number_twice() { var listId = await SeedListAsync(); var tasks = Enumerable.Range(0, 10).Select(async i => { await using var ctx = new ClaudeDoDbContext(_options); var task = NewTask(listId, $"C{i}"); await new TaskRepository(ctx).AddAsync(task); return task.Number; }); var numbers = await Task.WhenAll(tasks); Assert.Equal(numbers.Length, numbers.Distinct().Count()); Assert.All(numbers, n => Assert.True(n > 0)); } [Fact] public async Task GetByNumberAsync_finds_the_task_and_returns_null_for_unknown_numbers() { await SeedListAsync(); var task = NewTask("l1"); await new TaskRepository(_ctx).AddAsync(task); var found = await new TaskRepository(_ctx).GetByNumberAsync(task.Number); var missing = await new TaskRepository(_ctx).GetByNumberAsync(task.Number + 999); Assert.NotNull(found); Assert.Equal(task.Id, found!.Id); Assert.Null(missing); } }