using System.Collections.Generic; using ClaudeDo.Data.Models; using ClaudeDo.Ui.ViewModels.Islands; using Xunit; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Tests.UiVm; // Characterization test for TaskRowViewModel's PropertyChanged fan-out. For every source // property that today drives dependent notifications (via a handwritten partial OnXChanged), // this pins down the exact SET of names raised when that property changes. Order is // deliberately not asserted: a NotifyPropertyChangedFor-based rewrite raises the source // property's own notification before its dependents instead of after, which is an unobservable // reordering, not a behavior change. This test must stay green across that rewrite. public class TaskRowViewModelNotificationsTests { private static HashSet Raised(TaskRowViewModel vm, System.Action mutate) { var raised = new HashSet(); vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName); mutate(); return raised; } [Fact] public void StatusChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.Status = TaskStatus.Running); Assert.Equal(new HashSet { nameof(TaskRowViewModel.Status), nameof(TaskRowViewModel.StatusChipClass), nameof(TaskRowViewModel.StatusLabel), nameof(TaskRowViewModel.ShowStatusChip), nameof(TaskRowViewModel.IsRunning), nameof(TaskRowViewModel.IsWaitingForReview), nameof(TaskRowViewModel.IsParked), nameof(TaskRowViewModel.IsQueued), nameof(TaskRowViewModel.IsWaiting), nameof(TaskRowViewModel.HasFailureReason), nameof(TaskRowViewModel.FailureReasonTooltip), nameof(TaskRowViewModel.StatusChipTooltip), nameof(TaskRowViewModel.IsDraft), nameof(TaskRowViewModel.IsPlanned), nameof(TaskRowViewModel.CanOpenPlanningSession), nameof(TaskRowViewModel.CanRemoveFromQueue), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.CanRefine), nameof(TaskRowViewModel.SendToQueueDisabledReason), nameof(TaskRowViewModel.CancelDisabledReason), nameof(TaskRowViewModel.RefineDisabledReason), nameof(TaskRowViewModel.PlanningDisabledReason), nameof(TaskRowViewModel.MarkAsDisabledReason), }, raised); } [Fact] public void ParentTaskIdChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.ParentTaskId = "parent"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.ParentTaskId), nameof(TaskRowViewModel.IsChild), nameof(TaskRowViewModel.IsAgentSuggested), nameof(TaskRowViewModel.ShowAsChild), nameof(TaskRowViewModel.IsDraft), nameof(TaskRowViewModel.IsPlanned), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.CanOpenPlanningSession), nameof(TaskRowViewModel.SendToQueueDisabledReason), nameof(TaskRowViewModel.PlanningDisabledReason), }, raised); } [Fact] public void ParentInViewChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.ParentInView = false); Assert.Equal(new HashSet { nameof(TaskRowViewModel.ParentInView), nameof(TaskRowViewModel.ShowAsChild), nameof(TaskRowViewModel.IsDraft), nameof(TaskRowViewModel.IsPlanned), }, raised); } [Fact] public void CreatedByChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.CreatedBy = "someone"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.CreatedBy), nameof(TaskRowViewModel.IsAgentSuggested), }, raised); } [Fact] public void ParentFinalizedChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.ParentFinalized = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.ParentFinalized), nameof(TaskRowViewModel.IsDraft), nameof(TaskRowViewModel.IsPlanned), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.SendToQueueDisabledReason), }, raised); } [Fact] public void PlanningPhaseChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.PlanningPhase = PlanningPhase.Active); Assert.Equal(new HashSet { nameof(TaskRowViewModel.PlanningPhase), nameof(TaskRowViewModel.IsPlanningParent), nameof(TaskRowViewModel.PlanningBadge), nameof(TaskRowViewModel.ShowStatusChip), nameof(TaskRowViewModel.IsPlanActive), nameof(TaskRowViewModel.IsPlanFinalized), nameof(TaskRowViewModel.CanOpenPlanningSession), nameof(TaskRowViewModel.CanResumeOrDiscardPlanning), nameof(TaskRowViewModel.CanQueuePlan), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.CanFinalizePlanning), nameof(TaskRowViewModel.CanRefine), nameof(TaskRowViewModel.SendToQueueDisabledReason), nameof(TaskRowViewModel.RefineDisabledReason), nameof(TaskRowViewModel.PlanningDisabledReason), }, raised); } [Fact] public void IsRefiningChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.IsRefining = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.IsRefining), nameof(TaskRowViewModel.CanRefine), nameof(TaskRowViewModel.RefineDisabledReason), }, raised); } [Fact] public void IsManualChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.IsManual = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.IsManual), nameof(TaskRowViewModel.ManualBadge), nameof(TaskRowViewModel.CanRefine), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.CanOpenPlanningSession), nameof(TaskRowViewModel.SendToQueueDisabledReason), nameof(TaskRowViewModel.RefineDisabledReason), nameof(TaskRowViewModel.PlanningDisabledReason), }, raised); } [Fact] public void HasInteractiveSessionChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.HasInteractiveSession = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.HasInteractiveSession), nameof(TaskRowViewModel.IsParked), nameof(TaskRowViewModel.StatusChipClass), nameof(TaskRowViewModel.StatusLabel), nameof(TaskRowViewModel.ShowStatusChip), nameof(TaskRowViewModel.StatusChipTooltip), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.SendToQueueDisabledReason), nameof(TaskRowViewModel.CanDeleteTask), nameof(TaskRowViewModel.DeleteDisabledReason), }, raised); } [Fact] public void FailureReasonChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.FailureReason = "error"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.FailureReason), nameof(TaskRowViewModel.HasFailureReason), nameof(TaskRowViewModel.FailureReasonLabel), nameof(TaskRowViewModel.FailureReasonTooltip), nameof(TaskRowViewModel.StatusChipTooltip), }, raised); } [Fact] public void FailureTurnsUsedChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.FailureTurnsUsed = 5); Assert.Equal(new HashSet { nameof(TaskRowViewModel.FailureTurnsUsed), nameof(TaskRowViewModel.FailureReasonTooltip), }, raised); } [Fact] public void FailureMaxTurnsChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.FailureMaxTurns = 10); Assert.Equal(new HashSet { nameof(TaskRowViewModel.FailureMaxTurns), nameof(TaskRowViewModel.FailureReasonTooltip), }, raised); } [Fact] public void HasQueuedSubtasksChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.HasQueuedSubtasks = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.HasQueuedSubtasks), nameof(TaskRowViewModel.CanRemoveFromQueue), nameof(TaskRowViewModel.CanSendToQueue), nameof(TaskRowViewModel.CanQueuePlan), nameof(TaskRowViewModel.SendToQueueDisabledReason), }, raised); } [Fact] public void IsHoveredChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.IsHovered = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.IsHovered), nameof(TaskRowViewModel.ShowRowActions), }, raised); } [Fact] public void IsSelectedChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.IsSelected = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.IsSelected), nameof(TaskRowViewModel.ShowRowActions), }, raised); } [Fact] public void BlockedByTaskIdChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.BlockedByTaskId = "blocker"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.BlockedByTaskId), nameof(TaskRowViewModel.IsQueued), nameof(TaskRowViewModel.IsWaiting), nameof(TaskRowViewModel.StatusChipClass), nameof(TaskRowViewModel.StatusLabel), nameof(TaskRowViewModel.StatusChipTooltip), nameof(TaskRowViewModel.CanRemoveFromQueue), }, raised); } [Fact] public void IsBlockedByDependencyChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.IsBlockedByDependency = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.IsBlockedByDependency), nameof(TaskRowViewModel.IsQueued), nameof(TaskRowViewModel.IsWaiting), nameof(TaskRowViewModel.StatusChipClass), nameof(TaskRowViewModel.StatusLabel), nameof(TaskRowViewModel.StatusChipTooltip), nameof(TaskRowViewModel.CanRemoveFromQueue), }, raised); } [Fact] public void DependencyWaitLabelChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.DependencyWaitLabel = "#42"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.DependencyWaitLabel), nameof(TaskRowViewModel.StatusChipTooltip), }, raised); } [Fact] public void HasPlanningChildrenChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.HasPlanningChildren = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.HasPlanningChildren), nameof(TaskRowViewModel.IsPlanningParent), nameof(TaskRowViewModel.CanQueuePlan), }, raised); } [Fact] public void NumberChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.Number = 5); Assert.Equal(new HashSet { nameof(TaskRowViewModel.Number), nameof(TaskRowViewModel.ShowNumberBadge), }, raised); } [Fact] public void BranchChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.Branch = "main"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.Branch), nameof(TaskRowViewModel.HasBranch), }, raised); } [Fact] public void WorktreePathChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.WorktreePath = @"C:\repo\worktrees\t"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.WorktreePath), nameof(TaskRowViewModel.CanOpenWorktree), nameof(TaskRowViewModel.OpenWorktreeDisabledReason), }, raised); } [Fact] public void WorktreeStateChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.WorktreeState = ClaudeDo.Data.Models.WorktreeState.Active); Assert.Equal(new HashSet { nameof(TaskRowViewModel.WorktreeState), nameof(TaskRowViewModel.IsParked), nameof(TaskRowViewModel.StatusLabel), }, raised); } [Fact] public void DoneChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.Done = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.Done), nameof(TaskRowViewModel.IsOverdue), nameof(TaskRowViewModel.CanAddToMyDay), }, raised); } [Fact] public void IsMyDayChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.IsMyDay = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.IsMyDay), nameof(TaskRowViewModel.CanAddToMyDay), }, raised); } [Fact] public void ScheduledForChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.ScheduledFor = DateTime.UtcNow); Assert.Equal(new HashSet { nameof(TaskRowViewModel.ScheduledFor), nameof(TaskRowViewModel.IsOverdue), nameof(TaskRowViewModel.HasSchedule), }, raised); } [Fact] public void DiffAdditionsChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.DiffAdditions = 5); Assert.Equal(new HashSet { nameof(TaskRowViewModel.DiffAdditions), nameof(TaskRowViewModel.HasDiff), nameof(TaskRowViewModel.DiffAdditionsText), }, raised); } [Fact] public void DiffDeletionsChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.DiffDeletions = 5); Assert.Equal(new HashSet { nameof(TaskRowViewModel.DiffDeletions), nameof(TaskRowViewModel.HasDiff), nameof(TaskRowViewModel.DiffDeletionsText), }, raised); } [Fact] public void RoadblockCountChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.RoadblockCount = 3); Assert.Equal(new HashSet { nameof(TaskRowViewModel.RoadblockCount), nameof(TaskRowViewModel.HasRoadblock), nameof(TaskRowViewModel.RoadblockTooltip), }, raised); } [Fact] public void DropHintAboveChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.DropHintAbove = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.DropHintAbove), nameof(TaskRowViewModel.IsDropTarget), }, raised); } [Fact] public void DropHintBelowChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.DropHintBelow = true); Assert.Equal(new HashSet { nameof(TaskRowViewModel.DropHintBelow), nameof(TaskRowViewModel.IsDropTarget), }, raised); } [Fact] public void CreationPhaseChanged_Notifies_Dependents() { var vm = new TaskRowViewModel { Id = "t" }; var raised = Raised(vm, () => vm.CreationPhase = "creating_worktree"); Assert.Equal(new HashSet { nameof(TaskRowViewModel.CreationPhase), nameof(TaskRowViewModel.HasCreationPhase), nameof(TaskRowViewModel.CreationPhaseLabel), }, raised); } }