using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Tests.Infrastructure; namespace ClaudeDo.Worker.Tests.Repositories; public sealed class TaskRunRepositoryTests : IDisposable { private readonly DbFixture _db = new(); private readonly TaskRunRepository _runs; private readonly string _taskId; public TaskRunRepositoryTests() { _runs = new TaskRunRepository(_db.Factory); // Seed a list and task for all tests var lists = new ListRepository(_db.Factory); var tasks = new TaskRepository(_db.Factory); var listId = Guid.NewGuid().ToString(); lists.AddAsync(new ListEntity { Id = listId, Name = "Test List", CreatedAt = DateTime.UtcNow, }).GetAwaiter().GetResult(); _taskId = Guid.NewGuid().ToString(); tasks.AddAsync(new TaskEntity { Id = _taskId, ListId = listId, Title = "Test Task", Status = Data.Models.TaskStatus.Queued, CreatedAt = DateTime.UtcNow, CommitType = "feat", }).GetAwaiter().GetResult(); } public void Dispose() => _db.Dispose(); private TaskRunEntity MakeRun(int runNumber, bool isRetry = false) => new() { Id = Guid.NewGuid().ToString(), TaskId = _taskId, RunNumber = runNumber, IsRetry = isRetry, Prompt = $"Do something (run {runNumber})", StartedAt = DateTime.UtcNow, }; [Fact] public async Task Add_And_GetById_Roundtrips() { var entity = new TaskRunEntity { Id = Guid.NewGuid().ToString(), TaskId = _taskId, RunNumber = 1, SessionId = "sess-abc", IsRetry = false, Prompt = "Fix the bug", ResultMarkdown = "All done", StructuredOutputJson = """{"ok":true}""", ErrorMarkdown = null, ExitCode = 0, TurnCount = 5, TokensIn = 1000, TokensOut = 2000, LogPath = "/tmp/run1.ndjson", StartedAt = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), FinishedAt = new DateTime(2026, 1, 1, 0, 5, 0, DateTimeKind.Utc), }; await _runs.AddAsync(entity); var loaded = await _runs.GetByIdAsync(entity.Id); Assert.NotNull(loaded); Assert.Equal(entity.Id, loaded.Id); Assert.Equal(entity.TaskId, loaded.TaskId); Assert.Equal(entity.RunNumber, loaded.RunNumber); Assert.Equal(entity.SessionId, loaded.SessionId); Assert.Equal(entity.IsRetry, loaded.IsRetry); Assert.Equal(entity.Prompt, loaded.Prompt); Assert.Equal(entity.ResultMarkdown, loaded.ResultMarkdown); Assert.Equal(entity.StructuredOutputJson, loaded.StructuredOutputJson); Assert.Null(loaded.ErrorMarkdown); Assert.Equal(entity.ExitCode, loaded.ExitCode); Assert.Equal(entity.TurnCount, loaded.TurnCount); Assert.Equal(entity.TokensIn, loaded.TokensIn); Assert.Equal(entity.TokensOut, loaded.TokensOut); Assert.Equal(entity.LogPath, loaded.LogPath); Assert.Equal(entity.StartedAt, loaded.StartedAt); Assert.Equal(entity.FinishedAt, loaded.FinishedAt); } [Fact] public async Task GetByTaskId_Returns_Ordered_By_RunNumber() { var run3 = MakeRun(3); var run1 = MakeRun(1); var run2 = MakeRun(2); await _runs.AddAsync(run3); await _runs.AddAsync(run1); await _runs.AddAsync(run2); var runs = await _runs.GetByTaskIdAsync(_taskId); Assert.Equal(3, runs.Count); Assert.Equal(1, runs[0].RunNumber); Assert.Equal(2, runs[1].RunNumber); Assert.Equal(3, runs[2].RunNumber); } [Fact] public async Task GetLatestByTaskId_Returns_Highest_RunNumber() { var run1 = MakeRun(1); var run2 = MakeRun(2); await _runs.AddAsync(run1); await _runs.AddAsync(run2); var latest = await _runs.GetLatestByTaskIdAsync(_taskId); Assert.NotNull(latest); Assert.Equal(run2.Id, latest.Id); Assert.Equal(2, latest.RunNumber); } [Fact] public async Task Update_Persists_Completion_Fields() { var run = MakeRun(1); await _runs.AddAsync(run); run.SessionId = "sess-xyz"; run.ResultMarkdown = "Task completed"; run.StructuredOutputJson = """{"status":"done"}"""; run.ErrorMarkdown = null; run.ExitCode = 0; run.TurnCount = 12; run.TokensIn = 5000; run.TokensOut = 8000; run.FinishedAt = new DateTime(2026, 6, 1, 12, 0, 0, DateTimeKind.Utc); await _runs.UpdateAsync(run); var loaded = await _runs.GetByIdAsync(run.Id); Assert.NotNull(loaded); Assert.Equal("sess-xyz", loaded.SessionId); Assert.Equal("Task completed", loaded.ResultMarkdown); Assert.Equal("""{"status":"done"}""", loaded.StructuredOutputJson); Assert.Null(loaded.ErrorMarkdown); Assert.Equal(0, loaded.ExitCode); Assert.Equal(12, loaded.TurnCount); Assert.Equal(5000, loaded.TokensIn); Assert.Equal(8000, loaded.TokensOut); Assert.Equal(new DateTime(2026, 6, 1, 12, 0, 0, DateTimeKind.Utc), loaded.FinishedAt); } [Fact] public async Task GetLatestByTaskId_Returns_Null_When_No_Runs() { var latest = await _runs.GetLatestByTaskIdAsync(Guid.NewGuid().ToString()); Assert.Null(latest); } }