feat(worker): let review_task/merge_task leave conflicts in tree via MCP
This commit is contained in:
+111
-10
@@ -216,7 +216,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, CancellationToken.None);
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal("Done", result.Task.Status);
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.MergeStatus);
|
||||
@@ -230,7 +230,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sut.ReviewTask(task.Id, "reject_rerun", null, null, CancellationToken.None));
|
||||
sut.ReviewTask(task.Id, "reject_rerun", null, null, cancellationToken: CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -240,7 +240,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.ReviewTask(task.Id, "reject_rerun", "fix it", null, CancellationToken.None);
|
||||
var result = await sut.ReviewTask(task.Id, "reject_rerun", "fix it", null, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal("Queued", result.Task.Status);
|
||||
var loaded = await new TaskRepository(_db.CreateContext()).GetByIdAsync(task.Id);
|
||||
@@ -255,7 +255,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sut.ReviewTask(task.Id, "bogus", null, null, CancellationToken.None));
|
||||
sut.ReviewTask(task.Id, "bogus", null, null, cancellationToken: CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -275,7 +275,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
}
|
||||
|
||||
var sut = BuildSut(CreateQueue());
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, CancellationToken.None);
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.MergeStatus);
|
||||
Assert.Equal("Done", result.Task.Status);
|
||||
@@ -321,7 +321,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
}
|
||||
|
||||
var sut = BuildSut(CreateQueue());
|
||||
var result = await sut.ReviewTask(parentId, "approve", null, "main", CancellationToken.None);
|
||||
var result = await sut.ReviewTask(parentId, "approve", null, "main", cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.MergeStatus);
|
||||
Assert.Equal("Done", result.Task.Status);
|
||||
@@ -610,7 +610,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.MergeTask(task.Id, "main", true, false, false, CancellationToken.None));
|
||||
() => sut.MergeTask(task.Id, "main", true, false, false, cancellationToken: CancellationToken.None));
|
||||
Assert.Contains("Done", ex.Message);
|
||||
}
|
||||
|
||||
@@ -785,7 +785,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.MergeTask(task.Id, "main", true, false, false, CancellationToken.None));
|
||||
() => sut.MergeTask(task.Id, "main", true, false, false, cancellationToken: CancellationToken.None));
|
||||
Assert.Contains("Done", ex.Message);
|
||||
}
|
||||
|
||||
@@ -797,7 +797,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var (task, _, _) = await SeedWorktreeAsync(TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.MergeTask(task.Id, "main", true, dryRun: true, allowWaitingForReview: true, CancellationToken.None);
|
||||
var result = await sut.MergeTask(task.Id, "main", true, dryRun: true, allowWaitingForReview: true, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.False(result.Merged);
|
||||
Assert.Null(result.MergeCommit);
|
||||
@@ -817,13 +817,114 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var target = await new GitService().GetCurrentBranchAsync(list.WorkingDir, CancellationToken.None);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.MergeTask(task.Id, target, true, dryRun: false, allowWaitingForReview: true, CancellationToken.None);
|
||||
var result = await sut.MergeTask(task.Id, target, true, dryRun: false, allowWaitingForReview: true, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.True(result.Merged);
|
||||
var reloaded = await new TaskRepository(_db.CreateContext()).GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.Done, reloaded!.Status);
|
||||
}
|
||||
|
||||
// ── leaveConflictsInTree ──────────────────────────────────────────────────
|
||||
|
||||
private async Task<(TaskEntity task, ListEntity list, WorktreeContext wt)> SeedConflictingWorktreeAsync(
|
||||
TaskStatus status = TaskStatus.WaitingForReview)
|
||||
{
|
||||
var (task, list, wt) = await SeedWorktreeAsync(status);
|
||||
|
||||
File.WriteAllText(Path.Combine(wt.WorktreePath, "README.md"), "# from worktree\n");
|
||||
GitRepoFixture.RunGit(wt.WorktreePath, "add", "README.md");
|
||||
GitRepoFixture.RunGit(wt.WorktreePath, "commit", "-m", "worktree edit");
|
||||
|
||||
File.WriteAllText(Path.Combine(list.WorkingDir!, "README.md"), "# from main\n");
|
||||
GitRepoFixture.RunGit(list.WorkingDir!, "add", "README.md");
|
||||
GitRepoFixture.RunGit(list.WorkingDir!, "commit", "-m", "main edit");
|
||||
|
||||
return (task, list, wt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeTask_LeaveConflictsInTree_LeavesMarkersAndKeepsRepoMidMerge()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, list, _) = await SeedConflictingWorktreeAsync(TaskStatus.WaitingForReview);
|
||||
var target = await new GitService().GetCurrentBranchAsync(list.WorkingDir!);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.MergeTask(task.Id, target, true, dryRun: false,
|
||||
allowWaitingForReview: true, leaveConflictsInTree: true, CancellationToken.None);
|
||||
|
||||
Assert.False(result.Merged);
|
||||
Assert.True(result.ConflictsInTree);
|
||||
Assert.Equal(list.WorkingDir, result.RepoPath);
|
||||
Assert.Contains("README.md", result.Conflicts);
|
||||
Assert.Contains("<<<<<<<", File.ReadAllText(Path.Combine(list.WorkingDir!, "README.md")));
|
||||
Assert.True(await new GitService().IsMidMergeAsync(list.WorkingDir!));
|
||||
var reloaded = await new TaskRepository(_db.CreateContext()).GetByIdAsync(task.Id);
|
||||
Assert.Equal(TaskStatus.WaitingForReview, reloaded!.Status);
|
||||
|
||||
GitRepoFixture.RunGit(list.WorkingDir!, "merge", "--abort");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeTask_ConflictWithoutLeaveFlag_AbortsCleanly()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, list, _) = await SeedConflictingWorktreeAsync(TaskStatus.WaitingForReview);
|
||||
var target = await new GitService().GetCurrentBranchAsync(list.WorkingDir!);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.MergeTask(task.Id, target, true, dryRun: false,
|
||||
allowWaitingForReview: true, leaveConflictsInTree: false, CancellationToken.None);
|
||||
|
||||
Assert.False(result.Merged);
|
||||
Assert.False(result.ConflictsInTree);
|
||||
Assert.Null(result.RepoPath);
|
||||
Assert.Contains("README.md", result.Conflicts);
|
||||
Assert.DoesNotContain("<<<<<<<", File.ReadAllText(Path.Combine(list.WorkingDir!, "README.md")));
|
||||
Assert.False(await new GitService().IsMidMergeAsync(list.WorkingDir!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewTask_Approve_LeaveConflictsInTree_LeavesMarkersAndReportsRepoPath()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, list, _) = await SeedConflictingWorktreeAsync(TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null,
|
||||
leaveConflictsInTree: true, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusConflict, result.MergeStatus);
|
||||
Assert.Equal(list.WorkingDir, result.RepoPath);
|
||||
Assert.Contains("continue_merge", result.MergeMessage);
|
||||
Assert.Contains("<<<<<<<", File.ReadAllText(Path.Combine(list.WorkingDir!, "README.md")));
|
||||
Assert.True(await new GitService().IsMidMergeAsync(list.WorkingDir!));
|
||||
Assert.Equal("WaitingForReview", result.Task.Status);
|
||||
|
||||
GitRepoFixture.RunGit(list.WorkingDir!, "merge", "--abort");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewTask_Approve_ConflictWithoutLeaveFlag_KeepsOldBehaviour()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, list, _) = await SeedConflictingWorktreeAsync(TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, cancellationToken: CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusConflict, result.MergeStatus);
|
||||
Assert.Null(result.RepoPath);
|
||||
Assert.Contains("ClaudeDo UI", result.MergeMessage);
|
||||
Assert.DoesNotContain("<<<<<<<", File.ReadAllText(Path.Combine(list.WorkingDir!, "README.md")));
|
||||
Assert.False(await new GitService().IsMidMergeAsync(list.WorkingDir!));
|
||||
Assert.Equal("WaitingForReview", result.Task.Status);
|
||||
}
|
||||
|
||||
// ── AddTask model override ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user