From 912201770f8903a15d9aa189029ce2fc3ded114e Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 2 Sep 2026 12:20:02 +0200 Subject: [PATCH] fix(worker): add failureReason field to batch_get_tasks The tool description claimed Failed tasks carry failureReason, but the field was missing from BatchTaskDetailDto and rejected when requested in `fields`. Add failureReason to the DTO, KnownDetailFields, and BuildDetail so list handlers and other callers can read failure reasons without per-task get_task calls. Tested: 31 BatchMcpToolsTests pass, including two new cases for failureReason retrieval on Failed and Idle tasks. Co-Authored-By: Claude Haiku 4.5 --- src/ClaudeDo.Worker/External/BatchMcpTools.cs | 3 ++ .../External/BatchMcpToolsTests.cs | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/ClaudeDo.Worker/External/BatchMcpTools.cs b/src/ClaudeDo.Worker/External/BatchMcpTools.cs index 4d864718..aff487f8 100644 --- a/src/ClaudeDo.Worker/External/BatchMcpTools.cs +++ b/src/ClaudeDo.Worker/External/BatchMcpTools.cs @@ -45,6 +45,7 @@ public sealed record BatchTaskDetailDto( int? SortOrder = null, int? RoadblockCount = null, string? RoadblockText = null, + string? FailureReason = null, bool DescriptionTruncated = false, int? DescriptionFullLength = null, bool ResultTruncated = false, @@ -80,6 +81,7 @@ public sealed class BatchMcpTools { "listId", "title", "description", "status", "result", "createdBy", "createdAt", "startedAt", "finishedAt", "isMyDay", "sortOrder", "roadblockCount", "roadblockText", + "failureReason", }; private readonly ExternalMcpService _svc; @@ -187,6 +189,7 @@ public sealed class BatchMcpTools SortOrder: Want("sortOrder") ? t.SortOrder : null, RoadblockCount: Want("roadblockCount") ? t.RoadblockCount : null, RoadblockText: Want("roadblockText") ? ExtractRoadblockText(t.Result) : null, + FailureReason: Want("failureReason") ? t.FailureReason : null, DescriptionTruncated: descTruncated, DescriptionFullLength: descFullLength, ResultTruncated: resultTruncated, diff --git a/tests/ClaudeDo.Worker.Tests/External/BatchMcpToolsTests.cs b/tests/ClaudeDo.Worker.Tests/External/BatchMcpToolsTests.cs index 366d546f..db6e02f9 100644 --- a/tests/ClaudeDo.Worker.Tests/External/BatchMcpToolsTests.cs +++ b/tests/ClaudeDo.Worker.Tests/External/BatchMcpToolsTests.cs @@ -623,4 +623,38 @@ public sealed class BatchMcpToolsTests : IDisposable Assert.Equal(2, progress.Reports.Count); Assert.Equal(2, progress.Reports[1].Total); } + + [Fact] + public async Task BatchGetTasks_FailedTaskWithFailureReason_ReturnsReasonWhenRequested() + { + var listId = await SeedListAsync(); + var task = await SeedTaskAsync(listId, "failed", TaskStatus.Failed); + task.FailureReason = "max-turns-exceeded"; + await _tasks.UpdateAsync(task); + var sut = BuildSut(); + + var results = await sut.BatchGetTasks( + new[] { task.Id }, includeDescription: true, fields: new[] { "failureReason" }, + cancellationToken: CancellationToken.None); + + var found = results.Single(r => r.Id == task.Id).TaskFull!; + Assert.Equal("max-turns-exceeded", found.FailureReason); + Assert.Null(found.Description); + Assert.Null(found.Status); + } + + [Fact] + public async Task BatchGetTasks_IdleTaskWithoutFailureReason_ReturnsNullWhenRequested() + { + var listId = await SeedListAsync(); + var task = await SeedTaskAsync(listId, "idle", TaskStatus.Idle); + var sut = BuildSut(); + + var results = await sut.BatchGetTasks( + new[] { task.Id }, includeDescription: true, fields: new[] { "failureReason" }, + cancellationToken: CancellationToken.None); + + var found = results.Single(r => r.Id == task.Id).TaskFull!; + Assert.Null(found.FailureReason); + } }