fix(claude-do): merge Status-Force-Bypässe auf laufende Tasks schließen (MCP + UI-
ClaudeDo-Task: 3b2c4015-226b-41a1-80c4-3a8d5537f2a5
This commit is contained in:
@@ -900,6 +900,21 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
sut.DeleteTask(task.Id, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTask_WithChildren_ThrowsUnderstandableMessage()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var parent = await SeedTaskAsync(listId, status: TaskStatus.WaitingForChildren);
|
||||
await SeedTaskAsync(listId, status: TaskStatus.Idle, parentId: parent.Id);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sut.DeleteTask(parent.Id, CancellationToken.None));
|
||||
|
||||
Assert.Contains("child task", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.NotNull(await _tasks.GetByIdAsync(parent.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteTask_NotFound_Throws()
|
||||
{
|
||||
@@ -981,6 +996,35 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
Assert.Equal(TaskStatus.WaitingForReview, loaded!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTaskStatus_Idle_OnRunningTask_Throws()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Running);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.UpdateTaskStatus(task.Id, "Idle", CancellationToken.None));
|
||||
|
||||
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.Running, loaded!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTaskStatus_Done_OnRunningTask_Throws()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Running);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.UpdateTaskStatus(task.Id, "Done", CancellationToken.None));
|
||||
|
||||
Assert.Contains("running", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.Running, loaded!.Status);
|
||||
}
|
||||
|
||||
private ExternalMcpService NewService() => BuildSut(CreateQueue());
|
||||
|
||||
private async Task<string> SeedIdleTask(string title = "t")
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
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.Online.OnlineTokenStore(),
|
||||
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_Missing_Throws()
|
||||
{
|
||||
var hub = CreateHub();
|
||||
|
||||
await Assert.ThrowsAsync<HubException>(() => hub.SetTaskStatus("does-not-exist", "Done"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user