refactor(claude-do): merge TaskRowViewModel: CanX und DisabledReason aus einer Gate-Met

ClaudeDo-Task: 6c4b27aa-1b5e-4d73-98f5-dcafa3935247
This commit is contained in:
Mika Kuns
2026-08-24 10:08:55 +02:00
2 changed files with 120 additions and 49 deletions
@@ -159,4 +159,76 @@ public class TaskRowContextMenuTests
row.IsSelected = isSelected;
Assert.Equal(expected, row.ShowRowActions);
}
// Pins down what the gate methods guarantee by construction: each of the five CanX/reason
// pairs must agree on every reachable state, not just the handful of examples above. Swept
// across the full state matrix (every TaskStatus x PlanningPhase x IsChild/ParentFinalized x
// IsManual x HasInteractiveSession x HasQueuedSubtasks x IsRefining) rather than one example
// per gate, so a future edit to a gate's condition without touching its Reason branch (or
// vice versa) fails loudly instead of silently drifting.
public static IEnumerable<object[]> GateStateMatrix()
{
foreach (var status in Enum.GetValues<TaskStatus>())
foreach (var phase in Enum.GetValues<PlanningPhase>())
foreach (var isChild in Bools)
foreach (var parentFinalized in Bools)
foreach (var isManual in Bools)
foreach (var hasInteractiveSession in Bools)
foreach (var hasQueuedSubtasks in Bools)
foreach (var isRefining in Bools)
yield return new object[]
{
status, phase, isChild, parentFinalized, isManual,
hasInteractiveSession, hasQueuedSubtasks, isRefining,
};
}
private static readonly bool[] Bools = { false, true };
[Theory]
[MemberData(nameof(GateStateMatrix))]
public void CanX_Agrees_With_DisabledReason_Across_State_Matrix(
TaskStatus status, PlanningPhase phase, bool isChild, bool parentFinalized, bool isManual,
bool hasInteractiveSession, bool hasQueuedSubtasks, bool isRefining)
{
var row = new TaskRowViewModel
{
Id = "t1",
Status = status,
PlanningPhase = phase,
ParentTaskId = isChild ? "parent-id" : null,
ParentFinalized = parentFinalized,
IsManual = isManual,
HasInteractiveSession = hasInteractiveSession,
HasQueuedSubtasks = hasQueuedSubtasks,
IsRefining = isRefining,
};
Assert.Equal(row.CanSendToQueue, row.SendToQueueDisabledReason is null);
Assert.Equal(row.IsRunning, row.CancelDisabledReason is null);
Assert.Equal(row.CanRefine, row.RefineDisabledReason is null);
var canOpenPlanningMenu = row.CanOpenPlanningSession || row.CanResumeOrDiscardPlanning || row.CanFinalizePlanning;
Assert.Equal(canOpenPlanningMenu, row.PlanningDisabledReason is null);
}
[Fact]
public void OpenWorktreeDisabledReason_Agrees_With_CanOpenWorktree_When_NoPath()
{
var row = MakeRow();
Assert.Equal(row.CanOpenWorktree, row.OpenWorktreeDisabledReason is null);
}
[Fact]
public void OpenWorktreeDisabledReason_Agrees_With_CanOpenWorktree_When_Path_Exists()
{
var dir = Directory.CreateTempSubdirectory();
try
{
var row = MakeRow();
row.WorktreePath = dir.FullName;
Assert.Equal(row.CanOpenWorktree, row.OpenWorktreeDisabledReason is null);
}
finally { dir.Delete(true); }
}
}