refactor(tags): remove tag entity and all references

Drops TagEntity, TagRepository, and tag wiring across data layer, worker,
and UI. Adds RemoveTags migration to clean up schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-19 08:07:24 +02:00
parent 8d34db3f9b
commit 623ebf147b
42 changed files with 333 additions and 1118 deletions

View File

@@ -52,7 +52,6 @@ public sealed class ExternalMcpServiceTests : IDisposable
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
private readonly TagRepository _tags;
private readonly ExternalFakeHubContext _hub = new();
private readonly HubBroadcaster _broadcaster;
@@ -61,7 +60,6 @@ public sealed class ExternalMcpServiceTests : IDisposable
_ctx = _db.CreateContext();
_tasks = new TaskRepository(_ctx);
_lists = new ListRepository(_ctx);
_tags = new TagRepository(_ctx);
_broadcaster = new HubBroadcaster(_hub);
}
@@ -89,12 +87,8 @@ public sealed class ExternalMcpServiceTests : IDisposable
return task;
}
// QueueService is needed by ExternalMcpService's constructor. For tests that
// only exercise UpdateTask / DeleteTask / SetTaskTags / ListTags / ListTags,
// we never call its WakeQueue/RunNow/CancelTask paths, so a real QueueService
// built with the same approach used in QueueServiceTests is sufficient.
private ExternalMcpService BuildSut(QueueService queue) =>
new(_tasks, _lists, queue, _broadcaster, _tags,
new(_tasks, _lists, queue, _broadcaster,
TaskStateServiceBuilder.Build(_db.CreateFactory()).State);
private QueueService CreateQueue()
@@ -129,54 +123,6 @@ public sealed class ExternalMcpServiceTests : IDisposable
Assert.NotNull(await _tasks.GetByIdAsync(task.Id));
}
[Fact]
public async Task ListTags_ReturnsSeededAndCustomTags()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
await _tasks.SetTagsAsync(task.Id, new[] { "agent", "custom-tag" });
var queue = CreateQueue();
var sut = BuildSut(queue);
var tags = await sut.ListTags(CancellationToken.None);
Assert.Contains(tags, t => t.Name == "agent");
Assert.Contains(tags, t => t.Name == "custom-tag");
}
[Fact]
public async Task AddTask_WithTags_AttachesTags()
{
var listId = await SeedListAsync();
var queue = CreateQueue();
var sut = BuildSut(queue);
var dto = await sut.AddTask(
listId, "scope-creep handoff", "desc", "claude-cli",
queueImmediately: false,
tags: new[] { "agent", "custom" },
CancellationToken.None);
var tags = await _tasks.GetTagsAsync(dto.Id);
Assert.Contains(tags, t => t.Name == "agent");
Assert.Contains(tags, t => t.Name == "custom");
}
[Fact]
public async Task AddTask_NullTags_BehavesAsBefore()
{
var listId = await SeedListAsync();
var queue = CreateQueue();
var sut = BuildSut(queue);
var dto = await sut.AddTask(
listId, "no tags", null, "claude-cli",
queueImmediately: false, tags: null, CancellationToken.None);
Assert.Empty(await _tasks.GetTagsAsync(dto.Id));
}
[Fact]
public async Task UpdateTask_PatchesNonNullFieldsOnly()
{
@@ -185,29 +131,13 @@ public sealed class ExternalMcpServiceTests : IDisposable
var queue = CreateQueue();
var sut = BuildSut(queue);
var dto = await sut.UpdateTask(task.Id, "new title", null, null, null, CancellationToken.None);
var dto = await sut.UpdateTask(task.Id, "new title", null, null, CancellationToken.None);
Assert.Equal("new title", dto.Title);
var loaded = await _tasks.GetByIdAsync(task.Id);
Assert.Equal("new title", loaded!.Title);
}
[Fact]
public async Task UpdateTask_TagsReplaceFullSet()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
var queue = CreateQueue();
var sut = BuildSut(queue);
await sut.UpdateTask(task.Id, null, null, null, new[] { "manual" }, CancellationToken.None);
var tags = await _tasks.GetTagsAsync(task.Id);
Assert.Single(tags);
Assert.Equal("manual", tags[0].Name);
}
[Fact]
public async Task UpdateTask_OnRunning_Throws()
{
@@ -217,7 +147,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
var sut = BuildSut(queue);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateTask(task.Id, "x", null, null, null, CancellationToken.None));
sut.UpdateTask(task.Id, "x", null, null, CancellationToken.None));
}
[Fact]
@@ -227,15 +157,14 @@ public sealed class ExternalMcpServiceTests : IDisposable
var sut = BuildSut(queue);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateTask("does-not-exist", "x", null, null, null, CancellationToken.None));
sut.UpdateTask("does-not-exist", "x", null, null, CancellationToken.None));
}
[Fact]
public async Task DeleteTask_RemovesTaskAndTagJoins()
public async Task DeleteTask_RemovesTask()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
var queue = CreateQueue();
var sut = BuildSut(queue);
@@ -265,34 +194,4 @@ public sealed class ExternalMcpServiceTests : IDisposable
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.DeleteTask("does-not-exist", CancellationToken.None));
}
[Fact]
public async Task SetTaskTags_ReplacesTagSetAndBroadcasts()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId);
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
var queue = CreateQueue();
var sut = BuildSut(queue);
var dto = await sut.SetTaskTags(task.Id, new[] { "manual" }, CancellationToken.None);
var tags = await _tasks.GetTagsAsync(task.Id);
Assert.Single(tags);
Assert.Equal("manual", tags[0].Name);
Assert.Contains(_hub.RecordingClients.Proxy.Calls,
c => c.Method == "TaskUpdated" && (string)c.Args[0]! == task.Id);
}
[Fact]
public async Task SetTaskTags_OnRunning_Throws()
{
var listId = await SeedListAsync();
var task = await SeedTaskAsync(listId, status: TaskStatus.Running);
var queue = CreateQueue();
var sut = BuildSut(queue);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.SetTaskTags(task.Id, new[] { "manual" }, CancellationToken.None));
}
}

