Merge claudedo/973ea49ec0a642ea9b1ddcff9b71b6fe

This commit is contained in:
mika kuns
2026-08-06 13:47:35 +02:00
10 changed files with 205 additions and 5 deletions
@@ -20,7 +20,8 @@ public static class TaskStateServiceBuilder
CountingQueueWaker Waker,
RunCancellationRegistry RunCancels);
public static Built Build(IDbContextFactory<ClaudeDoDbContext> dbFactory)
public static Built Build(
IDbContextFactory<ClaudeDoDbContext> dbFactory, Func<IActiveMergeState>? mergeState = null)
{
var hub = new CapturingHubContext();
var broadcaster = new HubBroadcaster(hub);
@@ -35,12 +36,19 @@ public static class TaskStateServiceBuilder
waker,
chain,
runCancels,
mergeState ?? (() => NoActiveMergeState.Instance),
NullLogger<TaskStateService>.Instance);
return new Built(state, chain, hub, () => waker.Count, waker, runCancels);
}
}
file sealed class NoActiveMergeState : IActiveMergeState
{
public static readonly NoActiveMergeState Instance = new();
public bool HasActiveMerge(string taskId) => false;
}
public sealed class CountingQueueWaker : IQueueWaker
{
private int _count;
@@ -1,6 +1,7 @@
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.EntityFrameworkCore;
@@ -8,6 +9,12 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.State;
file sealed class FakeActiveMergeState : IActiveMergeState
{
public HashSet<string> ActiveTaskIds { get; } = new();
public bool HasActiveMerge(string taskId) => ActiveTaskIds.Contains(taskId);
}
public sealed class TaskStateServiceTests : IDisposable
{
private readonly DbFixture _db = new();
@@ -319,6 +326,34 @@ public sealed class TaskStateServiceTests : IDisposable
Assert.Equal(TaskStatus.Done, await GetStatusAsync(id));
}
[Fact]
public async Task CancelAsync_WhileUnitMergeDraining_Rejects_AndDoesNotMutate()
{
var mergeState = new FakeActiveMergeState();
var built = TaskStateServiceBuilder.Build(_factory, () => mergeState);
var id = await SeedTaskAsync(TaskStatus.WaitingForReview);
mergeState.ActiveTaskIds.Add(id);
var result = await built.State.CancelAsync(id, DateTime.UtcNow, default);
Assert.False(result.Ok);
Assert.Equal(TaskStatus.WaitingForReview, await GetStatusAsync(id));
}
[Fact]
public async Task CancelAsync_AfterUnitMergeDrainCompletes_Succeeds()
{
var mergeState = new FakeActiveMergeState();
var built = TaskStateServiceBuilder.Build(_factory, () => mergeState);
var id = await SeedTaskAsync(TaskStatus.WaitingForReview);
// Merge drain finished (or never started) for this task — cancel behaves as before.
var result = await built.State.CancelAsync(id, DateTime.UtcNow, default);
Assert.True(result.Ok);
Assert.Equal(TaskStatus.Cancelled, await GetStatusAsync(id));
}
// ─── ResetToIdleAsync ─────────────────────────────────────────────────
[Fact]