using System.Collections.ObjectModel; using System.ComponentModel; using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.Tests.ViewModels; public class DetailsIslandPlanningTests : IDisposable { private readonly string _dbPath; public DetailsIslandPlanningTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_details_test_{Guid.NewGuid():N}.db"); using var ctx = NewContext(); ctx.Database.EnsureCreated(); } public void Dispose() { try { File.Delete(_dbPath); } catch { } try { File.Delete(_dbPath + "-wal"); } catch { } try { File.Delete(_dbPath + "-shm"); } catch { } } private ClaudeDoDbContext NewContext() { var opts = new DbContextOptionsBuilder() .UseSqlite($"Data Source={_dbPath}") .Options; return new ClaudeDoDbContext(opts); } private sealed class TestDbFactory : IDbContextFactory { private readonly Func _create; public TestDbFactory(Func create) => _create = create; public ClaudeDoDbContext CreateDbContext() => _create(); } private sealed class FakeWorkerClient : IWorkerClient { public event PropertyChangedEventHandler? PropertyChanged; public event Action? TaskStartedEvent; public event Action? TaskFinishedEvent; public event Action? TaskUpdatedEvent; public event Action? WorktreeUpdatedEvent; public event Action? TaskMessageEvent; public event Action? PlanningMergeStartedEvent; public event Action? PlanningSubtaskMergedEvent; public event Action>? PlanningMergeConflictEvent; public event Action? PlanningMergeAbortedEvent; public event Action? PlanningCompletedEvent; public bool IsConnected => false; public MergeTargetsDto? MergeTargetsResult { get; set; } public Task WakeQueueAsync() => Task.CompletedTask; public Task RunNowAsync(string taskId) => Task.CompletedTask; public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask; public Task ResetTaskAsync(string taskId) => Task.CompletedTask; public Task CancelTaskAsync(string taskId) => Task.CompletedTask; public Task> GetAgentsAsync() => Task.FromResult(new List()); public Task GetListConfigAsync(string listId) => Task.FromResult(null); public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => Task.CompletedTask; public Task GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0); public Task GetMergeTargetsAsync(string taskId) => Task.FromResult(MergeTargetsResult); public Task> GetPlanningAggregateAsync(string planningTaskId) => Task.FromResult>(Array.Empty()); public Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult(null); public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; } private sealed class NullServiceProvider : IServiceProvider { public object? GetService(Type serviceType) => null; } private DetailsIslandViewModel BuildVm(FakeWorkerClient worker) { var factory = new TestDbFactory(NewContext); return new DetailsIslandViewModel(factory, worker, new NullServiceProvider()); } private static SubtaskRowViewModel MakeSubtask(TaskStatus status, WorktreeState wt = WorktreeState.Active) => new() { Id = Guid.NewGuid().ToString(), Title = "t", Status = status, WorktreeState = wt }; // ── CanMergeAll tests exercising the real VM ───────────────────────────── [Fact] public void CanMergeAll_AllChildrenDoneActiveWorktrees_True() { var vm = BuildVm(new FakeWorkerClient()); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); vm.RecomputeCanMergeAll(); Assert.True(vm.CanMergeAll); Assert.Null(vm.MergeAllDisabledReason); } [Fact] public void CanMergeAll_AnyChildNotDone_FalseWithReason() { var vm = BuildVm(new FakeWorkerClient()); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); vm.Subtasks.Add(MakeSubtask(TaskStatus.Running, WorktreeState.Active)); vm.RecomputeCanMergeAll(); Assert.False(vm.CanMergeAll); Assert.NotNull(vm.MergeAllDisabledReason); Assert.Contains("1 subtask", vm.MergeAllDisabledReason, StringComparison.OrdinalIgnoreCase); Assert.Contains("not done", vm.MergeAllDisabledReason, StringComparison.OrdinalIgnoreCase); } [Fact] public void CanMergeAll_AnyChildDiscarded_FalseWithReason() { var vm = BuildVm(new FakeWorkerClient()); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Discarded)); vm.RecomputeCanMergeAll(); Assert.False(vm.CanMergeAll); Assert.NotNull(vm.MergeAllDisabledReason); Assert.True( vm.MergeAllDisabledReason!.Contains("discarded", StringComparison.OrdinalIgnoreCase) || vm.MergeAllDisabledReason.Contains("kept", StringComparison.OrdinalIgnoreCase)); } [Fact] public void CanMergeAll_AnyChildKept_FalseWithReason() { var vm = BuildVm(new FakeWorkerClient()); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Active)); vm.Subtasks.Add(MakeSubtask(TaskStatus.Done, WorktreeState.Kept)); vm.RecomputeCanMergeAll(); Assert.False(vm.CanMergeAll); Assert.NotNull(vm.MergeAllDisabledReason); Assert.True( vm.MergeAllDisabledReason!.Contains("kept", StringComparison.OrdinalIgnoreCase) || vm.MergeAllDisabledReason.Contains("discarded", StringComparison.OrdinalIgnoreCase)); } // ── Branch-load test exercising the VM via Bind ────────────────────────── [Fact] public async Task MergeTargetBranches_LoadedFromWorkerOnPlanningParent() { // Seed a Planning parent with one child that has a worktree const string parentId = "parent-1"; const string childId = "child-1"; const string listId = "list-1"; await using (var ctx = NewContext()) { ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow }); ctx.Tasks.Add(new TaskEntity { Id = parentId, ListId = listId, Title = "Parent", Status = TaskStatus.Planning, CreatedAt = DateTime.UtcNow, }); ctx.Tasks.Add(new TaskEntity { Id = childId, ListId = listId, Title = "Child", Status = TaskStatus.Done, ParentTaskId = parentId, CreatedAt = DateTime.UtcNow, }); ctx.Set().Add(new WorktreeEntity { TaskId = childId, Path = "/tmp/wt", BranchName = "branch", BaseCommit = "abc", CreatedAt = DateTime.UtcNow, }); await ctx.SaveChangesAsync(); } var fake = new FakeWorkerClient { MergeTargetsResult = new MergeTargetsDto("main", new[] { "main", "dev" }), }; var vm = BuildVm(fake); // Bind triggers BindAsync → LoadPlanningChildrenAsync → GetMergeTargetsAsync var parentRow = new TaskRowViewModel { Id = parentId }; parentRow.Status = TaskStatus.Planning; vm.Bind(parentRow); // Wait for the background load to settle var deadline = DateTime.UtcNow.AddSeconds(5); while (DateTime.UtcNow < deadline && vm.MergeTargetBranches.Count == 0) await Task.Delay(20); Assert.Contains("main", vm.MergeTargetBranches); Assert.Contains("dev", vm.MergeTargetBranches); Assert.Equal("main", vm.SelectedMergeTarget); } }