fix(worker): survive a worker restart when cancelling a mid-merge unit-merge parent
CancelAsync only checked PlanningMergeOrchestrator's in-memory HasActiveMerge, which is empty after a restart. IActiveMergeState gains an async fallback that checks on-disk (GitService.IsMidMergeAsync) for a WaitingForReview parent with children, so the guard still blocks cancel until the merge is continued or aborted.
This commit is contained in:
@@ -66,6 +66,7 @@ file sealed class NoActiveMergeState : IActiveMergeState
|
||||
{
|
||||
public static readonly NoActiveMergeState Instance = new();
|
||||
public bool HasActiveMerge(string taskId) => false;
|
||||
public Task<bool> HasActiveMergeAsync(string taskId, CancellationToken ct) => Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Never actually invoked: TicketSystemConfig.IsConfigured is false in every TaskStateServiceBuilder
|
||||
|
||||
@@ -162,6 +162,36 @@ public sealed class PlanningMergeOrchestratorTests : IDisposable
|
||||
Assert.Contains(spy, c => c.Method == "PlanningCompleted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CancelAsync_UnitMergeParentMidMergeAfterSimulatedRestart_BlocksUntilAborted()
|
||||
{
|
||||
var db = NewDb();
|
||||
var repo = NewRepo();
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||
|
||||
var (parentId, _, _, _) = await SeedPlanningThreeChildrenMiddleConflictsAsync(db, repo);
|
||||
|
||||
var (orch, _) = BuildOrchestrator(db);
|
||||
var startResult = await orch.StartAsync(parentId, "main", CancellationToken.None);
|
||||
Assert.Equal(TaskMergeService.StatusConflict, startResult.Status);
|
||||
|
||||
// Simulate a worker restart: a brand-new orchestrator instance has an empty in-memory
|
||||
// _states dictionary, so the guard must fall back to the on-disk mid-merge check.
|
||||
var (freshOrch, _) = BuildOrchestrator(db);
|
||||
var cancelBuilt = TaskStateServiceBuilder.Build(db.CreateFactory(), () => freshOrch);
|
||||
|
||||
var blocked = await cancelBuilt.State.CancelAsync(parentId, DateTime.UtcNow, CancellationToken.None);
|
||||
Assert.False(blocked.Ok);
|
||||
|
||||
using (var ctx = db.CreateContext())
|
||||
Assert.Equal(TaskStatus.WaitingForReview, ctx.Tasks.Single(t => t.Id == parentId).Status);
|
||||
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "merge", "--abort");
|
||||
|
||||
var ok = await cancelBuilt.State.CancelAsync(parentId, DateTime.UtcNow, CancellationToken.None);
|
||||
Assert.True(ok.Ok);
|
||||
}
|
||||
|
||||
private async Task<(string parentId, string subA, string subB, string subC)> SeedPlanningThreeChildrenMiddleConflictsAsync(
|
||||
DbFixture db, GitRepoFixture repo)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ file sealed class FakeActiveMergeState : IActiveMergeState
|
||||
{
|
||||
public HashSet<string> ActiveTaskIds { get; } = new();
|
||||
public bool HasActiveMerge(string taskId) => ActiveTaskIds.Contains(taskId);
|
||||
public Task<bool> HasActiveMergeAsync(string taskId, CancellationToken ct) => Task.FromResult(HasActiveMerge(taskId));
|
||||
}
|
||||
|
||||
public sealed class TaskStateServiceTests : IDisposable
|
||||
|
||||
Reference in New Issue
Block a user