feat(claude-do): merge [D2] Die 7 batch_*-MCP-Tools mit Element-Fortschritt (i/n)

ClaudeDo-Task: 286efaaf-b38e-47d4-9655-0c2d87e4b329
This commit is contained in:
Mika Kuns
2026-08-17 09:06:27 +02:00
3 changed files with 177 additions and 22 deletions
@@ -14,10 +14,19 @@ using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Usage;
using ClaudeDo.Worker.Worktrees;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.External;
// A synchronously-collecting IProgress<T>: Progress<T> marshals through the SynchronizationContext
// captured at construction, which is unreliable to assert on immediately in a test.
file sealed class SyncProgress<T> : IProgress<T>
{
public readonly List<T> Reports = new();
public void Report(T value) => Reports.Add(value);
}
public sealed class BatchMcpToolsTests : IDisposable
{
private readonly DbFixture _db = new();
@@ -498,4 +507,118 @@ public sealed class BatchMcpToolsTests : IDisposable
() => sut.BatchGetTasks(ids, cancellationToken: CancellationToken.None));
Assert.Contains("max", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BatchGetTasks_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var a = await SeedTaskAsync(listId);
var b = await SeedTaskAsync(listId);
var c = await SeedTaskAsync(listId);
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
await sut.BatchGetTasks(new[] { a.Id, b.Id, c.Id }, cancellationToken: CancellationToken.None, progress: progress);
Assert.Equal(3, progress.Reports.Count);
Assert.Equal(3, progress.Reports[2].Total);
}
[Fact]
public async Task BatchAddTasks_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
await sut.BatchAddTasks(listId, new[]
{
new BatchAddTaskInput("a"),
new BatchAddTaskInput("b"),
}, cancellationToken: CancellationToken.None, progress: progress);
Assert.Equal(2, progress.Reports.Count);
Assert.Equal(2, progress.Reports[1].Total);
}
[Fact]
public async Task BatchUpdateTaskStatus_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var t1 = await SeedTaskAsync(listId, "a", TaskStatus.Idle);
var t2 = await SeedTaskAsync(listId, "b", TaskStatus.Idle);
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
await sut.BatchUpdateTaskStatus(new[] { t1.Id, t2.Id }, "Queued", CancellationToken.None, progress);
Assert.Equal(2, progress.Reports.Count);
Assert.Equal(2, progress.Reports[1].Total);
}
[Fact]
public async Task BatchCancelTasks_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var t1 = await SeedTaskAsync(listId, "a", TaskStatus.Idle);
var t2 = await SeedTaskAsync(listId, "b", TaskStatus.Idle);
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
await sut.BatchCancelTasks(new[] { t1.Id, t2.Id }, CancellationToken.None, progress);
Assert.Equal(2, progress.Reports.Count);
Assert.Equal(2, progress.Reports[1].Total);
}
[Fact]
public async Task BatchDeleteTasks_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var t1 = await SeedTaskAsync(listId, "a", TaskStatus.Idle);
var t2 = await SeedTaskAsync(listId, "b", TaskStatus.Idle);
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
await sut.BatchDeleteTasks(new[] { t1.Id, t2.Id }, CancellationToken.None, progress);
Assert.Equal(2, progress.Reports.Count);
Assert.Equal(2, progress.Reports[1].Total);
}
[Fact]
public async Task BatchSetMyDay_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var t1 = await SeedTaskAsync(listId, "a", TaskStatus.Idle);
var t2 = await SeedTaskAsync(listId, "b", TaskStatus.Idle);
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
await sut.BatchSetMyDay(new[]
{
new BatchSetMyDayInput(t1.Id, true),
new BatchSetMyDayInput(t2.Id, true),
}, CancellationToken.None, progress);
Assert.Equal(2, progress.Reports.Count);
Assert.Equal(2, progress.Reports[1].Total);
}
[Fact]
public async Task BatchCleanupTaskWorktrees_ReportsProgressPerItem()
{
var listId = await SeedListAsync();
var t1 = await SeedTaskAsync(listId, "a", TaskStatus.Idle);
var t2 = await SeedTaskAsync(listId, "b", TaskStatus.Idle);
var sut = BuildSut();
var progress = new SyncProgress<ProgressNotificationValue>();
// Neither task has a worktree -- each item fails individually (ok=false), but the batch
// still reports progress per item since the loop doesn't abort on a per-item error.
await sut.BatchCleanupTaskWorktrees(new[] { t1.Id, t2.Id }, cancellationToken: CancellationToken.None, progress: progress);
Assert.Equal(2, progress.Reports.Count);
Assert.Equal(2, progress.Reports[1].Total);
}
}