View File

@@ -142,8 +142,8 @@ public sealed class PlanningHubTests : IDisposable
{
var (_, taskId) = await SeedAsync();
await _planning.StartAsync(taskId, CancellationToken.None);
await _tasks.CreateChildAsync(taskId, "child 1", null, null, null);
await _tasks.CreateChildAsync(taskId, "child 2", null, null, null);
await _tasks.CreateChildAsync(taskId, "child 1", null, null);
await _tasks.CreateChildAsync(taskId, "child 2", null, null);
_proxy.Sent.Clear();
var hub = CreateHub();
@@ -158,8 +158,8 @@ public sealed class PlanningHubTests : IDisposable
{
var (_, taskId) = await SeedAsync();
await _planning.StartAsync(taskId, CancellationToken.None);
await _tasks.CreateChildAsync(taskId, "c1", null, null, null);
await _tasks.CreateChildAsync(taskId, "c2", null, null, null);
await _tasks.CreateChildAsync(taskId, "c1", null, null);
await _tasks.CreateChildAsync(taskId, "c2", null, null);
var hub = CreateHub();
var count = await hub.GetPendingDraftCountAsync(taskId);

View File

@@ -65,7 +65,6 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
await using var ctx = _factory.CreateDbContext();
return await ctx.Tasks
.AsNoTracking()
.Include(t => t.Tags)
.Where(t => t.ParentTaskId == parentId)
.OrderBy(t => t.SortOrder)
.ToListAsync();
@@ -88,17 +87,6 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
Assert.Equal(kids[1].Id, kids[2].BlockedByTaskId);
}
[Fact]
public async Task SetupChain_AttachesAgentTagToAllChildren()
{
await SeedPlanningFamilyAsync("P", 2);
await _sut.SetupChainAsync("P", default);
var kids = await GetChildrenAsync("P");
Assert.All(kids, k => Assert.Contains(k.Tags, t => t.Name == "agent"));
}
[Fact]
public async Task SetupChain_AcceptsIdleChildren()
{

View File

@@ -111,8 +111,8 @@ public sealed class PlanningEndToEndTests : IDisposable
// Wire the ambient context so _svc reads the correct parent
_httpContext.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };
await _svc.CreateChildTask("sub 1", null, null, null, CancellationToken.None);
await _svc.CreateChildTask("sub 2", null, null, null, CancellationToken.None);
await _svc.CreateChildTask("sub 1", null, null, CancellationToken.None);
await _svc.CreateChildTask("sub 2", null, null, CancellationToken.None);
var count = await _svc.Finalize(true, CancellationToken.None);
Assert.Equal(2, count);
@@ -154,9 +154,9 @@ public sealed class PlanningEndToEndTests : IDisposable
await _manager.StartAsync(parent.Id, CancellationToken.None);
_httpContext.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };
await _svc.CreateChildTask("c1", null, null, null, CancellationToken.None);
await _svc.CreateChildTask("c2", null, null, null, CancellationToken.None);
await _svc.CreateChildTask("c3", null, null, null, CancellationToken.None);
await _svc.CreateChildTask("c1", null, null, CancellationToken.None);
await _svc.CreateChildTask("c2", null, null, CancellationToken.None);
await _svc.CreateChildTask("c3", null, null, CancellationToken.None);
var kidsBefore = await _tasks.GetChildrenAsync(parent.Id);
var firstChildId = kidsBefore[0].Id;

