feat(ui): Review-Merge-Button — Claude nimmt Waiting-for-Review pro Liste ab
Neuer Button in der virtuellen Review-Liste: öffnet das Merge-Helper-Auswahlmodal im Review-Modus (alle WaitingForReview-Tasks repo-verknüpfter Listen, gruppiert unter Listen-Headern, alle vorgetickt) und startet pro gewählter Liste eine ConPTY-Handler-Session direkt in der Merge-Phase — via bestehendem Handoff-Spec (nextPhase "merge"), kein Triage/Wait davor. Handler-Host-Task wie beim vollen "Let Claude handle it"-Lauf (HandlerBaseCommit, IsManual).
This commit is contained in:
@@ -383,6 +383,115 @@ public class MergeHelperSelectionModalViewModelTests : IDisposable
|
||||
Assert.False(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
|
||||
}
|
||||
|
||||
// ---- Review mode (ConfigureForReview): all lists, WaitingForReview only ----
|
||||
|
||||
private async Task SeedReviewAcrossListsAsync()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", WorkingDir = "C:\\repo1", CreatedAt = DateTime.UtcNow });
|
||||
db.Lists.Add(new ListEntity { Id = "L2", Name = "Home", WorkingDir = "C:\\repo2", CreatedAt = DateTime.UtcNow });
|
||||
db.Lists.Add(new ListEntity { Id = "L3", Name = "NoRepo", CreatedAt = DateTime.UtcNow });
|
||||
var order = 0;
|
||||
void Add(string id, string listId, TaskStatus status, bool isManual = false) =>
|
||||
db.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Number = TestTaskNumbers.Next(),
|
||||
Id = id, ListId = listId, Title = $"task {id}", IsManual = isManual,
|
||||
Status = status, CreatedAt = DateTime.UtcNow, SortOrder = order++,
|
||||
});
|
||||
|
||||
Add("r1", "L1", TaskStatus.WaitingForReview);
|
||||
Add("r2", "L1", TaskStatus.WaitingForReview);
|
||||
Add("i1", "L1", TaskStatus.Idle); // wrong status
|
||||
Add("m1", "L1", TaskStatus.WaitingForReview, isManual: true); // handler/reminder task
|
||||
Add("r3", "L2", TaskStatus.WaitingForReview);
|
||||
Add("r4", "L3", TaskStatus.WaitingForReview); // list without working dir
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewMode_LoadsOnlyReviewTasksOfRepoLists_GroupedWithHeaders_AllTicked()
|
||||
{
|
||||
await SeedReviewAcrossListsAsync();
|
||||
var vm = BuildVm();
|
||||
vm.ConfigureForReview();
|
||||
await vm.LoadAsync();
|
||||
|
||||
// Ordered by list name: Home (L2) before Work (L1); L3 (no repo), i1, m1 excluded.
|
||||
Assert.Equal(new[] { "header:L2", "r3", "header:L1", "r1", "r2" }, vm.Tasks.Select(t => t.Id));
|
||||
Assert.True(vm.Tasks[0].IsListHeader);
|
||||
Assert.Equal("Home", vm.Tasks[0].Title);
|
||||
Assert.True(vm.Tasks[2].IsListHeader);
|
||||
Assert.Equal("Work", vm.Tasks[2].Title);
|
||||
Assert.All(vm.Tasks.Where(t => !t.IsListHeader), t => Assert.True(t.IsSelected));
|
||||
Assert.All(vm.Tasks.Where(t => t.IsListHeader), t => Assert.False(t.IsSelected));
|
||||
Assert.True(vm.CanConfirm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewMode_SelectAll_DoesNotTickHeaders()
|
||||
{
|
||||
await SeedReviewAcrossListsAsync();
|
||||
var vm = BuildVm();
|
||||
vm.ConfigureForReview();
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.SelectNoneCommand.Execute(null);
|
||||
vm.SelectAllCommand.Execute(null);
|
||||
|
||||
Assert.All(vm.Tasks.Where(t => t.IsListHeader), t => Assert.False(t.IsSelected));
|
||||
Assert.All(vm.Tasks.Where(t => !t.IsListHeader), t => Assert.True(t.IsSelected));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewMode_ConfirmAndGroupSelectionByList_GroupsInRowOrder()
|
||||
{
|
||||
await SeedReviewAcrossListsAsync();
|
||||
var vm = BuildVm();
|
||||
vm.ConfigureForReview();
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.ConfirmCommand.Execute(null);
|
||||
var ids = await vm.Result.Task;
|
||||
Assert.NotNull(ids);
|
||||
Assert.Equal(new[] { "r3", "r1", "r2" }, ids);
|
||||
|
||||
var groups = vm.GroupSelectionByList(ids!);
|
||||
Assert.Equal(2, groups.Count);
|
||||
Assert.Equal("L2", groups[0].ListId);
|
||||
Assert.Equal(new[] { "r3" }, groups[0].TaskIds);
|
||||
Assert.Equal("L1", groups[1].ListId);
|
||||
Assert.Equal(new[] { "r1", "r2" }, groups[1].TaskIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewMode_ReconcileTick_KeepsUnticks_AndPicksUpNewReviewTasks()
|
||||
{
|
||||
await SeedReviewAcrossListsAsync();
|
||||
var vm = BuildVm();
|
||||
vm.ConfigureForReview();
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.Tasks.Single(t => t.Id == "r1").IsSelected = false;
|
||||
|
||||
await using (var db = NewContext())
|
||||
{
|
||||
db.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Number = TestTaskNumbers.Next(),
|
||||
Id = "r5", ListId = "L1", Title = "arrived later",
|
||||
Status = TaskStatus.WaitingForReview, CreatedAt = DateTime.UtcNow, SortOrder = 99,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await vm.ReconcileTickAsync();
|
||||
|
||||
Assert.False(vm.Tasks.Single(t => t.Id == "r1").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "r5").IsSelected);
|
||||
Assert.Contains(vm.Tasks, t => t.Id == "header:L1" && t.IsListHeader);
|
||||
}
|
||||
|
||||
private sealed class CountingDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
|
||||
Reference in New Issue
Block a user