Merge claudedo/fc88143c0c704feab4ba6806cec49f4f

This commit is contained in:
mika kuns
2026-08-10 14:56:55 +02:00
3 changed files with 280 additions and 7 deletions
@@ -216,6 +216,156 @@ public sealed class BatchMcpToolsTests : IDisposable
Assert.Equal("the full description", found.TaskFull!.Description);
}
[Fact]
public async Task BatchGetTasks_DescriptionLongerThanCap_IsTruncated_WithFlagAndFullLength()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
var full = new string('x', 50);
task.Description = full;
await _tasks.UpdateAsync(task);
var sut = BuildSut();
var results = await sut.BatchGetTasks(
new[] { task.Id }, includeDescription: true, descriptionMaxChars: 10, cancellationToken: CancellationToken.None);
var found = results.Single(r => r.Id == task.Id).TaskFull!;
Assert.Equal(new string('x', 10), found.Description);
Assert.True(found.DescriptionTruncated);
Assert.Equal(50, found.DescriptionFullLength);
}
[Fact]
public async Task BatchGetTasks_DescriptionUnderCap_IsNotTruncated()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
task.Description = "short";
await _tasks.UpdateAsync(task);
var sut = BuildSut();
var results = await sut.BatchGetTasks(
new[] { task.Id }, includeDescription: true, cancellationToken: CancellationToken.None);
var found = results.Single(r => r.Id == task.Id).TaskFull!;
Assert.Equal("short", found.Description);
Assert.False(found.DescriptionTruncated);
Assert.Null(found.DescriptionFullLength);
}
[Fact]
public async Task BatchGetTasks_FieldsRestrictsPayload_ToRequestedFieldsOnly()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
task.Description = "a description that should not come back when fields excludes it";
await _tasks.UpdateAsync(task);
var sut = BuildSut();
var results = await sut.BatchGetTasks(
new[] { task.Id }, includeDescription: true, fields: new[] { "title", "status" },
cancellationToken: CancellationToken.None);
var found = results.Single(r => r.Id == task.Id).TaskFull!;
Assert.Equal(task.Title, found.Title);
Assert.NotNull(found.Status);
Assert.Null(found.Description);
Assert.Null(found.ListId);
Assert.Null(found.RoadblockCount);
}
[Fact]
public async Task BatchGetTasks_UnknownField_Throws()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
var sut = BuildSut();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => sut.BatchGetTasks(
new[] { task.Id }, includeDescription: true, fields: new[] { "notAField" },
cancellationToken: CancellationToken.None));
Assert.Contains("notAField", ex.Message);
}
[Fact]
public async Task BatchGetTasks_RoadblockTextField_ExtractsTailOfResult_WithoutFullResult()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
task.Result = "the task's own result\n\n⚠ Roadblocks reported during the run:\n- missing credentials";
await _tasks.UpdateAsync(task);
var sut = BuildSut();
var results = await sut.BatchGetTasks(
new[] { task.Id }, includeDescription: true, fields: new[] { "roadblockText" },
cancellationToken: CancellationToken.None);
var found = results.Single(r => r.Id == task.Id).TaskFull!;
Assert.Equal("- missing credentials", found.RoadblockText);
Assert.Null(found.Result);
}
[Fact]
public async Task BatchGetTasks_NoRoadblockMarker_RoadblockTextIsNull()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
task.Result = "plain result, no roadblocks";
await _tasks.UpdateAsync(task);
var sut = BuildSut();
var results = await sut.BatchGetTasks(
new[] { task.Id }, includeDescription: true, fields: new[] { "roadblockText" },
cancellationToken: CancellationToken.None);
Assert.Null(results.Single(r => r.Id == task.Id).TaskFull!.RoadblockText);
}
[Fact]
public async Task BatchGetTasks_OversizedResponse_ThrowsNamingTheFixingParameters()
{
var listId = await SeedListAsync();
var ids = new List<string>();
for (var i = 0; i < 10; i++)
{
var task = await SeedTaskAsync(listId, $"t{i}");
task.Description = new string('d', 6000);
await _tasks.UpdateAsync(task);
ids.Add(task.Id);
}
var sut = BuildSut();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => sut.BatchGetTasks(
ids.ToArray(), includeDescription: true, descriptionMaxChars: 6000, cancellationToken: CancellationToken.None));
Assert.Contains("descriptionMaxChars", ex.Message);
Assert.Contains("fields", ex.Message);
Assert.Contains("taskIds", ex.Message);
}
[Fact]
public async Task BatchGetTasks_TenTasksWithDescriptions_StaysWithinTokenBudget()
{
var listId = await SeedListAsync();
var ids = new List<string>();
for (var i = 0; i < 10; i++)
{
var task = await SeedTaskAsync(listId, $"t{i}");
task.Description = new string('d', 5000);
await _tasks.UpdateAsync(task);
ids.Add(task.Id);
}
var sut = BuildSut();
var results = await sut.BatchGetTasks(ids.ToArray(), includeDescription: true, cancellationToken: CancellationToken.None);
Assert.Equal(10, results.Count);
Assert.All(results, r => Assert.True(r.TaskFull!.DescriptionTruncated));
var json = System.Text.Json.JsonSerializer.Serialize(results);
Assert.True(json.Length < 25_000, $"response was {json.Length} chars, expected < 25000");
}
[Fact]
public async Task BatchDeleteTasks_RunningTask_ReportedNotOk_OthersDeleted()
{