View File

@@ -108,7 +108,7 @@ public sealed class PlanningMcpServiceTests : IDisposable
var parent = await SeedPlanningParentAsync();
var sut = BuildSut(parent.Id);
var result = await sut.CreateChildTask("My child", "desc", null, null, CancellationToken.None);
var result = await sut.CreateChildTask("My child", "desc", null, CancellationToken.None);
Assert.Equal("Idle", result.Status);
var child = await _tasks.GetByIdAsync(result.TaskId);
@@ -122,8 +122,8 @@ public sealed class PlanningMcpServiceTests : IDisposable
var parent = await SeedPlanningParentAsync();
var other = await SeedPlanningParentAsync();
await _tasks.CreateChildAsync(parent.Id, "mine", null, null, null);
await _tasks.CreateChildAsync(other.Id, "theirs", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "mine", null, null);
await _tasks.CreateChildAsync(other.Id, "theirs", null, null);
var sut = BuildSut(parent.Id);
var list = await sut.ListChildTasks(CancellationToken.None);
@@ -136,18 +136,18 @@ public sealed class PlanningMcpServiceTests : IDisposable
{
var parent = await SeedPlanningParentAsync();
var other = await SeedPlanningParentAsync();
var otherChild = await _tasks.CreateChildAsync(other.Id, "x", null, null, null);
var otherChild = await _tasks.CreateChildAsync(other.Id, "x", null, null);
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(otherChild.Id, "new", null, null, null, null, CancellationToken.None));
sut.UpdateChildTask(otherChild.Id, "new", null, null, null, CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_AfterFinalize_Throws()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
// Simulate post-finalize state directly: parent.PlanningPhase=Finalized
// is the gate the MCP service checks.
var sut = BuildSut(parent.Id);
@@ -155,47 +155,18 @@ public sealed class PlanningMcpServiceTests : IDisposable
Assert.True(result.Ok, result.Reason);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(c.Id, "new", null, null, null, null, CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_SetsTags()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
var result = await sut.UpdateChildTask(c.Id, null, null, new[] { "agent", "custom-tag" }, null, null, CancellationToken.None);
Assert.Contains("agent", result.Tags);
Assert.Contains("custom-tag", result.Tags);
Assert.Equal(2, result.Tags.Count);
}
[Fact]
public async Task UpdateChildTask_ReplacesTagSet()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, new[] { "agent" }, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
var result = await sut.UpdateChildTask(c.Id, null, null, new[] { "manual" }, null, null, CancellationToken.None);
Assert.Single(result.Tags);
Assert.Equal("manual", result.Tags[0]);
sut.UpdateChildTask(c.Id, "new", null, null, null, CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_SetsStatus()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
var result = await sut.UpdateChildTask(c.Id, null, null, null, null, "Queued", CancellationToken.None);
var result = await sut.UpdateChildTask(c.Id, null, null, null, "Queued", CancellationToken.None);
Assert.Equal("Queued", result.Status);
var loaded = await _tasks.GetByIdAsync(c.Id);
@@ -206,31 +177,31 @@ public sealed class PlanningMcpServiceTests : IDisposable
public async Task UpdateChildTask_DisallowedStatus_Throws()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(c.Id, null, null, null, null, "Running", CancellationToken.None));
sut.UpdateChildTask(c.Id, null, null, null, "Running", CancellationToken.None));
}
[Fact]
public async Task UpdateChildTask_UnknownStatus_Throws()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
sut.UpdateChildTask(c.Id, null, null, null, null, "NotARealStatus", CancellationToken.None));
sut.UpdateChildTask(c.Id, null, null, null, "NotARealStatus", CancellationToken.None));
}
[Fact]
public async Task DeleteChildTask_RemovesDraft()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
var sut = BuildSut(parent.Id);
await sut.DeleteChildTask(c.Id, CancellationToken.None);
@@ -255,8 +226,8 @@ public sealed class PlanningMcpServiceTests : IDisposable
public async Task Finalize_PromotesDraftsAndInvalidatesToken()
{
var parent = await SeedPlanningParentAsync();
await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
await _tasks.CreateChildAsync(parent.Id, "c2", null, null);
var sut = BuildSut(parent.Id);
var count = await sut.Finalize(true, CancellationToken.None);
@@ -273,7 +244,7 @@ public sealed class PlanningMcpServiceTests : IDisposable
var parent = await SeedPlanningParentAsync();
var sut = BuildSut(parent.Id);
var result = await sut.CreateChildTask("c", null, null, null, CancellationToken.None);
var result = await sut.CreateChildTask("c", null, null, CancellationToken.None);
var ids = TaskUpdatedIds();
Assert.Contains(result.TaskId, ids);
@@ -284,11 +255,11 @@ public sealed class PlanningMcpServiceTests : IDisposable
public async Task UpdateChildTask_BroadcastsBothChildAndParent()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
_ctx.ChangeTracker.Clear();
var sut = BuildSut(parent.Id);
await sut.UpdateChildTask(c.Id, "new title", null, null, null, null, CancellationToken.None);
await sut.UpdateChildTask(c.Id, "new title", null, null, null, CancellationToken.None);
var ids = TaskUpdatedIds();
Assert.Contains(c.Id, ids);
@@ -299,7 +270,7 @@ public sealed class PlanningMcpServiceTests : IDisposable
public async Task DeleteChildTask_BroadcastsBothChildAndParent()
{
var parent = await SeedPlanningParentAsync();
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
var sut = BuildSut(parent.Id);
await sut.DeleteChildTask(c.Id, CancellationToken.None);
@@ -313,8 +284,8 @@ public sealed class PlanningMcpServiceTests : IDisposable
public async Task Finalize_BroadcastsEachChildAndParent()
{
var parent = await SeedPlanningParentAsync();
var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null);
var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null);
var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null);
var sut = BuildSut(parent.Id);
await sut.Finalize(true, CancellationToken.None);

