fix(mcp): report a Failed task's failureReason instead of a bare status
get_task/batch_get_tasks now return failureReason (max_turns|timeout|error| cancelled|unknown) plus failureTurnsUsed/failureMaxTurns on a Failed task, so max_turns (worktree usually fine, continue_task) is distinguishable from a real error (reset_failed_task) without pulling get_task_log's raw NDJSON. Classified and stamped onto TaskEntity by TaskRunner.MarkFailed via TaskStateService.FailAsync; TaskRunEntity also keeps the CLI's raw terminal_reason/result_subtype/errors for deeper diagnosis. reset_failed_task's description now warns explicitly that it discards the worktree and points at continue_task for max_turns. Surfaced on the task card's status-chip tooltip.
This commit is contained in:
@@ -224,6 +224,68 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
Assert.Equal("the full description text", dto.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTask_Failed_ReturnsClassifiedFailureReason()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Failed);
|
||||
task.FailureReason = "max_turns";
|
||||
task.FailureTurnsUsed = 55;
|
||||
task.FailureMaxTurns = 60;
|
||||
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.GetTask(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal("max_turns", dto.FailureReason);
|
||||
Assert.Equal(55, dto.FailureTurnsUsed);
|
||||
Assert.Equal(60, dto.FailureMaxTurns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTask_FailedWithoutStoredReason_ReturnsUnknownNotError()
|
||||
{
|
||||
// A Failed task written before this field existed has FailureReason=null in the DB —
|
||||
// the MCP surface must still hand back a defined value, not null or a throw.
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Failed);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.GetTask(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal("unknown", dto.FailureReason);
|
||||
Assert.Null(dto.FailureTurnsUsed);
|
||||
Assert.Null(dto.FailureMaxTurns);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTask_NotFailed_FailureReasonIsNull()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Idle);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.GetTask(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Null(dto.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskRefAsync_Failed_ReturnsClassifiedFailureReason()
|
||||
{
|
||||
// GetTaskRefAsync backs batch_get_tasks' default (lean) path — the same field must be
|
||||
// present there, not just on the full get_task DTO.
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Failed);
|
||||
task.FailureReason = "error";
|
||||
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.GetTaskRefAsync(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal("error", dto.FailureReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTask_OnRunning_Throws()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user