fix(merge): apply the verify gate to worktree-less approvals and report it everywhere

ApproveAndMergeAsync short-circuits to Done whenever a task has no active
worktree -- which is exactly how a list-handler task works, since it commits
straight into the list working dir. The verify command was skipped for the run
that lands the most on the target branch at once; the loaded command was even
discarded at the destructuring. It now runs under the same per-repo gate as the
merge path before the task may reach Done.

The two merge entry points that did not know verify_failed reported it as
"Unknown status: verify_failed" (merge modal, dropping the command output) and
as a generic Failed (worktrees batch, claiming the merge never happened).
This commit is contained in:
mika kuns
2026-08-06 10:20:53 +02:00
parent 730ecb1abc
commit 091aca521f
6 changed files with 75 additions and 4 deletions
@@ -714,6 +714,46 @@ public class TaskMergeServiceTests : IDisposable
Assert.Equal(TaskStatus.Done, updated!.Status);
}
// A list-handler task owns no worktree -- it commits straight into the list's working dir --
// so the approve path short-circuits past the merge. The verify gate must still apply, or the
// run that lands the most on the target branch would be the one run nothing checks.
[Fact]
public async Task ApproveAndMergeAsync_NoWorktreeAndVerifyFails_StaysOutOfDone()
{
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
await SeedVerifyCommand(db, list.Id, "dotnet test");
var fakeVerify = new FakeVerifyCommandRunner { Result = new VerifyCommandResult(1, false, "2 failed") };
var (svc, _) = BuildService(db, fakeVerify);
var result = await svc.ApproveAndMergeAsync(task.Id, "main", CancellationToken.None);
Assert.Equal(TaskMergeService.StatusVerifyFailed, result.Status);
Assert.Contains("2 failed", result.ErrorMessage ?? "");
Assert.Equal("/tmp", fakeVerify.CapturedWorkingDir);
using var ctx = db.CreateContext();
var updated = await new TaskRepository(ctx).GetByIdAsync(task.Id);
Assert.Equal(TaskStatus.WaitingForReview, updated!.Status);
}
[Fact]
public async Task ApproveAndMergeAsync_NoWorktreeAndVerifyPasses_MarksDone()
{
var db = NewDb();
var (list, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.WaitingForReview);
await SeedVerifyCommand(db, list.Id, "dotnet test");
var fakeVerify = new FakeVerifyCommandRunner { Result = new VerifyCommandResult(0, false, "ok") };
var (svc, _) = BuildService(db, fakeVerify);
var result = await svc.ApproveAndMergeAsync(task.Id, "main", CancellationToken.None);
Assert.Equal(TaskMergeService.StatusMerged, result.Status);
Assert.Equal("dotnet test", fakeVerify.CapturedCommand);
using var ctx = db.CreateContext();
var updated = await new TaskRepository(ctx).GetByIdAsync(task.Id);
Assert.Equal(TaskStatus.Done, updated!.Status);
}
[Fact]
public async Task ApproveAndMergeAsync_NoVerifyCommandConfigured_NeverInvokesRunnerAndMarksDone()
{