feat(mcp/planning): allow status changes and post-finalize edits in active session

Extend UpdateChildTask with a status parameter (restricted to Draft, Manual,
Queued, Waiting) and replace the 'only Draft is editable' rule with 'planning
session is active'. Same loosening applied to DeleteChildTask. Lets planning
agents iterate on children that already escaped Draft state.
This commit is contained in:
Mika Kuns
2026-04-27 10:16:32 +02:00
parent 721c36a66b
commit 2d7f825ff3
2 changed files with 99 additions and 14 deletions

View File

@@ -138,11 +138,11 @@ public sealed class PlanningMcpServiceTests : IDisposable
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(otherChild.Id, "new", null, null, null, CancellationToken.None));
sut.UpdateChildTask(otherChild.Id, "new", null, null, null, null, CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_NotDraft_Throws()
public async Task UpdateChildTask_AfterFinalize_Throws()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
@@ -150,7 +150,75 @@ public sealed class PlanningMcpServiceTests : IDisposable
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(c.Id, "new", null, null, null, CancellationToken.None));
sut.UpdateChildTask(c.Id, "new", null, null, null, null, CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_SetsTags()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
var result = await sut.UpdateChildTask(c.Id, null, null, new[] { "agent", "custom-tag" }, null, null, CancellationToken.None);
Assert.Contains("agent", result.Tags);
Assert.Contains("custom-tag", result.Tags);
Assert.Equal(2, result.Tags.Count);
}
[Fact]
public async Task UpdateChildTask_ReplacesTagSet()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, new[] { "agent" }, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
var result = await sut.UpdateChildTask(c.Id, null, null, new[] { "manual" }, null, null, CancellationToken.None);
Assert.Single(result.Tags);
Assert.Equal("manual", result.Tags[0]);
}
[Fact]
public async Task UpdateChildTask_SetsStatus()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
var result = await sut.UpdateChildTask(c.Id, null, null, null, null, "Queued", CancellationToken.None);
Assert.Equal("Queued", result.Status);
var loaded = await _tasks.GetByIdAsync(c.Id);
Assert.Equal(TaskStatus.Queued, loaded!.Status);
}
[Fact]
public async Task UpdateChildTask_DisallowedStatus_Throws()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(c.Id, null, null, null, null, "Running", CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_UnknownStatus_Throws()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(c.Id, null, null, null, null, "NotARealStatus", CancellationToken.None));
}
[Fact]
@@ -215,7 +283,7 @@ public sealed class PlanningMcpServiceTests : IDisposable
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
await sut.UpdateChildTask(c.Id, "new title", null, null, null, CancellationToken.None);
await sut.UpdateChildTask(c.Id, "new title", null, null, null, null, CancellationToken.None);
var ids = TaskUpdatedIds();
Assert.Contains(c.Id, ids);