chore(claude-do): merge TaskRowViewModel: manuelle OnPropertyChanged-Kette auf [Noti

ClaudeDo-Task: 87976462-5845-4e01-8233-32afadca02bd
This commit is contained in:
Mika Kuns
2026-09-02 12:59:35 +02:00
2 changed files with 632 additions and 203 deletions
@@ -8,67 +8,206 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TaskRowViewModel : ViewModelBase public sealed partial class TaskRowViewModel : ViewModelBase
{ {
public required string Id { get; init; } public required string Id { get; init; }
[ObservableProperty] private int _number; [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowNumberBadge))]
private int _number;
[ObservableProperty] private string _title = ""; [ObservableProperty] private string _title = "";
[ObservableProperty] private string _listName = ""; [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 _isStarred;
[ObservableProperty] private bool _isMyDay; [ObservableProperty]
[ObservableProperty] private bool _isSelected; [NotifyPropertyChangedFor(nameof(CanAddToMyDay))]
[ObservableProperty] private TaskStatus _status; private bool _isMyDay;
[ObservableProperty] private PlanningPhase _planningPhase; [ObservableProperty]
[ObservableProperty] private string? _branch; [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 string? _diffStat;
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState? _worktreeState; [ObservableProperty]
[ObservableProperty] private string? _worktreePath; [NotifyPropertyChangedFor(nameof(IsParked))]
[ObservableProperty] private DateTime? _scheduledFor; [NotifyPropertyChangedFor(nameof(StatusLabel))]
[ObservableProperty] private int _diffAdditions; private ClaudeDo.Data.Models.WorktreeState? _worktreeState;
[ObservableProperty] private int _diffDeletions; [ObservableProperty]
[ObservableProperty] private bool _dropHintAbove; [NotifyPropertyChangedFor(nameof(CanOpenWorktree))]
[ObservableProperty] private bool _dropHintBelow; [NotifyPropertyChangedFor(nameof(OpenWorktreeDisabledReason))]
[ObservableProperty] private string? _parentTaskId; private string? _worktreePath;
[ObservableProperty] private string? _createdBy; [ObservableProperty]
[ObservableProperty] private string? _blockedByTaskId; [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 string? _dependsOnTaskId;
[ObservableProperty] private bool _isExpanded = true; [ObservableProperty] private bool _isExpanded = true;
[ObservableProperty] private bool _hasPlanningChildren; [ObservableProperty]
[ObservableProperty] private bool _hasQueuedSubtasks; [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 _showListChip = true;
[ObservableProperty] private bool _parentFinalized; [ObservableProperty]
[ObservableProperty] private bool _parentInView = true; [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 // 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 // (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. // ParentInView is assigned by the same pass rather than derived locally.
[ObservableProperty] private bool _showAsChainMember; [ObservableProperty] private bool _showAsChainMember;
[ObservableProperty] private int? _chainStep; [ObservableProperty] private int? _chainStep;
[ObservableProperty] private string? _chainAfterLabel; [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 // Only meaningful when Status=Failed; stamped by TaskRunner.MarkFailed. "unknown" for a
// Failed task that predates this field. Null on every other status. // Failed task that predates this field. Null on every other status.
[ObservableProperty] private string? _failureReason; [ObservableProperty]
[ObservableProperty] private int? _failureTurnsUsed; [NotifyPropertyChangedFor(nameof(HasFailureReason))]
[ObservableProperty] private int? _failureMaxTurns; [NotifyPropertyChangedFor(nameof(FailureReasonLabel))]
[ObservableProperty] private bool _isRefining; [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 // 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. // 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 // 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 // session, which outranks the persisted status on the lifecycle chip (an interactive task is
// typically Idle+Active-worktree, i.e. would otherwise read "Parked"). // 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. // Set by the custom drag while this row is being dragged — drives the "grabbed" row style.
[ObservableProperty] private bool _isDragging; [ObservableProperty] private bool _isDragging;
// Set from PointerEntered/PointerExited in TaskRowView.axaml.cs. Drives ShowRowActions // Set from PointerEntered/PointerExited in TaskRowView.axaml.cs. Drives ShowRowActions
// together with IsSelected, so hover-only affordances (refine button, unset star, dequeue-X) // together with IsSelected, so hover-only affordances (refine button, unset star, dequeue-X)
// stay reachable via keyboard/selection, not just mouse hover. // 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 // 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 // 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 // by the next entity refresh — UpdateFromEntity always reflects a settled state, so there's
// nothing left to show past that point. // 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 // 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. // suppress the ordinary hover highlight/transitions so they don't fight the hint.
@@ -309,179 +448,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
_ => "idle", _ => "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() public void RefreshLocalized()
{ {
OnPropertyChanged(nameof(StatusLabel)); OnPropertyChanged(nameof(StatusLabel));
@@ -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<string?> Raised(TaskRowViewModel vm, System.Action mutate)
{
var raised = new HashSet<string?>();
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
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<string?>
{
nameof(TaskRowViewModel.CreationPhase),
nameof(TaskRowViewModel.HasCreationPhase),
nameof(TaskRowViewModel.CreationPhaseLabel),
}, raised);
}
}