diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index 87fa0100..1341439e 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -8,67 +8,206 @@ namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class TaskRowViewModel : ViewModelBase { public required string Id { get; init; } - [ObservableProperty] private int _number; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowNumberBadge))] + private int _number; [ObservableProperty] private string _title = ""; [ObservableProperty] private string _listName = ""; - [ObservableProperty] private bool _done; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsOverdue))] + [NotifyPropertyChangedFor(nameof(CanAddToMyDay))] + private bool _done; [ObservableProperty] private bool _isStarred; - [ObservableProperty] private bool _isMyDay; - [ObservableProperty] private bool _isSelected; - [ObservableProperty] private TaskStatus _status; - [ObservableProperty] private PlanningPhase _planningPhase; - [ObservableProperty] private string? _branch; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanAddToMyDay))] + private bool _isMyDay; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowRowActions))] + private bool _isSelected; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(StatusChipClass))] + [NotifyPropertyChangedFor(nameof(StatusLabel))] + [NotifyPropertyChangedFor(nameof(ShowStatusChip))] + [NotifyPropertyChangedFor(nameof(IsRunning))] + [NotifyPropertyChangedFor(nameof(IsWaitingForReview))] + [NotifyPropertyChangedFor(nameof(IsParked))] + [NotifyPropertyChangedFor(nameof(IsQueued))] + [NotifyPropertyChangedFor(nameof(IsWaiting))] + [NotifyPropertyChangedFor(nameof(HasFailureReason))] + [NotifyPropertyChangedFor(nameof(FailureReasonTooltip))] + [NotifyPropertyChangedFor(nameof(StatusChipTooltip))] + [NotifyPropertyChangedFor(nameof(IsDraft))] + [NotifyPropertyChangedFor(nameof(IsPlanned))] + [NotifyPropertyChangedFor(nameof(CanOpenPlanningSession))] + [NotifyPropertyChangedFor(nameof(CanRemoveFromQueue))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(CanRefine))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + [NotifyPropertyChangedFor(nameof(CancelDisabledReason))] + [NotifyPropertyChangedFor(nameof(RefineDisabledReason))] + [NotifyPropertyChangedFor(nameof(PlanningDisabledReason))] + [NotifyPropertyChangedFor(nameof(MarkAsDisabledReason))] + private TaskStatus _status; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsPlanningParent))] + [NotifyPropertyChangedFor(nameof(PlanningBadge))] + [NotifyPropertyChangedFor(nameof(ShowStatusChip))] + [NotifyPropertyChangedFor(nameof(IsPlanActive))] + [NotifyPropertyChangedFor(nameof(IsPlanFinalized))] + [NotifyPropertyChangedFor(nameof(CanOpenPlanningSession))] + [NotifyPropertyChangedFor(nameof(CanResumeOrDiscardPlanning))] + [NotifyPropertyChangedFor(nameof(CanQueuePlan))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(CanFinalizePlanning))] + [NotifyPropertyChangedFor(nameof(CanRefine))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + [NotifyPropertyChangedFor(nameof(RefineDisabledReason))] + [NotifyPropertyChangedFor(nameof(PlanningDisabledReason))] + private PlanningPhase _planningPhase; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasBranch))] + private string? _branch; [ObservableProperty] private string? _diffStat; - [ObservableProperty] private ClaudeDo.Data.Models.WorktreeState? _worktreeState; - [ObservableProperty] private string? _worktreePath; - [ObservableProperty] private DateTime? _scheduledFor; - [ObservableProperty] private int _diffAdditions; - [ObservableProperty] private int _diffDeletions; - [ObservableProperty] private bool _dropHintAbove; - [ObservableProperty] private bool _dropHintBelow; - [ObservableProperty] private string? _parentTaskId; - [ObservableProperty] private string? _createdBy; - [ObservableProperty] private string? _blockedByTaskId; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsParked))] + [NotifyPropertyChangedFor(nameof(StatusLabel))] + private ClaudeDo.Data.Models.WorktreeState? _worktreeState; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanOpenWorktree))] + [NotifyPropertyChangedFor(nameof(OpenWorktreeDisabledReason))] + private string? _worktreePath; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsOverdue))] + [NotifyPropertyChangedFor(nameof(HasSchedule))] + private DateTime? _scheduledFor; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasDiff))] + [NotifyPropertyChangedFor(nameof(DiffAdditionsText))] + private int _diffAdditions; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasDiff))] + [NotifyPropertyChangedFor(nameof(DiffDeletionsText))] + private int _diffDeletions; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsDropTarget))] + private bool _dropHintAbove; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsDropTarget))] + private bool _dropHintBelow; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsChild))] + [NotifyPropertyChangedFor(nameof(IsAgentSuggested))] + [NotifyPropertyChangedFor(nameof(ShowAsChild))] + [NotifyPropertyChangedFor(nameof(IsDraft))] + [NotifyPropertyChangedFor(nameof(IsPlanned))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(CanOpenPlanningSession))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + [NotifyPropertyChangedFor(nameof(PlanningDisabledReason))] + private string? _parentTaskId; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsAgentSuggested))] + private string? _createdBy; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsQueued))] + [NotifyPropertyChangedFor(nameof(IsWaiting))] + [NotifyPropertyChangedFor(nameof(StatusChipClass))] + private string? _blockedByTaskId; [ObservableProperty] private string? _dependsOnTaskId; [ObservableProperty] private bool _isExpanded = true; - [ObservableProperty] private bool _hasPlanningChildren; - [ObservableProperty] private bool _hasQueuedSubtasks; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsPlanningParent))] + [NotifyPropertyChangedFor(nameof(CanQueuePlan))] + private bool _hasPlanningChildren; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanRemoveFromQueue))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(CanQueuePlan))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + private bool _hasQueuedSubtasks; [ObservableProperty] private bool _showListChip = true; - [ObservableProperty] private bool _parentFinalized; - [ObservableProperty] private bool _parentInView = true; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsDraft))] + [NotifyPropertyChangedFor(nameof(IsPlanned))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + private bool _parentFinalized; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowAsChild))] + [NotifyPropertyChangedFor(nameof(IsDraft))] + [NotifyPropertyChangedFor(nameof(IsPlanned))] + private bool _parentInView = true; // The three below are extrinsic — computed and assigned by TasksIslandViewModel.Regroup // (it alone has the cross-row graph needed to walk DependsOnTaskId chains), mirroring how // ParentInView is assigned by the same pass rather than derived locally. [ObservableProperty] private bool _showAsChainMember; [ObservableProperty] private int? _chainStep; [ObservableProperty] private string? _chainAfterLabel; - [ObservableProperty] private int _roadblockCount; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasRoadblock))] + [NotifyPropertyChangedFor(nameof(RoadblockTooltip))] + private int _roadblockCount; // Only meaningful when Status=Failed; stamped by TaskRunner.MarkFailed. "unknown" for a // Failed task that predates this field. Null on every other status. - [ObservableProperty] private string? _failureReason; - [ObservableProperty] private int? _failureTurnsUsed; - [ObservableProperty] private int? _failureMaxTurns; - [ObservableProperty] private bool _isRefining; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasFailureReason))] + [NotifyPropertyChangedFor(nameof(FailureReasonLabel))] + [NotifyPropertyChangedFor(nameof(FailureReasonTooltip))] + [NotifyPropertyChangedFor(nameof(StatusChipTooltip))] + private string? _failureReason; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FailureReasonTooltip))] + private int? _failureTurnsUsed; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FailureReasonTooltip))] + private int? _failureMaxTurns; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanRefine))] + [NotifyPropertyChangedFor(nameof(RefineDisabledReason))] + private bool _isRefining; // Manual = a reminder only the user can do. Every "hand this to Claude" affordance is hidden // and automation skips it; opening a hand-driven ConPTY session stays allowed. - [ObservableProperty] private bool _isManual; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ManualBadge))] + [NotifyPropertyChangedFor(nameof(CanRefine))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(CanOpenPlanningSession))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + [NotifyPropertyChangedFor(nameof(RefineDisabledReason))] + [NotifyPropertyChangedFor(nameof(PlanningDisabledReason))] + private bool _isManual; // Set by the shell from Mission Control's open ConPTY panes: this task has a live hand-driven // session, which outranks the persisted status on the lifecycle chip (an interactive task is // typically Idle+Active-worktree, i.e. would otherwise read "Parked"). - [ObservableProperty] private bool _hasInteractiveSession; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsParked))] + [NotifyPropertyChangedFor(nameof(StatusChipClass))] + [NotifyPropertyChangedFor(nameof(StatusLabel))] + [NotifyPropertyChangedFor(nameof(ShowStatusChip))] + [NotifyPropertyChangedFor(nameof(StatusChipTooltip))] + [NotifyPropertyChangedFor(nameof(CanSendToQueue))] + [NotifyPropertyChangedFor(nameof(SendToQueueDisabledReason))] + [NotifyPropertyChangedFor(nameof(CanDeleteTask))] + [NotifyPropertyChangedFor(nameof(DeleteDisabledReason))] + private bool _hasInteractiveSession; // Set by the custom drag while this row is being dragged — drives the "grabbed" row style. [ObservableProperty] private bool _isDragging; // Set from PointerEntered/PointerExited in TaskRowView.axaml.cs. Drives ShowRowActions // together with IsSelected, so hover-only affordances (refine button, unset star, dequeue-X) // stay reachable via keyboard/selection, not just mouse hover. - [ObservableProperty] private bool _isHovered; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowRowActions))] + private bool _isHovered; // Transient: set from HubBroadcaster's OperationProgress while the worker is still creating // this task's worktree (the silent gap between Queued and the first agent output). Cleared // by the next entity refresh — UpdateFromEntity always reflects a settled state, so there's // nothing left to show past that point. - [ObservableProperty] private string? _creationPhase; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasCreationPhase))] + [NotifyPropertyChangedFor(nameof(CreationPhaseLabel))] + private string? _creationPhase; // True while a drag is hovering this row (i.e. it would show a drop-hint gap). Used to // suppress the ordinary hover highlight/transitions so they don't fight the hint. @@ -309,179 +448,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase _ => "idle", }; - partial void OnStatusChanged(TaskStatus value) - { - OnPropertyChanged(nameof(StatusChipClass)); - OnPropertyChanged(nameof(StatusLabel)); - OnPropertyChanged(nameof(ShowStatusChip)); - OnPropertyChanged(nameof(IsRunning)); - OnPropertyChanged(nameof(IsWaitingForReview)); - OnPropertyChanged(nameof(IsParked)); - OnPropertyChanged(nameof(IsQueued)); - OnPropertyChanged(nameof(IsWaiting)); - OnPropertyChanged(nameof(HasFailureReason)); - OnPropertyChanged(nameof(FailureReasonTooltip)); - OnPropertyChanged(nameof(StatusChipTooltip)); - OnPropertyChanged(nameof(IsDraft)); - OnPropertyChanged(nameof(IsPlanned)); - OnPropertyChanged(nameof(CanOpenPlanningSession)); - OnPropertyChanged(nameof(CanRemoveFromQueue)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(CanRefine)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - OnPropertyChanged(nameof(CancelDisabledReason)); - OnPropertyChanged(nameof(RefineDisabledReason)); - OnPropertyChanged(nameof(PlanningDisabledReason)); - OnPropertyChanged(nameof(MarkAsDisabledReason)); - } - - partial void OnParentTaskIdChanged(string? value) - { - OnPropertyChanged(nameof(IsChild)); - OnPropertyChanged(nameof(IsAgentSuggested)); - OnPropertyChanged(nameof(ShowAsChild)); - OnPropertyChanged(nameof(IsDraft)); - OnPropertyChanged(nameof(IsPlanned)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(CanOpenPlanningSession)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - OnPropertyChanged(nameof(PlanningDisabledReason)); - } - - partial void OnParentInViewChanged(bool value) - { - OnPropertyChanged(nameof(ShowAsChild)); - OnPropertyChanged(nameof(IsDraft)); - OnPropertyChanged(nameof(IsPlanned)); - } - - partial void OnCreatedByChanged(string? value) => OnPropertyChanged(nameof(IsAgentSuggested)); - - partial void OnParentFinalizedChanged(bool value) - { - OnPropertyChanged(nameof(IsDraft)); - OnPropertyChanged(nameof(IsPlanned)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - } - - partial void OnPlanningPhaseChanged(PlanningPhase value) - { - OnPropertyChanged(nameof(IsPlanningParent)); - OnPropertyChanged(nameof(PlanningBadge)); - OnPropertyChanged(nameof(ShowStatusChip)); - OnPropertyChanged(nameof(IsPlanActive)); - OnPropertyChanged(nameof(IsPlanFinalized)); - OnPropertyChanged(nameof(CanOpenPlanningSession)); - OnPropertyChanged(nameof(CanResumeOrDiscardPlanning)); - OnPropertyChanged(nameof(CanQueuePlan)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(CanFinalizePlanning)); - OnPropertyChanged(nameof(CanRefine)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - OnPropertyChanged(nameof(RefineDisabledReason)); - OnPropertyChanged(nameof(PlanningDisabledReason)); - } - - partial void OnIsRefiningChanged(bool value) - { - OnPropertyChanged(nameof(CanRefine)); - OnPropertyChanged(nameof(RefineDisabledReason)); - } - - partial void OnIsManualChanged(bool value) - { - OnPropertyChanged(nameof(ManualBadge)); - OnPropertyChanged(nameof(CanRefine)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(CanOpenPlanningSession)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - OnPropertyChanged(nameof(RefineDisabledReason)); - OnPropertyChanged(nameof(PlanningDisabledReason)); - } - - partial void OnHasInteractiveSessionChanged(bool value) - { - OnPropertyChanged(nameof(IsParked)); - OnPropertyChanged(nameof(StatusChipClass)); - OnPropertyChanged(nameof(StatusLabel)); - OnPropertyChanged(nameof(ShowStatusChip)); - OnPropertyChanged(nameof(StatusChipTooltip)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - OnPropertyChanged(nameof(CanDeleteTask)); - OnPropertyChanged(nameof(DeleteDisabledReason)); - } - - partial void OnFailureReasonChanged(string? value) - { - OnPropertyChanged(nameof(HasFailureReason)); - OnPropertyChanged(nameof(FailureReasonLabel)); - OnPropertyChanged(nameof(FailureReasonTooltip)); - OnPropertyChanged(nameof(StatusChipTooltip)); - } - - partial void OnFailureTurnsUsedChanged(int? value) => OnPropertyChanged(nameof(FailureReasonTooltip)); - partial void OnFailureMaxTurnsChanged(int? value) => OnPropertyChanged(nameof(FailureReasonTooltip)); - - partial void OnHasQueuedSubtasksChanged(bool value) - { - OnPropertyChanged(nameof(CanRemoveFromQueue)); - OnPropertyChanged(nameof(CanSendToQueue)); - OnPropertyChanged(nameof(CanQueuePlan)); - OnPropertyChanged(nameof(SendToQueueDisabledReason)); - } - - partial void OnIsHoveredChanged(bool value) => OnPropertyChanged(nameof(ShowRowActions)); - partial void OnIsSelectedChanged(bool value) => OnPropertyChanged(nameof(ShowRowActions)); - - partial void OnBlockedByTaskIdChanged(string? value) - { - OnPropertyChanged(nameof(IsQueued)); - OnPropertyChanged(nameof(IsWaiting)); - OnPropertyChanged(nameof(StatusChipClass)); - } - - partial void OnHasPlanningChildrenChanged(bool value) - { - OnPropertyChanged(nameof(IsPlanningParent)); - OnPropertyChanged(nameof(CanQueuePlan)); - } - - partial void OnNumberChanged(int value) => OnPropertyChanged(nameof(ShowNumberBadge)); - partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); - partial void OnWorktreePathChanged(string? value) - { - OnPropertyChanged(nameof(CanOpenWorktree)); - OnPropertyChanged(nameof(OpenWorktreeDisabledReason)); - } - partial void OnWorktreeStateChanged(ClaudeDo.Data.Models.WorktreeState? value) - { - OnPropertyChanged(nameof(IsParked)); - OnPropertyChanged(nameof(StatusLabel)); - } - partial void OnDoneChanged(bool value) - { - OnPropertyChanged(nameof(IsOverdue)); - OnPropertyChanged(nameof(CanAddToMyDay)); - } - partial void OnIsMyDayChanged(bool value) => OnPropertyChanged(nameof(CanAddToMyDay)); - partial void OnScheduledForChanged(DateTime? value) - { - OnPropertyChanged(nameof(IsOverdue)); - OnPropertyChanged(nameof(HasSchedule)); - } - partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); } - partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); } - partial void OnRoadblockCountChanged(int value) { OnPropertyChanged(nameof(HasRoadblock)); OnPropertyChanged(nameof(RoadblockTooltip)); } - partial void OnDropHintAboveChanged(bool value) => OnPropertyChanged(nameof(IsDropTarget)); - partial void OnDropHintBelowChanged(bool value) => OnPropertyChanged(nameof(IsDropTarget)); - partial void OnCreationPhaseChanged(string? value) - { - OnPropertyChanged(nameof(HasCreationPhase)); - OnPropertyChanged(nameof(CreationPhaseLabel)); - } - public void RefreshLocalized() { OnPropertyChanged(nameof(StatusLabel)); diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs new file mode 100644 index 00000000..d8fe1112 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelNotificationsTests.cs @@ -0,0 +1,463 @@ +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), + }, 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); + } +}