refactor(data): retire legacy TaskStatus values and backfill existing rows

Slice 6 of the worker state and queue consolidation refactor.

* Drop Manual, Planning, Planned, Draft, Waiting from the TaskStatus enum
  and from the EF value converter; only the lifecycle values remain
  (Idle, Queued, Running, Done, Failed, Cancelled).
* Add migration RetireLegacyTaskStatus that rewrites existing rows:
  manual/draft -> idle, planning -> idle+planning_phase=active,
  planned -> idle+planning_phase=finalized, waiting -> queued+blocked_by
  derived from sort_order via a CTE with LAG().
* Reroute every call site that compared/set legacy values to the new
  three-field model (Status + PlanningPhase + BlockedByTaskId), including
  the planning repo helpers, MCP services, the planning chain coordinator,
  and the UI view-models. TaskRowViewModel now exposes PlanningPhase to
  drive the planning badge.
* Refresh Worker/CLAUDE.md and Data/CLAUDE.md, the docs/plan.md status
  section, and the planning verification notes in docs/open.md.
This commit is contained in:
Mika Kuns
2026-04-27 15:28:55 +02:00
parent ff7c239959
commit dc3fc443b4
37 changed files with 306 additions and 229 deletions

View File

@@ -40,7 +40,8 @@ public sealed class TaskStateServiceTests : IDisposable
TaskStatus status,
string? parentId = null,
int sortOrder = 0,
string? blockedBy = null)
string? blockedBy = null,
PlanningPhase phase = PlanningPhase.None)
{
var id = Guid.NewGuid().ToString();
await using var ctx = _factory.CreateDbContext();
@@ -50,6 +51,7 @@ public sealed class TaskStateServiceTests : IDisposable
ListId = _listId,
Title = "task",
Status = status,
PlanningPhase = phase,
CreatedAt = DateTime.UtcNow,
ParentTaskId = parentId,
SortOrder = sortOrder,
@@ -256,15 +258,15 @@ public sealed class TaskStateServiceTests : IDisposable
// ─── StartPlanningAsync ───────────────────────────────────────────────
[Fact]
public async Task StartPlanningAsync_FromManual_FlipsStatus_AndPlanningPhase()
public async Task StartPlanningAsync_FromIdle_SetsPlanningPhase()
{
var id = await SeedTaskAsync(TaskStatus.Manual);
var id = await SeedTaskAsync(TaskStatus.Idle);
var result = await _sut.StartPlanningAsync(id, default);
Assert.True(result.Ok);
var t = await GetTaskAsync(id);
Assert.Equal(TaskStatus.Planning, t.Status);
Assert.Equal(TaskStatus.Idle, t.Status);
Assert.Equal(PlanningPhase.Active, t.PlanningPhase);
}
@@ -283,7 +285,7 @@ public sealed class TaskStateServiceTests : IDisposable
[Fact]
public async Task FinalizePlanningAsync_OnActivePhase_TransitionsToFinalized()
{
var id = await SeedTaskAsync(TaskStatus.Manual);
var id = await SeedTaskAsync(TaskStatus.Idle);
await _sut.StartPlanningAsync(id, default);
var result = await _sut.FinalizePlanningAsync(id, default);
@@ -297,7 +299,7 @@ public sealed class TaskStateServiceTests : IDisposable
[Fact]
public async Task FinalizePlanningAsync_OnNonePhase_Rejects()
{
var id = await SeedTaskAsync(TaskStatus.Manual);
var id = await SeedTaskAsync(TaskStatus.Idle);
var result = await _sut.FinalizePlanningAsync(id, default);
@@ -335,18 +337,6 @@ public sealed class TaskStateServiceTests : IDisposable
Assert.True(_built.WakeCount() > wakesBefore);
}
[Fact]
public async Task UnblockAsync_OnWaitingTask_FlipsToQueued()
{
// Bridge to legacy chain layout: a Status=Waiting sibling becomes Queued on unblock.
var task = await SeedTaskAsync(TaskStatus.Waiting);
var result = await _sut.UnblockAsync(task, default);
Assert.True(result.Ok);
Assert.Equal(TaskStatus.Queued, await GetStatusAsync(task));
}
// ─── RecoverStaleRunningAsync ─────────────────────────────────────────
[Fact]
@@ -371,7 +361,7 @@ public sealed class TaskStateServiceTests : IDisposable
[Fact]
public async Task CompleteAsync_OnChild_AdvancesNextBlockedSibling()
{
var parent = await SeedTaskAsync(TaskStatus.Planned);
var parent = await SeedTaskAsync(TaskStatus.Idle, phase: PlanningPhase.Finalized);
var c0 = await SeedTaskAsync(TaskStatus.Running, parentId: parent, sortOrder: 0);
var c1 = await SeedTaskAsync(TaskStatus.Queued, parentId: parent, sortOrder: 1, blockedBy: c0);
var c2 = await SeedTaskAsync(TaskStatus.Queued, parentId: parent, sortOrder: 2, blockedBy: c1);