View File

@@ -131,7 +131,7 @@ public sealed class PlanningSessionManagerTests : IDisposable
var (listId, _) = await SeedListAsync();
var parent = await SeedManualTaskAsync(listId);
await _tasks.SetPlanningStartedAsync(parent.Id, "t");
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_sut.StartAsync(child.Id, CancellationToken.None));
@@ -182,8 +182,8 @@ public sealed class PlanningSessionManagerTests : IDisposable
var (listId, _) = await SeedListAsync();
var parent = await SeedManualTaskAsync(listId);
await _sut.StartAsync(parent.Id, CancellationToken.None);
await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
await _tasks.CreateChildAsync(parent.Id, "c2", null, null);
var count = await _sut.FinalizeAsync(parent.Id, queueAgentTasks: true, CancellationToken.None);
@@ -200,9 +200,9 @@ public sealed class PlanningSessionManagerTests : IDisposable
var (listId, _) = await SeedListAsync();
var parent = await SeedManualTaskAsync(listId);
await _sut.StartAsync(parent.Id, CancellationToken.None);
await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c3", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
await _tasks.CreateChildAsync(parent.Id, "c2", null, null);
await _tasks.CreateChildAsync(parent.Id, "c3", null, null);
var n = await _sut.GetPendingDraftCountAsync(parent.Id, CancellationToken.None);

