Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/External/TaskIdResolverTests.cs
T
mika kuns 2a3133efad feat(worker): accept #123 task numbers as MCP tool input
TaskIdResolver resolves a #123/bare-123 taskId parameter to its GUID
before any lookup, across every External/ MCP tool that takes a task
id, including the batch tools' id arrays (via delegation to the
already-resolving single-entity methods) and update_task's
dependsOnTaskId (empty string still passes through unchanged as the
clear-link sentinel). An unknown number throws a clear error instead
of a silent null. McpToolDocs.TaskNumberHint tells the agent to refer
to tasks as #<number> when reporting to the user, added to the
description of get_task, list_tasks, add_task, update_task_status and
review_task.
2026-08-11 13:10:49 +02:00

105 lines
3.1 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.External;
public sealed class TaskIdResolverTests : IDisposable
{
private readonly DbFixture _db = new();
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
public TaskIdResolverTests()
{
_ctx = _db.CreateContext();
_tasks = new TaskRepository(_ctx);
_lists = new ListRepository(_ctx);
}
public void Dispose() { _ctx.Dispose(); _db.Dispose(); }
private async Task<TaskEntity> SeedTaskAsync()
{
var listId = Guid.NewGuid().ToString();
await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
var task = new TaskEntity
{
Id = Guid.NewGuid().ToString(), ListId = listId, Title = "t",
Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, CommitType = "chore",
};
await _tasks.AddAsync(task);
return task;
}
[Fact]
public async Task ResolveAsync_HashNumber_ResolvesToGuid()
{
var task = await SeedTaskAsync();
var resolved = await TaskIdResolver.ResolveAsync(_tasks, $"#{task.Number}", CancellationToken.None);
Assert.Equal(task.Id, resolved);
}
[Fact]
public async Task ResolveAsync_BareNumber_ResolvesToGuid()
{
var task = await SeedTaskAsync();
var resolved = await TaskIdResolver.ResolveAsync(_tasks, task.Number.ToString(), CancellationToken.None);
Assert.Equal(task.Id, resolved);
}
[Fact]
public async Task ResolveAsync_Guid_PassesThroughUnchanged()
{
var task = await SeedTaskAsync();
var resolved = await TaskIdResolver.ResolveAsync(_tasks, task.Id, CancellationToken.None);
Assert.Equal(task.Id, resolved);
}
[Fact]
public async Task ResolveAsync_UnknownNumber_ThrowsWithNumberInMessage()
{
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
TaskIdResolver.ResolveAsync(_tasks, "#999999", CancellationToken.None));
Assert.Contains("999999", ex.Message);
}
[Fact]
public async Task ResolveOptionalAsync_EmptyString_PassesThroughUnchanged()
{
var resolved = await TaskIdResolver.ResolveOptionalAsync(_tasks, "", CancellationToken.None);
Assert.Equal("", resolved);
}
[Fact]
public async Task ResolveOptionalAsync_Null_ReturnsNull()
{
var resolved = await TaskIdResolver.ResolveOptionalAsync(_tasks, null, CancellationToken.None);
Assert.Null(resolved);
}
[Fact]
public async Task ResolveManyAsync_MixedArray_ResolvesBoth()
{
var a = await SeedTaskAsync();
var b = await SeedTaskAsync();
var resolved = await TaskIdResolver.ResolveManyAsync(_tasks, [$"#{a.Number}", b.Id], CancellationToken.None);
Assert.Equal(new[] { a.Id, b.Id }, resolved);
}
}