fix(worker): honour targetBranch in MergeAsync by checking out before merge

Add GitService.CheckoutBranchAsync; compare targetBranch to current HEAD
before MergeNoFfAsync and switch when they differ. Returns Blocked if the
branch does not exist. Add three new tests (two service, one GitService).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-22 10:25:35 +02:00
parent 1bc7fcc609
commit 953d93179d
4 changed files with 86 additions and 0 deletions

View File

@@ -155,6 +155,20 @@ public class GitServiceMergeTests : IDisposable
Assert.False(await git.IsMidMergeAsync(repo.RepoDir));
}
[Fact]
public async Task CheckoutBranchAsync_ExistingBranch_SwitchesBranch()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var repo = NewRepo();
GitRepoFixture.RunGit(repo.RepoDir, "branch", "feature/checkout-test");
var git = new GitService();
await git.CheckoutBranchAsync(repo.RepoDir, "feature/checkout-test");
var current = await git.GetCurrentBranchAsync(repo.RepoDir);
Assert.Equal("feature/checkout-test", current);
}
[Fact]
public async Task ListConflictedFilesAsync_MidConflict_ReturnsConflictedFile()
{

View File

@@ -293,6 +293,64 @@ public class TaskMergeServiceTests : IDisposable
Assert.Equal("blocked", result.Status);
Assert.Contains("uncommitted", result.ErrorMessage ?? "");
}
[Fact]
public async Task MergeAsync_TargetBranchDifferentFromHead_ChecksOutBeforeMerging()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var repo = NewRepo();
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
// Create a feature branch in the repo (from current HEAD).
GitRepoFixture.RunGit(repo.RepoDir, "branch", "feature/target");
var wtMgr = BuildWorktreeManager(db);
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "feat.txt"), "data\n");
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
var (svc, _) = BuildService(db);
// Repo is currently on main/master; request merge into feature/target.
var result = await svc.MergeAsync(task.Id, "feature/target", removeWorktree: false,
commitMessage: "Merge into feature", ct: CancellationToken.None);
Assert.Equal("merged", result.Status);
// HEAD must now be feature/target.
var head = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
Assert.Equal("feature/target", head);
// The merged file must exist on feature/target.
Assert.True(File.Exists(Path.Combine(repo.RepoDir, "feat.txt")));
}
[Fact]
public async Task MergeAsync_TargetBranchDoesNotExist_ReturnsBlocked()
{
if (!GitRepoFixture.IsGitAvailable()) return;
var repo = NewRepo();
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
var wtMgr = BuildWorktreeManager(db);
var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
_wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));
File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "x.txt"), "x\n");
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
var (svc, _) = BuildService(db);
var result = await svc.MergeAsync(task.Id, "nonexistent/branch", removeWorktree: false,
commitMessage: "Merge", ct: CancellationToken.None);
Assert.Equal("blocked", result.Status);
Assert.Contains("switch target branch", result.ErrorMessage ?? "");
}
}
#region Test doubles