fix(merge): pass the stash consent to the conflict resolver so it opens on a dirty target

The resolver restarts the merge via StartConflictMerge, which had no autostash.
After a stash-retry approve reported 'conflict', the restart hit the same dirty
target, got 'dirty_target' and the resolver silently never opened. Thread the
consent through the hub, client, coordinator and resolver; surface a refusal in
the footer instead of dropping it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-10-02 17:25:52 +02:00
co-authored by Claude Sonnet 5.5
parent ff7197c410
commit feb0afa33a
16 changed files with 173 additions and 20 deletions
@@ -30,9 +30,14 @@ public class ConflictResolverViewModelTests
public string? ContinuedPlanningParent;
public string? AbortedPlanningParent;
public override Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
/// <summary>Models a target working tree with uncommitted changes: refused unless the caller consented to the stash.</summary>
public bool DirtyTarget;
public override Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch, bool autostash = false)
{
StartCalled = true;
if (DirtyTarget && !autostash)
return Task.FromResult(new MergeResultDto("dirty_target", System.Array.Empty<string>(), "target working tree has uncommitted changes"));
return Task.FromResult(new MergeResultDto("conflict", new[] { "README.md" }, null));
}
@@ -63,6 +68,19 @@ public class ConflictResolverViewModelTests
public override Task AbortConflictMergeAsync(string taskId) { Aborted = true; return Task.CompletedTask; }
}
[Fact]
public async Task OpenAsync_DirtyTarget_OpensOnlyWithStashConsent()
{
// Regression: a dirty target refused the resolver's own merge restart, so it never opened.
var refused = new ConflictResolverViewModel(new FakeWorker { DirtyTarget = true }, "task-1");
Assert.False(await refused.OpenAsync("main"));
Assert.Equal("target working tree has uncommitted changes", refused.Error);
var consented = new ConflictResolverViewModel(new FakeWorker { DirtyTarget = true }, "task-1");
Assert.True(await consented.OpenAsync("main", autostash: true));
Assert.Null(consented.Error);
}
[Fact]
public async Task OpenAsync_LoadsConflicts_AndBlocksContinueUntilResolved()
{