refactor(merge): single IMergeCoordinator replaces the 5 conflict seams

The RequestConflictResolution Func was declared on 5 VMs and hand-threaded shell->details->merge-section->diff->merge-modal. Replaced with a DI-singleton IMergeCoordinator (MergeCoordinator holder; shell wires its Handler at composition, breaking the shell<->island cycle). Invokers (MergeModal, DetailsIsland, WorktreesOverview) depend on the interface; the two pass-through VMs (DiffModal, MergeSection) drop the seam entirely. No behavior change; conflict-seam + batch tests rewired to assert via the coordinator.
This commit is contained in:
Mika Kuns
2026-06-22 17:18:57 +02:00
parent 3f9f047955
commit 5be4b5c5fb
15 changed files with 79 additions and 75 deletions

View File

@@ -64,10 +64,10 @@ public class DetailsIslandConflictSeamTests : IDisposable
Task.FromResult<MergeResultDto?>(new MergeResultDto("conflict", new[] { "a.cs" }, null));
}
private DetailsIslandViewModel BuildVm(StubWorkerClient worker)
private DetailsIslandViewModel BuildVm(StubWorkerClient worker, IMergeCoordinator merge)
{
var factory = new TestDbFactory(NewContext);
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi());
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi(), merge);
}
[Fact]
@@ -75,19 +75,22 @@ public class DetailsIslandConflictSeamTests : IDisposable
{
const string taskId = "task-conflict-1";
var vm = BuildVm(new ConflictApproveWorkerClient());
vm.Bind(new TaskRowViewModel { Id = taskId, Status = TaskStatus.WaitingForReview });
vm.Merge.SelectedMergeTarget = "main";
string? capturedTaskId = null;
string? capturedTarget = null;
vm.Merge.RequestConflictResolution = (tid, target) =>
var coordinator = new MergeCoordinator
{
capturedTaskId = tid;
capturedTarget = target;
return Task.CompletedTask;
Handler = (tid, target) =>
{
capturedTaskId = tid;
capturedTarget = target;
return Task.CompletedTask;
},
};
var vm = BuildVm(new ConflictApproveWorkerClient(), coordinator);
vm.Bind(new TaskRowViewModel { Id = taskId, Status = TaskStatus.WaitingForReview });
vm.Merge.SelectedMergeTarget = "main";
await vm.ApproveReviewCommand.ExecuteAsync(null);
Assert.Equal(taskId, capturedTaskId);

View File

@@ -66,7 +66,7 @@ public class DetailsIslandPlanningTests : IDisposable
private DetailsIslandViewModel BuildVm(StubWorkerClient worker)
{
var factory = new TestDbFactory(NewContext);
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi());
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi(), new ClaudeDo.Ui.Services.MergeCoordinator());
}
// Connected worker whose review calls fail the way the hub does when the task

View File

@@ -51,7 +51,7 @@ public class DetailsIslandPrepModeTests : IDisposable
private DetailsIslandViewModel NewDetailsVm(StubWorkerClient stub)
{
var factory = new TestDbFactory(NewContext);
return new DetailsIslandViewModel(factory, stub, new NullServiceProvider(), new StubNotesApi());
return new DetailsIslandViewModel(factory, stub, new NullServiceProvider(), new StubNotesApi(), new ClaudeDo.Ui.Services.MergeCoordinator());
}
private sealed class NullServiceProvider : IServiceProvider

View File

@@ -69,7 +69,7 @@ public class DetailsIslandReviewActionsTests : IDisposable
private DetailsIslandViewModel BuildVm(StubWorkerClient worker)
{
var factory = new TestDbFactory(NewContext);
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi());
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi(), new ClaudeDo.Ui.Services.MergeCoordinator());
}
[Fact]

View File

@@ -57,7 +57,7 @@ public class DetailsIslandTabsTests : IDisposable
private DetailsIslandViewModel NewVm()
{
var factory = new TestDbFactory(NewContext);
return new DetailsIslandViewModel(factory, new DefaultStub(), new NullServiceProvider(), new StubNotesApi());
return new DetailsIslandViewModel(factory, new DefaultStub(), new NullServiceProvider(), new StubNotesApi(), new ClaudeDo.Ui.Services.MergeCoordinator());
}
[Fact]

View File

@@ -30,8 +30,9 @@ public class WorktreesOverviewBatchMergeTests
Assert.False(row.IsConflict);
}
private static WorktreesOverviewModalViewModel NewVm() =>
new(new ClaudeDo.Ui.Services.WorkerClient("http://127.0.0.1:1/hub"), () => null!);
private static WorktreesOverviewModalViewModel NewVm(IMergeCoordinator? merge = null) =>
new(new ClaudeDo.Ui.Services.WorkerClient("http://127.0.0.1:1/hub"), () => null!,
merge ?? new ClaudeDo.Ui.Services.MergeCoordinator());
private static MergeResultDto Merged() => new("merged", System.Array.Empty<string>(), null);
private static MergeResultDto Conflict() => new("conflict", new[] { "f.cs" }, null);
@@ -137,13 +138,15 @@ public class WorktreesOverviewBatchMergeTests
[Fact]
public void ResolveConflict_invokes_seam_with_task_and_target()
{
var vm = NewVm();
(string Task, string Target)? captured = null;
var coordinator = new ClaudeDo.Ui.Services.MergeCoordinator
{
Handler = (taskId, target) => { captured = (taskId, target); return System.Threading.Tasks.Task.CompletedTask; },
};
var vm = NewVm(coordinator);
vm.SelectedTarget = "release";
var row = ActiveRow("x"); row.MergeOutcome = BatchMergeOutcome.Conflict;
(string Task, string Target)? captured = null;
vm.RequestConflictResolution = (taskId, target) => { captured = (taskId, target); return System.Threading.Tasks.Task.CompletedTask; };
vm.ResolveConflictCommand.Execute(row);
Assert.Equal(("x", "release"), captured);