feat(claude-do): depends_on als Menge statt Einzelfeld
Ein Task kann jetzt auf beliebig viele Vorgaenger warten. Die Einzelspalte tasks.depends_on_task_id wird durch die Beziehungstabelle task_dependencies ersetzt (Composite-PK, beide FKs ON DELETE CASCADE -- das reproduziert das alte ON DELETE SET NULL: geloeschter Vorgaenger = Kante weg = Task wieder pickbar). BlockedByTaskId bleibt unberuehrt. - Migration AddTaskDependencies kopiert jede bestehende depends_on_task_id verlustfrei in die Tabelle, bevor die Spalte fallengelassen wird - QueuePicker gatet per doppelt negiertem NOT EXISTS: kein Task mit 0 Kanten wird blockiert, alle Kanten muessen done sein, eine Kante auf einen verschwundenen Task blockiert weiter - SetDependsOnAsync ersetzt die ganze Menge; Zykluspruefung ist jetzt eine BFS ueber den Graphen, faengt also auch A->B->C->A - blockedReason nennt alle offenen Vorgaenger (External/TaskBlocking, von ExternalMcpService und TaskWaitMcpTools gemeinsam genutzt) - add_task/update_task nehmen dependsOnTaskIds; die alte Skalarform bleibt als Ein-Element-Menge inkl. ""-Clear-Sentinel gueltig - UI: Fan-in-Zeile bleibt flach und listet alle Vorgaenger im After-Label, die lineare Kette rendert unveraendert
This commit is contained in:
@@ -102,6 +102,17 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
return await new TaskRepository(ctx).GetByIdAsync(id) ?? throw new InvalidOperationException($"task {id} not found");
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetDependsOnAsync(string id)
|
||||
{
|
||||
await using var ctx = _factory.CreateDbContext();
|
||||
return await ctx.TaskDependencies.Where(d => d.TaskId == id)
|
||||
.Select(d => d.DependsOnTaskId).ToListAsync();
|
||||
}
|
||||
|
||||
// The dependency set has no defined order, and the ids are random GUIDs -- sort both sides.
|
||||
private static List<string> Sorted(params string[] ids) => ids.OrderBy(x => x).ToList();
|
||||
private static List<string> Sorted(IEnumerable<string> ids) => ids.OrderBy(x => x).ToList();
|
||||
|
||||
// ─── EnqueueAsync ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
@@ -659,31 +670,55 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
// ─── SetDependsOnAsync ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task SetDependsOnAsync_SetsDependsOnTaskId()
|
||||
public async Task SetDependsOnAsync_SetsDependsOnTaskIds()
|
||||
{
|
||||
var predecessor = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var task = await SeedTaskAsync(TaskStatus.Queued);
|
||||
|
||||
var result = await _sut.SetDependsOnAsync(task, predecessor, default);
|
||||
var result = await _sut.SetDependsOnAsync(task, [predecessor], default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
var t = await GetTaskAsync(task);
|
||||
Assert.Equal(predecessor, t.DependsOnTaskId);
|
||||
Assert.Equal([predecessor], await GetDependsOnAsync(task));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetDependsOnAsync_Null_ClearsDependsOnTaskId_AndWakesQueue()
|
||||
public async Task SetDependsOnAsync_SetsSeveralPredecessors()
|
||||
{
|
||||
var a = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var b = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var task = await SeedTaskAsync(TaskStatus.Queued);
|
||||
|
||||
Assert.True((await _sut.SetDependsOnAsync(task, [a, b], default)).Ok);
|
||||
|
||||
Assert.Equal(Sorted(a, b), Sorted(await GetDependsOnAsync(task)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetDependsOnAsync_ReplacingSet_KeepsOverlapAndDropsTheRest()
|
||||
{
|
||||
var a = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var b = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var c = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var task = await SeedTaskAsync(TaskStatus.Queued);
|
||||
Assert.True((await _sut.SetDependsOnAsync(task, [a, b], default)).Ok);
|
||||
|
||||
Assert.True((await _sut.SetDependsOnAsync(task, [b, c], default)).Ok);
|
||||
|
||||
Assert.Equal(Sorted(b, c), Sorted(await GetDependsOnAsync(task)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetDependsOnAsync_Empty_ClearsDependsOnTaskIds_AndWakesQueue()
|
||||
{
|
||||
var predecessor = await SeedTaskAsync(TaskStatus.Idle);
|
||||
var task = await SeedTaskAsync(TaskStatus.Queued);
|
||||
await _sut.SetDependsOnAsync(task, predecessor, default);
|
||||
await _sut.SetDependsOnAsync(task, [predecessor], default);
|
||||
var wakesBefore = _built.WakeCount();
|
||||
|
||||
var result = await _sut.SetDependsOnAsync(task, null, default);
|
||||
var result = await _sut.SetDependsOnAsync(task, [], default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
var t = await GetTaskAsync(task);
|
||||
Assert.Null(t.DependsOnTaskId);
|
||||
Assert.Empty(await GetDependsOnAsync(task));
|
||||
Assert.True(_built.WakeCount() > wakesBefore);
|
||||
}
|
||||
|
||||
@@ -692,11 +727,10 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
{
|
||||
var task = await SeedTaskAsync(TaskStatus.Queued);
|
||||
|
||||
var result = await _sut.SetDependsOnAsync(task, task, default);
|
||||
var result = await _sut.SetDependsOnAsync(task, [task], default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
var t = await GetTaskAsync(task);
|
||||
Assert.Null(t.DependsOnTaskId);
|
||||
Assert.Empty(await GetDependsOnAsync(task));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -704,7 +738,7 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
{
|
||||
var task = await SeedTaskAsync(TaskStatus.Queued);
|
||||
|
||||
var result = await _sut.SetDependsOnAsync(task, "does-not-exist", default);
|
||||
var result = await _sut.SetDependsOnAsync(task, ["does-not-exist"], default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
}
|
||||
@@ -714,7 +748,7 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
{
|
||||
var predecessor = await SeedTaskAsync(TaskStatus.Idle);
|
||||
|
||||
var result = await _sut.SetDependsOnAsync("does-not-exist", predecessor, default);
|
||||
var result = await _sut.SetDependsOnAsync("does-not-exist", [predecessor], default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
}
|
||||
@@ -724,15 +758,14 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
{
|
||||
var a = await SeedTaskAsync(TaskStatus.Queued);
|
||||
var b = await SeedTaskAsync(TaskStatus.Queued);
|
||||
var setup = await _sut.SetDependsOnAsync(b, a, default);
|
||||
var setup = await _sut.SetDependsOnAsync(b, [a], default);
|
||||
Assert.True(setup.Ok);
|
||||
|
||||
// a -> b would close the loop a -> b -> a.
|
||||
var result = await _sut.SetDependsOnAsync(a, b, default);
|
||||
var result = await _sut.SetDependsOnAsync(a, [b], default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
var t = await GetTaskAsync(a);
|
||||
Assert.Null(t.DependsOnTaskId);
|
||||
Assert.Empty(await GetDependsOnAsync(a));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -741,15 +774,31 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
var a = await SeedTaskAsync(TaskStatus.Queued);
|
||||
var b = await SeedTaskAsync(TaskStatus.Queued);
|
||||
var c = await SeedTaskAsync(TaskStatus.Queued);
|
||||
Assert.True((await _sut.SetDependsOnAsync(b, a, default)).Ok);
|
||||
Assert.True((await _sut.SetDependsOnAsync(c, b, default)).Ok);
|
||||
Assert.True((await _sut.SetDependsOnAsync(b, [a], default)).Ok);
|
||||
Assert.True((await _sut.SetDependsOnAsync(c, [b], default)).Ok);
|
||||
|
||||
// a -> c would close the loop a -> c -> b -> a.
|
||||
var result = await _sut.SetDependsOnAsync(a, c, default);
|
||||
var result = await _sut.SetDependsOnAsync(a, [c], default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetDependsOnAsync_CycleViaOneOfSeveralProposedEdges_Rejected()
|
||||
{
|
||||
// Only the second proposed edge closes a loop -- the check must look at every one of
|
||||
// them, not just the first, and must leave the whole set unwritten when one fails.
|
||||
var a = await SeedTaskAsync(TaskStatus.Queued);
|
||||
var b = await SeedTaskAsync(TaskStatus.Queued);
|
||||
var innocent = await SeedTaskAsync(TaskStatus.Idle);
|
||||
Assert.True((await _sut.SetDependsOnAsync(b, [a], default)).Ok);
|
||||
|
||||
var result = await _sut.SetDependsOnAsync(a, [innocent, b], default);
|
||||
|
||||
Assert.False(result.Ok);
|
||||
Assert.Empty(await GetDependsOnAsync(a));
|
||||
}
|
||||
|
||||
// ─── RecoverStaleRunningAsync ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user