using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.State; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.State; public sealed class ReviewTransitionTests : IDisposable { private readonly DbFixture _db = new(); private readonly TestDbContextFactory _factory; private readonly TaskStateServiceBuilder.Built _built; private readonly ITaskStateService _sut; private readonly string _listId; public ReviewTransitionTests() { _factory = _db.CreateFactory(); _built = TaskStateServiceBuilder.Build(_factory); _sut = _built.State; _listId = Guid.NewGuid().ToString(); using var ctx = _factory.CreateDbContext(); ctx.Lists.Add(new ListEntity { Id = _listId, Name = "Test", CreatedAt = DateTime.UtcNow, DefaultCommitType = "chore", }); ctx.SaveChanges(); } public void Dispose() => _db.Dispose(); private async Task SeedTaskAsync(TaskStatus status, string? result = null) { var id = Guid.NewGuid().ToString(); await using var ctx = _factory.CreateDbContext(); ctx.Tasks.Add(new TaskEntity { Id = id, ListId = _listId, Title = "task", Status = status, Result = result, CreatedAt = DateTime.UtcNow, }); await ctx.SaveChangesAsync(); return id; } private async Task GetTaskAsync(string id) { await using var ctx = _factory.CreateDbContext(); return await new TaskRepository(ctx).GetByIdAsync(id) ?? throw new InvalidOperationException($"task {id} not found"); } // ─── SubmitForReviewAsync ───────────────────────────────────────────── [Fact] public async Task SubmitForReviewAsync_FromRunning_TransitionsToWaitingForReview() { var id = await SeedTaskAsync(TaskStatus.Running); var result = await _sut.SubmitForReviewAsync(id, DateTime.UtcNow, "the result", default); Assert.True(result.Ok); var t = await GetTaskAsync(id); Assert.Equal(TaskStatus.WaitingForReview, t.Status); Assert.Equal("the result", t.Result); Assert.NotNull(t.FinishedAt); } [Fact] public async Task SubmitForReviewAsync_FromQueued_Rejects() { var id = await SeedTaskAsync(TaskStatus.Queued); var result = await _sut.SubmitForReviewAsync(id, DateTime.UtcNow, "x", default); Assert.False(result.Ok); Assert.Equal(TaskStatus.Queued, (await GetTaskAsync(id)).Status); } // ─── ApproveReviewAsync ─────────────────────────────────────────────── [Fact] public async Task ApproveReviewAsync_FromWaitingForReview_TransitionsToDone() { var id = await SeedTaskAsync(TaskStatus.WaitingForReview); var result = await _sut.ApproveReviewAsync(id, default); Assert.True(result.Ok); Assert.Equal(TaskStatus.Done, (await GetTaskAsync(id)).Status); } [Fact] public async Task ApproveReviewAsync_FromIdle_Rejects() { var id = await SeedTaskAsync(TaskStatus.Idle); var result = await _sut.ApproveReviewAsync(id, default); Assert.False(result.Ok); Assert.Equal(TaskStatus.Idle, (await GetTaskAsync(id)).Status); } // ─── RejectToQueueAsync ─────────────────────────────────────────────── [Fact] public async Task RejectToQueueAsync_StoresFeedback_AndQueues_AndWakes() { var id = await SeedTaskAsync(TaskStatus.WaitingForReview); var wakesBefore = _built.WakeCount(); var result = await _sut.RejectToQueueAsync(id, "please fix the bug", default); Assert.True(result.Ok); var t = await GetTaskAsync(id); Assert.Equal(TaskStatus.Queued, t.Status); Assert.Equal("please fix the bug", t.ReviewFeedback); Assert.True(_built.WakeCount() > wakesBefore); } [Fact] public async Task RejectToQueueAsync_EmptyFeedback_Rejects() { var id = await SeedTaskAsync(TaskStatus.WaitingForReview); var result = await _sut.RejectToQueueAsync(id, " ", default); Assert.False(result.Ok); Assert.Equal(TaskStatus.WaitingForReview, (await GetTaskAsync(id)).Status); } [Fact] public async Task RejectToQueueAsync_FromIdle_Rejects() { var id = await SeedTaskAsync(TaskStatus.Idle); var result = await _sut.RejectToQueueAsync(id, "feedback", default); Assert.False(result.Ok); } // ─── RejectToIdleAsync ──────────────────────────────────────────────── [Fact] public async Task RejectToIdleAsync_Parks_KeepsResult_ClearsFeedback() { var id = await SeedTaskAsync(TaskStatus.WaitingForReview, result: "run output"); var result = await _sut.RejectToIdleAsync(id, default); Assert.True(result.Ok); var t = await GetTaskAsync(id); Assert.Equal(TaskStatus.Idle, t.Status); Assert.Equal("run output", t.Result); Assert.Null(t.ReviewFeedback); } [Fact] public async Task RejectToIdleAsync_FromRunning_Rejects() { var id = await SeedTaskAsync(TaskStatus.Running); var result = await _sut.RejectToIdleAsync(id, default); Assert.False(result.Ok); } // ─── ClearReviewFeedbackAsync ───────────────────────────────────────── [Fact] public async Task ClearReviewFeedbackAsync_RemovesFeedback_WithoutChangingStatus() { var id = await SeedTaskAsync(TaskStatus.WaitingForReview); await _sut.RejectToQueueAsync(id, "feedback", default); // sets feedback + Queued var result = await _sut.ClearReviewFeedbackAsync(id, default); Assert.True(result.Ok); var t = await GetTaskAsync(id); Assert.Null(t.ReviewFeedback); Assert.Equal(TaskStatus.Queued, t.Status); } // ─── CancelAsync from WaitingForReview ──────────────────────────────── [Fact] public async Task CancelAsync_FromWaitingForReview_TransitionsToCancelled() { var id = await SeedTaskAsync(TaskStatus.WaitingForReview); var result = await _sut.CancelAsync(id, DateTime.UtcNow, default); Assert.True(result.Ok); Assert.Equal(TaskStatus.Cancelled, (await GetTaskAsync(id)).Status); } }