fix(merge): conventional merge-commit default and live verify progress
The merge commit message was hand-rolled per caller ("Merge task: <title>",
"Merge <branch>", "Merge subtask") and ignored the task's commit type. Every
caller now passes a blank message and TaskMergeService fills in
CommitMessageBuilder.BuildMerge -> {commitType}(list-slug): merge <title> plus the
ClaudeDo-Task trailer; the merge modal prefills it from GetMergeTargets.
A merge whose list has a verify command holds the MergeTask call for minutes (5m46s
on this repo), during which the modal only disabled its button - no spinner, no
message, so a landed merge looked like a dead app. TaskMergeService now broadcasts
MergeProgress(taskId, phase, elapsedSeconds) for the merging and verifying phases
(re-reported every 30s) plus a WorkerLog line when verify starts; the modal shows a
spinner and the localized phase.
This commit is contained in:
@@ -283,6 +283,80 @@ public class TaskMergeServiceTests : IDisposable
|
||||
Assert.DoesNotContain(proxy.Calls, c => c.Method == "WorktreeUpdated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeAsync_BlankCommitMessage_UsesConventionalDefault()
|
||||
{
|
||||
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, "feature.txt"), "feat\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: false,
|
||||
commitMessage: "", ct: CancellationToken.None);
|
||||
|
||||
Assert.Equal("merged", result.Status);
|
||||
var subject = GitRepoFixture.RunGit(repo.RepoDir, "log", "-1", "--format=%s").Trim();
|
||||
// task.CommitType (not the list default) — same type the task's own commits used.
|
||||
Assert.Equal($"{task.CommitType}(merge-test): merge merge-task", subject);
|
||||
var body = GitRepoFixture.RunGit(repo.RepoDir, "log", "-1", "--format=%b");
|
||||
Assert.Contains($"ClaudeDo-Task: {task.Id}", body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeAsync_VerifyGate_BroadcastsMergingAndVerifyingPhases()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
|
||||
var repo = NewRepo();
|
||||
var db = NewDb();
|
||||
var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||
await SeedVerifyCommand(db, list.Id, "echo ok");
|
||||
|
||||
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, "feature.txt"), "feat\n");
|
||||
await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);
|
||||
|
||||
var (svc, proxy) = BuildService(db, new FakeVerifyCommandRunner());
|
||||
var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
|
||||
|
||||
var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: false,
|
||||
commitMessage: "", ct: CancellationToken.None);
|
||||
|
||||
Assert.Equal("merged", result.Status);
|
||||
Assert.Contains(proxy.Calls, c => c.Method == "MergeProgress"
|
||||
&& (string?)c.Args[0] == task.Id && (string?)c.Args[1] == TaskMergeService.PhaseMerging);
|
||||
Assert.Contains(proxy.Calls, c => c.Method == "MergeProgress"
|
||||
&& (string?)c.Args[0] == task.Id && (string?)c.Args[1] == TaskMergeService.PhaseVerifying);
|
||||
Assert.Contains(proxy.Calls, c => c.Method == "WorkerLog"
|
||||
&& c.Args[0] is string s && s.Contains("Verify command running"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTargetsAsync_ReturnsDefaultCommitMessage()
|
||||
{
|
||||
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||
var repo = NewRepo();
|
||||
var db = NewDb();
|
||||
var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);
|
||||
|
||||
var (svc, _) = BuildService(db);
|
||||
var targets = await svc.GetTargetsAsync(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal($"{task.CommitType}(merge-test): merge merge-task", targets.DefaultCommitMessage.Split('\n')[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTargetsAsync_ReturnsCurrentAndLocalBranches()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user