feat(mcp/external): add UpdateTask for content/tag patching
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,33 @@ public sealed class ExternalMcpService
|
|||||||
return ToDto(entity);
|
return ToDto(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[McpServerTool, Description("Update an existing task's title, description, commit type, and/or tags. Pass null to leave a field unchanged. Tags are replaced as a full set when non-null. Refuses if the task is currently Running.")]
|
||||||
|
public async Task<TaskDto> UpdateTask(
|
||||||
|
string taskId,
|
||||||
|
string? title,
|
||||||
|
string? description,
|
||||||
|
string? commitType,
|
||||||
|
IReadOnlyList<string>? tags,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||||
|
if (task.Status == TaskStatus.Running)
|
||||||
|
throw new InvalidOperationException("Cannot update a running task. Cancel it first.");
|
||||||
|
|
||||||
|
if (title is not null) task.Title = title;
|
||||||
|
if (description is not null) task.Description = description;
|
||||||
|
if (commitType is not null) task.CommitType = commitType;
|
||||||
|
await _tasks.UpdateAsync(task, cancellationToken);
|
||||||
|
|
||||||
|
if (tags is not null)
|
||||||
|
await _tasks.SetTagsAsync(taskId, tags, cancellationToken);
|
||||||
|
|
||||||
|
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||||
|
await _broadcaster.TaskUpdated(taskId);
|
||||||
|
return ToDto(reload);
|
||||||
|
}
|
||||||
|
|
||||||
[McpServerTool, Description("Update a task's status. Only 'Manual' and 'Queued' are permitted — use RunTaskNow or CancelTask for execution control.")]
|
[McpServerTool, Description("Update a task's status. Only 'Manual' and 'Queued' are permitted — use RunTaskNow or CancelTask for execution control.")]
|
||||||
public async Task<TaskDto> UpdateTaskStatus(
|
public async Task<TaskDto> UpdateTaskStatus(
|
||||||
string taskId,
|
string taskId,
|
||||||
|
|||||||
@@ -172,4 +172,57 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
|
|
||||||
Assert.Empty(await _tasks.GetTagsAsync(dto.Id));
|
Assert.Empty(await _tasks.GetTagsAsync(dto.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_PatchesNonNullFieldsOnly()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId, "old title");
|
||||||
|
var queue = CreateQueue();
|
||||||
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
|
var dto = await sut.UpdateTask(task.Id, "new title", null, null, null, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal("new title", dto.Title);
|
||||||
|
var loaded = await _tasks.GetByIdAsync(task.Id);
|
||||||
|
Assert.Equal("new title", loaded!.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_TagsReplaceFullSet()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId);
|
||||||
|
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
|
||||||
|
var queue = CreateQueue();
|
||||||
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
|
await sut.UpdateTask(task.Id, null, null, null, new[] { "manual" }, CancellationToken.None);
|
||||||
|
|
||||||
|
var tags = await _tasks.GetTagsAsync(task.Id);
|
||||||
|
Assert.Single(tags);
|
||||||
|
Assert.Equal("manual", tags[0].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_OnRunning_Throws()
|
||||||
|
{
|
||||||
|
var listId = await SeedListAsync();
|
||||||
|
var task = await SeedTaskAsync(listId, status: TaskStatus.Running);
|
||||||
|
var queue = CreateQueue();
|
||||||
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sut.UpdateTask(task.Id, "x", null, null, null, CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateTask_NotFound_Throws()
|
||||||
|
{
|
||||||
|
var queue = CreateQueue();
|
||||||
|
var sut = BuildSut(queue);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
sut.UpdateTask("does-not-exist", "x", null, null, null, CancellationToken.None));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user