View File

@@ -13,7 +13,6 @@ public sealed class QueuePickerTests : IDisposable
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
private readonly TagRepository _tags;
private readonly QueuePicker _picker;
public QueuePickerTests()
@@ -21,7 +20,6 @@ public sealed class QueuePickerTests : IDisposable
_ctx = _db.CreateContext();
_tasks = new TaskRepository(_ctx);
_lists = new ListRepository(_ctx);
_tags = new TagRepository(_ctx);
_picker = new QueuePicker(_db.CreateFactory());
}
@@ -40,11 +38,6 @@ public sealed class QueuePickerTests : IDisposable
Name = "Test",
CreatedAt = DateTime.UtcNow,
});
if (listAgentTag)
{
var tagId = await _tags.GetOrCreateAsync("agent");
await _lists.AddTagAsync(listId, tagId);
}
return listId;
}
@@ -69,11 +62,6 @@ public sealed class QueuePickerTests : IDisposable
CommitType = "feat",
};
await _tasks.AddAsync(task);
if (taskAgentTag)
{
var tagId = await _tags.GetOrCreateAsync("agent");
await _tasks.AddTagAsync(task.Id, tagId);
}
if (sortOrder is not null)
{
task.SortOrder = sortOrder.Value;

View File

@@ -10,13 +10,11 @@ public sealed class ListRepositoryTests : IDisposable
private readonly DbFixture _db = new();
private readonly ClaudeDoDbContext _ctx;
private readonly ListRepository _lists;
private readonly TagRepository _tags;
public ListRepositoryTests()
{
_ctx = _db.CreateContext();
_lists = new ListRepository(_ctx);
_tags = new TagRepository(_ctx);
}
public void Dispose()
@@ -95,20 +93,4 @@ public sealed class ListRepositoryTests : IDisposable
Assert.True(all.Count >= 2);
}
[Fact]
public async Task TagJunction_AddAndRemove()
{
var listId = Guid.NewGuid().ToString();
await _lists.AddAsync(new ListEntity { Id = listId, Name = "Tagged", CreatedAt = DateTime.UtcNow });
var tagId = await _tags.GetOrCreateAsync("agent");
await _lists.AddTagAsync(listId, tagId);
var tags = await _lists.GetTagsAsync(listId);
Assert.Single(tags);
Assert.Equal("agent", tags[0].Name);
await _lists.RemoveTagAsync(listId, tagId);
tags = await _lists.GetTagsAsync(listId);
Assert.Empty(tags);
}
}

View File

