119 lines
4.2 KiB
C#
119 lines
4.2 KiB
C#
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Hub;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Hub;
|
|
|
|
public sealed class SetTaskStatusHubTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
|
|
public void Dispose() => _db.Dispose();
|
|
|
|
private WorkerHub CreateHub()
|
|
{
|
|
var factory = _db.CreateFactory();
|
|
var built = TaskStateServiceBuilder.Build(factory);
|
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
|
var hub = new WorkerHub(
|
|
null!, null!, null!, null!, broadcaster, factory,
|
|
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
|
built.State,
|
|
null!, null!,
|
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
|
hub.Context = new FakeHubCallerContext();
|
|
return hub;
|
|
}
|
|
|
|
private async Task<string> SeedTaskAsync(TaskStatus status)
|
|
{
|
|
using var ctx = _db.CreateContext();
|
|
var listId = Guid.NewGuid().ToString();
|
|
await new ListRepository(ctx).AddAsync(new ListEntity
|
|
{
|
|
Id = listId, Name = "L", CreatedAt = DateTime.UtcNow,
|
|
});
|
|
|
|
var taskId = Guid.NewGuid().ToString();
|
|
await new TaskRepository(ctx).AddAsync(new TaskEntity
|
|
{
|
|
Id = taskId, ListId = listId, Title = "T",
|
|
Status = status, CreatedAt = DateTime.UtcNow, CommitType = "feat",
|
|
});
|
|
return taskId;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskStatus_RunningTask_Throws_AndLeavesStatusUnchanged()
|
|
{
|
|
var taskId = await SeedTaskAsync(TaskStatus.Running);
|
|
var hub = CreateHub();
|
|
|
|
await Assert.ThrowsAsync<HubException>(() => hub.SetTaskStatus(taskId, "Done"));
|
|
|
|
await using var ctx = _db.CreateContext();
|
|
var task = await ctx.Tasks.FindAsync(taskId);
|
|
Assert.Equal(TaskStatus.Running, task!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskStatus_IdleTask_StillSucceeds()
|
|
{
|
|
var taskId = await SeedTaskAsync(TaskStatus.Idle);
|
|
var hub = CreateHub();
|
|
|
|
await hub.SetTaskStatus(taskId, "Cancelled");
|
|
|
|
await using var ctx = _db.CreateContext();
|
|
var task = await ctx.Tasks.FindAsync(taskId);
|
|
Assert.Equal(TaskStatus.Cancelled, task!.Status);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskStatus_Idle_OnCancelledTask_ClearsTimestampsAndKeepsWorktree()
|
|
{
|
|
var taskId = await SeedTaskAsync(TaskStatus.Cancelled);
|
|
await using (var seed = _db.CreateContext())
|
|
{
|
|
var t = (await seed.Tasks.FindAsync(taskId))!;
|
|
t.StartedAt = DateTime.UtcNow.AddMinutes(-5);
|
|
t.FinishedAt = DateTime.UtcNow;
|
|
t.Result = "stale";
|
|
await seed.SaveChangesAsync();
|
|
await new WorktreeRepository(seed).AddAsync(new WorktreeEntity
|
|
{
|
|
TaskId = taskId, Path = "/tmp/x", BranchName = "claudedo/x",
|
|
BaseCommit = "deadbeef", State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
|
|
});
|
|
}
|
|
var hub = CreateHub();
|
|
|
|
await hub.SetTaskStatus(taskId, "Idle");
|
|
|
|
await using var ctx = _db.CreateContext();
|
|
var task = await ctx.Tasks.AsNoTracking().SingleAsync(t => t.Id == taskId);
|
|
Assert.Equal(TaskStatus.Idle, task.Status);
|
|
Assert.Null(task.StartedAt);
|
|
Assert.Null(task.FinishedAt);
|
|
Assert.Null(task.Result);
|
|
var wt = await ctx.Worktrees.AsNoTracking().SingleAsync(w => w.TaskId == taskId);
|
|
Assert.Equal(WorktreeState.Active, wt.State);
|
|
Assert.Equal("claudedo/x", wt.BranchName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetTaskStatus_Missing_Throws()
|
|
{
|
|
var hub = CreateHub();
|
|
|
|
await Assert.ThrowsAsync<HubException>(() => hub.SetTaskStatus("does-not-exist", "Done"));
|
|
}
|
|
}
|