From b9896399fac8bae71b2cd4c13d562b5fd69a4e51 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Fri, 24 Apr 2026 18:18:49 +0200 Subject: [PATCH] feat(worker): add PlanningMergeOrchestrator.AbortAsync --- .../Planning/PlanningMergeOrchestrator.cs | 10 +++++++ .../PlanningMergeOrchestratorTests.cs | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs index d76cbf0..503ee38 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs @@ -82,6 +82,16 @@ public sealed class PlanningMergeOrchestrator await DrainAsync(planningTaskId, ct); } + public async Task AbortAsync(string planningTaskId, CancellationToken ct) + { + if (!_states.TryGetValue(planningTaskId, out var state) || state.CurrentSubtaskId is null) + throw new InvalidOperationException("no in-progress merge to abort"); + + await _merge.AbortMergeAsync(state.CurrentSubtaskId, ct); + _states.TryRemove(planningTaskId, out _); + await _broadcaster.PlanningMergeAborted(planningTaskId); + } + private async Task DrainAsync(string planningTaskId, CancellationToken ct) { if (!_states.TryGetValue(planningTaskId, out var state)) return; diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs index 561bf08..38a91b5 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningMergeOrchestratorTests.cs @@ -216,6 +216,35 @@ public sealed class PlanningMergeOrchestratorTests : IDisposable }); } + [Fact] + public async Task AbortAsync_AfterConflict_RestoresCleanRepoAndClearsState() + { + var db = NewDb(); + var repo = NewRepo(); + GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main"); + + var (parentId, subA, subB, _) = await SeedPlanningThreeChildrenMiddleConflictsAsync(db, repo); + + var (orch, spy) = BuildOrchestrator(db); + await orch.StartAsync(parentId, "main", CancellationToken.None); + + await orch.AbortAsync(parentId, CancellationToken.None); + + using var ctx = db.CreateContext(); + // Planning stays in Planned — NOT flipped to Done. + Assert.Equal(TaskStatus.Planned, ctx.Tasks.Single(t => t.Id == parentId).Status); + // Earlier successful merge stays merged. + Assert.Equal(WorktreeState.Merged, ctx.Worktrees.Single(w => w.TaskId == subA).State); + // Conflicted subtask's worktree stays Active (abort doesn't flip it). + Assert.Equal(WorktreeState.Active, ctx.Worktrees.Single(w => w.TaskId == subB).State); + + Assert.Contains(spy, c => c.Method == "PlanningMergeAborted" && (string)c.Args[0]! == parentId); + + // Repo no longer mid-merge. + var git = new GitService(); + Assert.False(await git.IsMidMergeAsync(repo.RepoDir, CancellationToken.None)); + } + private (PlanningMergeOrchestrator orch, List<(string Method, object?[] Args)> calls) BuildOrchestrator(DbFixture db) { var fakeHub = new OrchestratorFakeHubContext();