@@ -68,7 +68,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
await _tasks.AddAsync(parent);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => _tasks.CreateChildAsync(parent.Id, "child", null, null, null));
() => _tasks.CreateChildAsync(parent.Id, "child", null, null));
Assert.Contains("not in a planning phase", ex.Message);
}
@@ -78,7 +78,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
var child = await _tasks.CreateChildAsync(parent.Id, "child", null, null, null);
var child = await _tasks.CreateChildAsync(parent.Id, "child", null, null);
Assert.Equal(parent.Id, child.ParentTaskId);
Assert.Equal(TaskStatus.Idle, child.Status);
}
@@ -101,8 +101,8 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
await _tasks.CreateChildAsync(parent.Id, "a", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "b", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "a", null, null);
await _tasks.CreateChildAsync(parent.Id, "b", null, null);
var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);
@@ -117,7 +117,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
await SetChildStatusAsync(child.Id, TaskStatus.Queued);
var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);
@@ -134,7 +134,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
await SetChildStatusAsync(child.Id, TaskStatus.Queued);
var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: true);
@@ -149,7 +149,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
await SetChildStatusAsync(child.Id, TaskStatus.Running);
var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: true);
@@ -164,8 +164,8 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
var done = await _tasks.CreateChildAsync(parent.Id, "done", null, null, null);
var failed = await _tasks.CreateChildAsync(parent.Id, "failed", null, null, null);
var done = await _tasks.CreateChildAsync(parent.Id, "done", null, null);
var failed = await _tasks.CreateChildAsync(parent.Id, "failed", null, null);
await SetChildStatusAsync(done.Id, TaskStatus.Done);
await SetChildStatusAsync(failed.Id, TaskStatus.Failed);
@@ -220,7 +220,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable
{
var listId = await CreateListAsync();
var parent = await SeedPlanningParentAsync(listId);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
var child = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
var dequeued = await _tasks.DequeueOrphanedChildrenAsync();
Assert.Equal(0, dequeued);

View File

@@ -12,14 +12,12 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
private readonly TagRepository _tags;
public TaskRepositoryPlanningTests()
{
_ctx = _db.CreateContext();
_tasks = new TaskRepository(_ctx);
_lists = new ListRepository(_ctx);
_tags = new TagRepository(_ctx);
}
public void Dispose()
@@ -97,7 +95,6 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
parent.Id,
title: "child title",
description: "child desc",
tagNames: new[] { "agent" },
commitType: "feat");
Assert.Equal(TaskStatus.Idle, child.Status);
@@ -110,9 +107,6 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
var loaded = await _tasks.GetByIdAsync(child.Id);
Assert.NotNull(loaded);
Assert.Equal(TaskStatus.Idle, loaded!.Status);
var tags = await _tasks.GetTagsAsync(child.Id);
Assert.Contains(tags, t => t.Name == "agent");
}
[Fact]
@@ -122,7 +116,7 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
_ = listId;
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_tasks.CreateChildAsync("nonexistent-parent-id", "t", null, null, null));
_tasks.CreateChildAsync("nonexistent-parent-id", "t", null, null));
}
[Fact]
@@ -202,8 +196,8 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
await _tasks.AddAsync(parent);
await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
await _tasks.UpdatePlanningSessionIdAsync(parent.Id, "claude-42");
var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null, null);
var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null, null);
var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null);
var outcome = await _tasks.DiscardPlanningAsync(parent.Id, dequeueQueuedChildren: false);
@@ -237,7 +231,7 @@ public sealed class TaskRepositoryPlanningTests : IDisposable
var listId = await CreateListAsync();
var parent = MakeTask(listId, phase: PlanningPhase.Active);
await _tasks.AddAsync(parent);
await _tasks.CreateChildAsync(parent.Id, "c", null, null, null);
await _tasks.CreateChildAsync(parent.Id, "c", null, null);
await Assert.ThrowsAsync<Microsoft.Data.Sqlite.SqliteException>(async () =>
{

View File

@@ -12,14 +12,12 @@ public sealed class TaskRepositoryTests : IDisposable
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
private readonly TagRepository _tags;
public TaskRepositoryTests()
{
_ctx = _db.CreateContext();
_tasks = new TaskRepository(_ctx);
_lists = new ListRepository(_ctx);
_tags = new TagRepository(_ctx);
}
public void Dispose()
@@ -239,83 +237,4 @@ public sealed class TaskRepositoryTests : IDisposable
Assert.Equal(0, reloadB!.SortOrder);
}
[Fact]
public async Task GetEffectiveTagsAsync_Returns_Union_Of_ListTags_And_TaskTags()
{
var listId = await CreateListAsync();
var agentTagId = await _tags.GetOrCreateAsync("agent");
var manualTagId = await _tags.GetOrCreateAsync("manual");
var codeTagId = await _tags.GetOrCreateAsync("code");
await _lists.AddTagAsync(listId, agentTagId);
var task = MakeTask(listId);
await _tasks.AddAsync(task);
await _tasks.AddTagAsync(task.Id, manualTagId);
await _tasks.AddTagAsync(task.Id, codeTagId);
var effective = await _tasks.GetEffectiveTagsAsync(task.Id);
var names = effective.Select(t => t.Name).OrderBy(n => n).ToList();
Assert.Equal(3, names.Count);
Assert.Contains("agent", names);
Assert.Contains("code", names);
Assert.Contains("manual", names);
}
[Fact]
public async Task SetTagsAsync_AttachesNewTagsAndCreatesMissingRows()
{
var listId = await CreateListAsync("L");
var task = MakeTask(listId);
await _tasks.AddAsync(task);
await _tasks.SetTagsAsync(task.Id, new[] { "agent", "novel-tag" });
var tags = await _tasks.GetTagsAsync(task.Id);
Assert.Contains(tags, t => t.Name == "agent");
Assert.Contains(tags, t => t.Name == "novel-tag");
Assert.Equal(2, tags.Count);
}
[Fact]
public async Task SetTagsAsync_ReplacesExistingTagSet()
{
var listId = await CreateListAsync("L");
var task = MakeTask(listId);
await _tasks.AddAsync(task);
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
await _tasks.SetTagsAsync(task.Id, new[] { "manual" });
var tags = await _tasks.GetTagsAsync(task.Id);
Assert.Single(tags);
Assert.Equal("manual", tags[0].Name);
}
[Fact]
public async Task SetTagsAsync_DeduplicatesCaseInsensitively()
{
var listId = await CreateListAsync("L");
var task = MakeTask(listId);
await _tasks.AddAsync(task);
await _tasks.SetTagsAsync(task.Id, new[] { "agent", "AGENT", "Agent" });
var tags = await _tasks.GetTagsAsync(task.Id);
Assert.Single(tags);
}
[Fact]
public async Task SetTagsAsync_EmptyListClearsAllTags()
{
var listId = await CreateListAsync("L");
var task = MakeTask(listId);
await _tasks.AddAsync(task);
await _tasks.SetTagsAsync(task.Id, new[] { "agent" });
await _tasks.SetTagsAsync(task.Id, Array.Empty<string>());
Assert.Empty(await _tasks.GetTagsAsync(task.Id));
}
}

