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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () =>
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user