feat(worker): add review state transitions to TaskStateService
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
206
tests/ClaudeDo.Worker.Tests/State/ReviewTransitionTests.cs
Normal file
206
tests/ClaudeDo.Worker.Tests/State/ReviewTransitionTests.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
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<string> 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<TaskEntity> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user