View File

@@ -18,7 +18,6 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _taskRepo;
private readonly ListRepository _listRepo;
private readonly TagRepository _tagRepo;
private readonly WorkerConfig _cfg;
private readonly string _tempDir;
@@ -27,7 +26,6 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
_ctx = _db.CreateContext();
_taskRepo = new TaskRepository(_ctx);
_listRepo = new ListRepository(_ctx);
_tagRepo = new TagRepository(_ctx);
_tempDir = Path.Combine(Path.GetTempPath(), $"claudedo_slotguard_{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_cfg = new WorkerConfig
@@ -68,9 +66,6 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
{
var listId = Guid.NewGuid().ToString();
await _listRepo.AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow });
var tags = await _tagRepo.GetAllAsync();
var agentTag = tags.First(t => t.Name == "agent");
await _listRepo.AddTagAsync(listId, agentTag.Id);
return listId;
}

View File

@@ -19,7 +19,6 @@ public sealed class QueueServiceTests : IDisposable
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _taskRepo;
private readonly ListRepository _listRepo;
private readonly TagRepository _tagRepo;
private readonly WorkerConfig _cfg;
private readonly string _tempDir;
@@ -28,7 +27,6 @@ public sealed class QueueServiceTests : IDisposable
_ctx = _db.CreateContext();
_taskRepo = new TaskRepository(_ctx);
_listRepo = new ListRepository(_ctx);
_tagRepo = new TagRepository(_ctx);
_tempDir = Path.Combine(Path.GetTempPath(), $"claudedo_test_{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_cfg = new WorkerConfig
@@ -69,11 +67,7 @@ public sealed class QueueServiceTests : IDisposable
{
var listId = Guid.NewGuid().ToString();
await _listRepo.AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow });
var tags = await _tagRepo.GetAllAsync();
var agentTag = tags.First(t => t.Name == "agent");
await _listRepo.AddTagAsync(listId, agentTag.Id);
return (listId, agentTag.Id);
return (listId, 0L);
}
private async Task<TaskEntity> SeedQueuedTask(string listId, DateTime? scheduledFor = null, DateTime? createdAt = null)

View File

@@ -40,8 +40,6 @@ sealed class FakeWorkerClient : IWorkerClient
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
public Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames) => Task.CompletedTask;
public Task<List<string>> GetAllTagsAsync() => Task.FromResult(new List<string>());
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;