From 623ebf147b7e9f44cd09534f9ad0865f12815f3e Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 19 May 2026 08:07:24 +0200 Subject: [PATCH] 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) --- src/ClaudeDo.Data/ClaudeDoDbContext.cs | 1 - .../Configuration/ListEntityConfiguration.cs | 11 - .../Configuration/TagEntityConfiguration.cs | 22 -- .../Configuration/TaskEntityConfiguration.cs | 11 - .../Migrations/20260519044715_RemoveTags.cs | 115 +++++++++++ .../ClaudeDoDbContextModelSnapshot.cs | 92 --------- src/ClaudeDo.Data/Models/ListEntity.cs | 1 - src/ClaudeDo.Data/Models/TagEntity.cs | 11 - src/ClaudeDo.Data/Models/TaskEntity.cs | 1 - .../Repositories/ListRepository.cs | 32 --- .../Repositories/TagRepository.cs | 28 --- .../Repositories/TaskRepository.cs | 103 +--------- src/ClaudeDo.Ui/Services/IWorkerClient.cs | 2 - src/ClaudeDo.Ui/Services/WorkerClient.cs | 17 -- .../Islands/DetailsIslandViewModel.cs | 194 ++++++------------ .../ViewModels/Islands/TaskRowViewModel.cs | 25 +-- .../Islands/TasksIslandViewModel.cs | 49 +---- .../Views/Islands/AgentStripView.axaml | 31 ++- .../Views/Islands/DetailsIslandView.axaml | 113 +++------- .../Views/Islands/TaskRowView.axaml | 40 ++-- .../Views/Islands/TaskRowView.axaml.cs | 37 +--- src/ClaudeDo.Ui/Views/MainWindow.axaml | 8 +- .../External/ExternalMcpService.cs | 41 +--- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 35 ---- .../Planning/PlanningChainCoordinator.cs | 14 -- .../Planning/PlanningMcpService.cs | 23 +-- src/ClaudeDo.Worker/Program.cs | 1 - src/ClaudeDo.Worker/Runner/TaskRunner.cs | 7 +- .../External/ExternalMcpServiceTests.cs | 111 +--------- .../Hub/PlanningHubTests.cs | 8 +- .../Planning/PlanningChainCoordinatorTests.cs | 12 -- .../Planning/PlanningEndToEndTests.cs | 10 +- .../Planning/PlanningMcpServiceTests.cs | 73 ++----- .../Planning/PlanningSessionManagerTests.cs | 12 +- .../Queue/QueuePickerTests.cs | 12 -- .../Repositories/ListRepositoryTests.cs | 18 -- .../TaskRepositoryOrphanGuardTests.cs | 20 +- .../TaskRepositoryPlanningTests.cs | 14 +- .../Repositories/TaskRepositoryTests.cs | 81 -------- .../Services/QueueServiceSlotGuardTests.cs | 5 - .../Services/QueueServiceTests.cs | 8 +- .../UiVm/TasksIslandViewModelPlanningTests.cs | 2 - 42 files changed, 333 insertions(+), 1118 deletions(-) delete mode 100644 src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Migrations/20260519044715_RemoveTags.cs delete mode 100644 src/ClaudeDo.Data/Models/TagEntity.cs delete mode 100644 src/ClaudeDo.Data/Repositories/TagRepository.cs diff --git a/src/ClaudeDo.Data/ClaudeDoDbContext.cs b/src/ClaudeDo.Data/ClaudeDoDbContext.cs index c96a7a7..a79dda4 100644 --- a/src/ClaudeDo.Data/ClaudeDoDbContext.cs +++ b/src/ClaudeDo.Data/ClaudeDoDbContext.cs @@ -12,7 +12,6 @@ public class ClaudeDoDbContext : DbContext public DbSet Tasks => Set(); public DbSet Lists => Set(); - public DbSet Tags => Set(); public DbSet ListConfigs => Set(); public DbSet Worktrees => Set(); public DbSet TaskRuns => Set(); diff --git a/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs index 17e5ebc..2c77187 100644 --- a/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs @@ -21,16 +21,5 @@ public class ListEntityConfiguration : IEntityTypeConfiguration .WithOne(c => c.List) .HasForeignKey(c => c.ListId) .OnDelete(DeleteBehavior.Cascade); - - builder.HasMany(l => l.Tags) - .WithMany(tag => tag.Lists) - .UsingEntity("list_tags", - l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade), - r => r.HasOne(typeof(ListEntity)).WithMany().HasForeignKey("list_id").OnDelete(DeleteBehavior.Cascade), - j => - { - j.HasKey("list_id", "tag_id"); - j.ToTable("list_tags"); - }); } } diff --git a/src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs deleted file mode 100644 index 066b0ec..0000000 --- a/src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ClaudeDo.Data.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace ClaudeDo.Data.Configuration; - -public class TagEntityConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("tags"); - - builder.HasKey(t => t.Id); - builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedOnAdd(); - builder.Property(t => t.Name).HasColumnName("name").IsRequired(); - builder.HasIndex(t => t.Name).IsUnique(); - - builder.HasData( - new TagEntity { Id = 1, Name = "agent" }, - new TagEntity { Id = 2, Name = "manual" }); - } -} diff --git a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs index f2a2913..d7d5d85 100644 --- a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs +++ b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs @@ -112,17 +112,6 @@ public class TaskEntityConfiguration : IEntityTypeConfiguration .WithOne(w => w.Task) .HasForeignKey(w => w.TaskId); - builder.HasMany(t => t.Tags) - .WithMany(tag => tag.Tasks) - .UsingEntity("task_tags", - l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade), - r => r.HasOne(typeof(TaskEntity)).WithMany().HasForeignKey("task_id").OnDelete(DeleteBehavior.Cascade), - j => - { - j.HasKey("task_id", "tag_id"); - j.ToTable("task_tags"); - }); - builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id"); builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status"); builder.HasIndex(t => new { t.ListId, t.SortOrder }).HasDatabaseName("idx_tasks_list_sort"); diff --git a/src/ClaudeDo.Data/Migrations/20260519044715_RemoveTags.cs b/src/ClaudeDo.Data/Migrations/20260519044715_RemoveTags.cs new file mode 100644 index 0000000..c220206 --- /dev/null +++ b/src/ClaudeDo.Data/Migrations/20260519044715_RemoveTags.cs @@ -0,0 +1,115 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace ClaudeDo.Data.Migrations +{ + /// + public partial class RemoveTags : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "list_tags"); + + migrationBuilder.DropTable( + name: "task_tags"); + + migrationBuilder.DropTable( + name: "tags"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "tags", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_tags", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "list_tags", + columns: table => new + { + list_id = table.Column(type: "TEXT", nullable: false), + tag_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_list_tags", x => new { x.list_id, x.tag_id }); + table.ForeignKey( + name: "FK_list_tags_lists_list_id", + column: x => x.list_id, + principalTable: "lists", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_list_tags_tags_tag_id", + column: x => x.tag_id, + principalTable: "tags", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "task_tags", + columns: table => new + { + task_id = table.Column(type: "TEXT", nullable: false), + tag_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_task_tags", x => new { x.task_id, x.tag_id }); + table.ForeignKey( + name: "FK_task_tags_tags_tag_id", + column: x => x.tag_id, + principalTable: "tags", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_task_tags_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "tags", + columns: new[] { "id", "name" }, + values: new object[,] + { + { 1L, "agent" }, + { 2L, "manual" } + }); + + migrationBuilder.CreateIndex( + name: "IX_list_tags_tag_id", + table: "list_tags", + column: "tag_id"); + + migrationBuilder.CreateIndex( + name: "IX_tags_name", + table: "tags", + column: "name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_task_tags_tag_id", + table: "task_tags", + column: "tag_id"); + } + } +} diff --git a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs index a98345b..333d8c5 100644 --- a/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs +++ b/src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs @@ -230,38 +230,6 @@ namespace ClaudeDo.Data.Migrations b.ToTable("subtasks", (string)null); }); - modelBuilder.Entity("ClaudeDo.Data.Models.TagEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER") - .HasColumnName("id"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("name"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("tags", (string)null); - - b.HasData( - new - { - Id = 1L, - Name = "agent" - }, - new - { - Id = 2L, - Name = "manual" - }); - }); - modelBuilder.Entity("ClaudeDo.Data.Models.TaskEntity", b => { b.Property("Id") @@ -526,36 +494,6 @@ namespace ClaudeDo.Data.Migrations b.ToTable("worktrees", (string)null); }); - modelBuilder.Entity("list_tags", b => - { - b.Property("list_id") - .HasColumnType("TEXT"); - - b.Property("tag_id") - .HasColumnType("INTEGER"); - - b.HasKey("list_id", "tag_id"); - - b.HasIndex("tag_id"); - - b.ToTable("list_tags", (string)null); - }); - - modelBuilder.Entity("task_tags", b => - { - b.Property("task_id") - .HasColumnType("TEXT"); - - b.Property("tag_id") - .HasColumnType("INTEGER"); - - b.HasKey("task_id", "tag_id"); - - b.HasIndex("tag_id"); - - b.ToTable("task_tags", (string)null); - }); - modelBuilder.Entity("ClaudeDo.Data.Models.ListConfigEntity", b => { b.HasOne("ClaudeDo.Data.Models.ListEntity", "List") @@ -623,36 +561,6 @@ namespace ClaudeDo.Data.Migrations b.Navigation("Task"); }); - modelBuilder.Entity("list_tags", b => - { - b.HasOne("ClaudeDo.Data.Models.ListEntity", null) - .WithMany() - .HasForeignKey("list_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ClaudeDo.Data.Models.TagEntity", null) - .WithMany() - .HasForeignKey("tag_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("task_tags", b => - { - b.HasOne("ClaudeDo.Data.Models.TagEntity", null) - .WithMany() - .HasForeignKey("tag_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("ClaudeDo.Data.Models.TaskEntity", null) - .WithMany() - .HasForeignKey("task_id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - modelBuilder.Entity("ClaudeDo.Data.Models.ListEntity", b => { b.Navigation("Config"); diff --git a/src/ClaudeDo.Data/Models/ListEntity.cs b/src/ClaudeDo.Data/Models/ListEntity.cs index 020494a..075693a 100644 --- a/src/ClaudeDo.Data/Models/ListEntity.cs +++ b/src/ClaudeDo.Data/Models/ListEntity.cs @@ -11,5 +11,4 @@ public sealed class ListEntity // Navigation properties public ListConfigEntity? Config { get; set; } public ICollection Tasks { get; set; } = new List(); - public ICollection Tags { get; set; } = new List(); } diff --git a/src/ClaudeDo.Data/Models/TagEntity.cs b/src/ClaudeDo.Data/Models/TagEntity.cs deleted file mode 100644 index 626684a..0000000 --- a/src/ClaudeDo.Data/Models/TagEntity.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace ClaudeDo.Data.Models; - -public sealed class TagEntity -{ - public long Id { get; init; } - public required string Name { get; set; } - - // Navigation properties - public ICollection Lists { get; set; } = new List(); - public ICollection Tasks { get; set; } = new List(); -} diff --git a/src/ClaudeDo.Data/Models/TaskEntity.cs b/src/ClaudeDo.Data/Models/TaskEntity.cs index 96b39fe..e637cbb 100644 --- a/src/ClaudeDo.Data/Models/TaskEntity.cs +++ b/src/ClaudeDo.Data/Models/TaskEntity.cs @@ -51,7 +51,6 @@ public sealed class TaskEntity // Navigation properties public ListEntity List { get; set; } = null!; public WorktreeEntity? Worktree { get; set; } - public ICollection Tags { get; set; } = new List(); public ICollection Runs { get; set; } = new List(); public ICollection Subtasks { get; set; } = new List(); diff --git a/src/ClaudeDo.Data/Repositories/ListRepository.cs b/src/ClaudeDo.Data/Repositories/ListRepository.cs index d595f44..ab968f0 100644 --- a/src/ClaudeDo.Data/Repositories/ListRepository.cs +++ b/src/ClaudeDo.Data/Repositories/ListRepository.cs @@ -36,38 +36,6 @@ public sealed class ListRepository return await _context.Lists.OrderBy(l => l.CreatedAt).ToListAsync(ct); } - public async Task> GetTagsAsync(string listId, CancellationToken ct = default) - { - return await _context.Lists - .Where(l => l.Id == listId) - .SelectMany(l => l.Tags) - .ToListAsync(ct); - } - - public async Task AddTagAsync(string listId, long tagId, CancellationToken ct = default) - { - var list = await _context.Lists.Include(l => l.Tags).FirstOrDefaultAsync(l => l.Id == listId, ct); - if (list is null) return; - var tag = await _context.Tags.FindAsync([tagId], ct); - if (tag is not null && !list.Tags.Any(t => t.Id == tagId)) - { - list.Tags.Add(tag); - await _context.SaveChangesAsync(ct); - } - } - - public async Task RemoveTagAsync(string listId, long tagId, CancellationToken ct = default) - { - var list = await _context.Lists.Include(l => l.Tags).FirstOrDefaultAsync(l => l.Id == listId, ct); - if (list is null) return; - var tag = list.Tags.FirstOrDefault(t => t.Id == tagId); - if (tag is not null) - { - list.Tags.Remove(tag); - await _context.SaveChangesAsync(ct); - } - } - public async Task GetConfigAsync(string listId, CancellationToken ct = default) { return await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == listId, ct); diff --git a/src/ClaudeDo.Data/Repositories/TagRepository.cs b/src/ClaudeDo.Data/Repositories/TagRepository.cs deleted file mode 100644 index 647a927..0000000 --- a/src/ClaudeDo.Data/Repositories/TagRepository.cs +++ /dev/null @@ -1,28 +0,0 @@ -using ClaudeDo.Data.Models; -using Microsoft.EntityFrameworkCore; - -namespace ClaudeDo.Data.Repositories; - -public sealed class TagRepository -{ - private readonly ClaudeDoDbContext _context; - - public TagRepository(ClaudeDoDbContext context) => _context = context; - - public async Task> GetAllAsync(CancellationToken ct = default) - { - return await _context.Tags.OrderBy(t => t.Id).ToListAsync(ct); - } - - public async Task GetOrCreateAsync(string name, CancellationToken ct = default) - { - var existing = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct); - if (existing is not null) - return existing.Id; - - var tag = new TagEntity { Name = name }; - _context.Tags.Add(tag); - await _context.SaveChangesAsync(ct); - return tag.Id; - } -} diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index a05f1f1..8ef880d 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -171,74 +171,6 @@ public sealed class TaskRepository #endregion - #region Tags - - public async Task AddTagAsync(string taskId, long tagId, CancellationToken ct = default) - { - var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct); - if (task is null) return; - var tag = await _context.Tags.FindAsync([tagId], ct); - if (tag is not null && !task.Tags.Any(t => t.Id == tagId)) - { - task.Tags.Add(tag); - await _context.SaveChangesAsync(ct); - } - } - - public async Task RemoveTagAsync(string taskId, long tagId, CancellationToken ct = default) - { - var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct); - if (task is null) return; - var tag = task.Tags.FirstOrDefault(t => t.Id == tagId); - if (tag is not null) - { - task.Tags.Remove(tag); - await _context.SaveChangesAsync(ct); - } - } - - public async Task SetTagsAsync(string taskId, IReadOnlyList tagNames, CancellationToken ct = default) - { - var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct); - if (task is null) return; - - task.Tags.Clear(); - - foreach (var name in tagNames.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase)) - { - var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct); - if (tag is null) - { - tag = new TagEntity { Name = name }; - _context.Tags.Add(tag); - } - task.Tags.Add(tag); - } - - await _context.SaveChangesAsync(ct); - } - - public async Task> GetTagsAsync(string taskId, CancellationToken ct = default) - { - return await _context.Tasks - .Where(t => t.Id == taskId) - .SelectMany(t => t.Tags) - .ToListAsync(ct); - } - - public async Task> GetEffectiveTagsAsync(string taskId, CancellationToken ct = default) - { - var taskTags = _context.Tasks - .Where(t => t.Id == taskId) - .SelectMany(t => t.Tags); - var listTags = _context.Tasks - .Where(t => t.Id == taskId) - .SelectMany(t => t.List.Tags); - return await taskTags.Union(listTags).Distinct().ToListAsync(ct); - } - - #endregion - #region Planning public async Task> GetChildrenAsync(string parentId, CancellationToken ct = default) @@ -254,7 +186,6 @@ public sealed class TaskRepository string parentId, string title, string? description, - IReadOnlyList? tagNames, string? commitType, CancellationToken ct = default) { @@ -286,22 +217,6 @@ public sealed class TaskRepository SortOrder = (maxSort ?? -1) + 1, }; _context.Tasks.Add(child); - - if (tagNames is not null && tagNames.Count > 0) - { - foreach (var tagName in tagNames.Distinct(StringComparer.OrdinalIgnoreCase)) - { - var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == tagName, ct); - if (tag is null) - { - tag = new TagEntity { Name = tagName }; - _context.Tags.Add(tag); - await _context.SaveChangesAsync(ct); - } - child.Tags.Add(tag); - } - } - await _context.SaveChangesAsync(ct); return child; } @@ -311,11 +226,10 @@ public sealed class TaskRepository string? title, string? description, string? commitType, - IReadOnlyList? tagNames, TaskStatus? status, CancellationToken ct = default) { - var task = await _context.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId, ct) + var task = await _context.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, ct) ?? throw new InvalidOperationException($"Task {taskId} not found."); if (title is not null) task.Title = title; @@ -323,21 +237,6 @@ public sealed class TaskRepository if (commitType is not null) task.CommitType = commitType; if (status.HasValue) task.Status = status.Value; - if (tagNames is not null) - { - task.Tags.Clear(); - foreach (var name in tagNames.Where(n => !string.IsNullOrWhiteSpace(n)).Distinct(StringComparer.OrdinalIgnoreCase)) - { - var tag = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct); - if (tag is null) - { - tag = new TagEntity { Name = name }; - _context.Tags.Add(tag); - } - task.Tags.Add(tag); - } - } - await _context.SaveChangesAsync(ct); } diff --git a/src/ClaudeDo.Ui/Services/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/IWorkerClient.cs index 19b44e9..cc4950f 100644 --- a/src/ClaudeDo.Ui/Services/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/IWorkerClient.cs @@ -32,8 +32,6 @@ public interface IWorkerClient : INotifyPropertyChanged Task GetListConfigAsync(string listId); Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto); Task SetTaskStatusAsync(string taskId, TaskStatus status); - Task SetTaskTagsAsync(string taskId, IEnumerable tagNames); - Task> GetAllTagsAsync(); Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default); Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default); Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 252151d..6332b23 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -395,23 +395,6 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString()); } - public async Task SetTaskTagsAsync(string taskId, IEnumerable tagNames) - { - await _hub.InvokeAsync("SetTaskTags", taskId, tagNames.ToArray()); - } - - public async Task> GetAllTagsAsync() - { - try - { - return await _hub.InvokeAsync>("GetAllTags") ?? new List(); - } - catch - { - return new List(); - } - } - public async Task CleanupFinishedWorktreesAsync() { try diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 935d897..16f17f1 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -21,7 +21,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Current task row (set by IslandsShellViewModel via Bind) [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(RunNowCommand))] + [NotifyCanExecuteChangedFor(nameof(EnqueueCommand))] + [NotifyCanExecuteChangedFor(nameof(DequeueCommand))] + [NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))] private TaskRowViewModel? _task; // Editable fields @@ -56,74 +58,23 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Short task-id badge, e.g. "#T1A" public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : ""; - // Agent strip fields - // Status editor (Details panel) — set freely; broadcast refreshes other panes. - public System.Collections.ObjectModel.ObservableCollection StatusOptions { get; } = new() - { - ClaudeDo.Data.Models.TaskStatus.Idle, - ClaudeDo.Data.Models.TaskStatus.Queued, - ClaudeDo.Data.Models.TaskStatus.Running, - ClaudeDo.Data.Models.TaskStatus.Done, - ClaudeDo.Data.Models.TaskStatus.Failed, - ClaudeDo.Data.Models.TaskStatus.Cancelled, - }; - - private bool _suppressStatusSave; - [ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _selectedStatus; - - partial void OnSelectedStatusChanged(ClaudeDo.Data.Models.TaskStatus value) - { - if (_suppressStatusSave || Task is null) return; - _ = SaveStatusAsync(value); - } - - private async System.Threading.Tasks.Task SaveStatusAsync(ClaudeDo.Data.Models.TaskStatus value) - { - if (Task is null) return; - try { await _worker.SetTaskStatusAsync(Task.Id, value); } - catch { /* offline */ } - } - - // Tag editor - public ObservableCollection Tags { get; } = new(); - public ObservableCollection AvailableTags { get; } = new(); - [ObservableProperty] private string _newTagInput = ""; - - [RelayCommand] - private async System.Threading.Tasks.Task AddTagAsync() - { - if (Task is null) return; - var name = NewTagInput?.Trim().ToLowerInvariant(); - NewTagInput = ""; - if (string.IsNullOrEmpty(name)) return; - if (Tags.Contains(name)) return; - var next = Tags.ToList(); - next.Add(name); - try { await _worker.SetTaskTagsAsync(Task.Id, next); } - catch { /* offline */ } - } - - [RelayCommand] - private async System.Threading.Tasks.Task RemoveTagAsync(string? tagName) - { - if (Task is null || string.IsNullOrWhiteSpace(tagName)) return; - if (!Tags.Contains(tagName)) return; - var next = Tags.Where(t => t != tagName).ToList(); - try { await _worker.SetTaskTagsAsync(Task.Id, next); } - catch { /* offline */ } - } - - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(RunNowCommand))] - private string _agentStatusLabel = "Idle"; - public bool IsRunning => AgentStatusLabel == "Running"; - public bool IsDone => AgentStatusLabel == "Done"; - public bool IsFailed => AgentStatusLabel == "Failed"; - [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(EnqueueCommand))] + [NotifyCanExecuteChangedFor(nameof(DequeueCommand))] + [NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] - [NotifyCanExecuteChangedFor(nameof(ResetCommand))] - private bool _showFailedActions; + private string _agentStatusLabel = "Idle"; + public bool IsIdle => AgentStatusLabel == "Idle"; + public bool IsQueued => AgentStatusLabel == "Queued"; + public bool IsRunning => AgentStatusLabel == "Running"; + public bool IsDone => AgentStatusLabel == "Done"; + public bool IsFailed => AgentStatusLabel == "Failed"; + public bool IsCancelled => AgentStatusLabel == "Cancelled"; + + // Recovery actions: Continue (resume session) for Failed/Cancelled. + public bool ShowContinue => IsFailed || IsCancelled; + // Reset & retry available from any terminal state. + public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ContinueCommand))] @@ -131,11 +82,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase partial void OnAgentStatusLabelChanged(string value) { + OnPropertyChanged(nameof(IsIdle)); + OnPropertyChanged(nameof(IsQueued)); OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsDone)); OnPropertyChanged(nameof(IsFailed)); + OnPropertyChanged(nameof(IsCancelled)); + OnPropertyChanged(nameof(ShowContinue)); + OnPropertyChanged(nameof(ShowResetAndRetry)); OnPropertyChanged(nameof(IsAgentSectionEnabled)); - ShowFailedActions = value == "Failed"; } [ObservableProperty] private string? _model; @@ -237,40 +192,17 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase // Set by the view so DeleteTaskCommand can show an error message public Func? ShowErrorAsync { get; set; } - private void ApplyTagsFromEntity(ClaudeDo.Data.Models.TaskEntity entity) - { - Tags.Clear(); - foreach (var t in entity.Tags) Tags.Add(t.Name); - } - - private async System.Threading.Tasks.Task RefreshAvailableTagsAsync() - { - try - { - var all = await _worker.GetAllTagsAsync(); - AvailableTags.Clear(); - foreach (var t in all) AvailableTags.Add(t); - } - catch { } - } - - private async System.Threading.Tasks.Task RefreshTagsAndStatusAsync(string taskId) + private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId) { try { await using var ctx = await _dbFactory.CreateDbContextAsync(); var entity = await ctx.Tasks .AsNoTracking() - .Include(t => t.Tags) .FirstOrDefaultAsync(t => t.Id == taskId); if (entity is null || Task?.Id != taskId) return; - _suppressStatusSave = true; - try { SelectedStatus = entity.Status; } - finally { _suppressStatusSave = false; } AgentStatusLabel = entity.Status.ToString(); - ApplyTagsFromEntity(entity); - await RefreshAvailableTagsAsync(); } catch { } } @@ -289,9 +221,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase { if (e.PropertyName == nameof(WorkerClient.IsConnected)) { - RunNowCommand.NotifyCanExecuteChanged(); + EnqueueCommand.NotifyCanExecuteChanged(); + DequeueCommand.NotifyCanExecuteChanged(); + ResetAndRetryCommand.NotifyCanExecuteChanged(); ContinueCommand.NotifyCanExecuteChanged(); - ResetCommand.NotifyCanExecuteChanged(); ApproveMergeCommand.NotifyCanExecuteChanged(); } }; @@ -323,7 +256,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase _worker.TaskUpdatedEvent += taskId => { - if (Task?.Id == taskId) _ = RefreshTagsAndStatusAsync(taskId); + if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId); if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId); }; @@ -503,13 +436,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase BranchLine = null; AgentStatusLabel = "Idle"; LatestRunSessionId = null; - ShowFailedActions = false; - Tags.Clear(); - AvailableTags.Clear(); - NewTagInput = ""; - _suppressStatusSave = true; - try { SelectedStatus = ClaudeDo.Data.Models.TaskStatus.Idle; } - finally { _suppressStatusSave = false; } _suppressAgentSave = true; try { @@ -537,11 +463,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase await using var ctx = await _dbFactory.CreateDbContextAsync(ct); var subtaskRepo = new SubtaskRepository(ctx); - // Own query with Include so WorktreePath/BranchLine/Tags are populated. + // Own query with Include so WorktreePath/BranchLine are populated. var entity = await ctx.Tasks .AsNoTracking() .Include(t => t.Worktree) - .Include(t => t.Tags) .FirstOrDefaultAsync(t => t.Id == row.Id, ct); ct.ThrowIfCancellationRequested(); if (entity == null) return; @@ -557,11 +482,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase WorktreeStateLabel = entity.Worktree?.State.ToString(); BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; AgentStatusLabel = entity.Status.ToString(); - _suppressStatusSave = true; - try { SelectedStatus = entity.Status; } - finally { _suppressStatusSave = false; } - ApplyTagsFromEntity(entity); - await RefreshAvailableTagsAsync(); await LoadAgentSettingsAsync(entity, ct); ct.ThrowIfCancellationRequested(); @@ -926,24 +846,35 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase await _worker.CancelTaskAsync(Task.Id); } - [RelayCommand(CanExecute = nameof(CanRunNow))] - private async System.Threading.Tasks.Task RunNowAsync() + [RelayCommand(CanExecute = nameof(CanEnqueue))] + private async System.Threading.Tasks.Task EnqueueAsync() { if (Task == null) return; - AgentStatusLabel = "Running"; try { - await _worker.RunNowAsync(Task.Id); - } - catch - { - AgentStatusLabel = "Failed"; - throw; + await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued); + AgentStatusLabel = "Queued"; } + catch { /* offline */ } } - private bool CanRunNow() => - Task != null && _worker.IsConnected && !IsRunning; + private bool CanEnqueue() => + Task != null && _worker.IsConnected && IsIdle; + + [RelayCommand(CanExecute = nameof(CanDequeue))] + private async System.Threading.Tasks.Task DequeueAsync() + { + if (Task == null) return; + try + { + await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle); + AgentStatusLabel = "Idle"; + } + catch { /* offline */ } + } + + private bool CanDequeue() => + Task != null && _worker.IsConnected && IsQueued; [RelayCommand(CanExecute = nameof(CanContinue))] private async System.Threading.Tasks.Task ContinueAsync() @@ -953,23 +884,32 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase } private bool CanContinue() => - Task != null && _worker.IsConnected && ShowFailedActions && !string.IsNullOrEmpty(LatestRunSessionId); + Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId); - [RelayCommand(CanExecute = nameof(CanReset))] - private async System.Threading.Tasks.Task ResetAsync() + [RelayCommand(CanExecute = nameof(CanResetAndRetry))] + private async System.Threading.Tasks.Task ResetAndRetryAsync() { if (Task == null) return; if (ConfirmAsync == null) return; var branchName = $"claudedo/{Task.Id.Replace("-", "")}"; - var ok = await ConfirmAsync($"Discard worktree and reset task?\nThis deletes branch {branchName} and all uncommitted changes."); + var ok = await ConfirmAsync( + $"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning."); if (!ok) return; - await _worker.ResetTaskAsync(Task.Id); + if (WorktreePath != null) + await _worker.ResetTaskAsync(Task.Id); + + try + { + await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued); + AgentStatusLabel = "Queued"; + } + catch { /* offline */ } } - private bool CanReset() => - Task != null && _worker.IsConnected && ShowFailedActions; + private bool CanResetAndRetry() => + Task != null && _worker.IsConnected && ShowResetAndRetry; } public sealed partial class SubtaskRowViewModel : ViewModelBase diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs index b6c9d45..054c36a 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using ClaudeDo.Data.Models; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; @@ -8,11 +6,6 @@ namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class TaskRowViewModel : ViewModelBase { - public TaskRowViewModel() - { - Tags.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasTags)); - } - public required string Id { get; init; } [ObservableProperty] private string _title = ""; [ObservableProperty] private string _listName = ""; @@ -39,7 +32,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase public DateTime CreatedAt { get; init; } public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}"; - public ObservableCollection Tags { get; } = new(); public int StepsCount { get; init; } public int StepsCompleted { get; init; } @@ -62,13 +54,13 @@ public sealed partial class TaskRowViewModel : ViewModelBase public bool HasBranch => !string.IsNullOrWhiteSpace(Branch); public bool HasDiff => DiffAdditions > 0 || DiffDeletions > 0; - public bool HasTags => Tags.Count > 0; public bool HasSteps => StepsCount > 0; public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done; public bool IsRunning => Status == TaskStatus.Running; public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId); public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId); public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks; + public bool CanSendToQueue => !IsRunning && !IsQueued && !HasQueuedSubtasks; public bool HasSchedule => ScheduledFor.HasValue; public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail); @@ -96,6 +88,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase OnPropertyChanged(nameof(IsDraft)); OnPropertyChanged(nameof(CanOpenPlanningSession)); OnPropertyChanged(nameof(CanRemoveFromQueue)); + OnPropertyChanged(nameof(CanSendToQueue)); } partial void OnPlanningPhaseChanged(PlanningPhase value) @@ -107,7 +100,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase } partial void OnHasQueuedSubtasksChanged(bool value) - => OnPropertyChanged(nameof(CanRemoveFromQueue)); + { + OnPropertyChanged(nameof(CanRemoveFromQueue)); + OnPropertyChanged(nameof(CanSendToQueue)); + } partial void OnBlockedByTaskIdChanged(string? value) { @@ -160,15 +156,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase DiffDeletions = del; ParentTaskId = t.ParentTaskId; BlockedByTaskId = t.BlockedByTaskId; - SetTags(t.Tags.Select(tag => tag.Name)); - } - - public void SetTags(IEnumerable names) - { - var snapshot = names.ToList(); - if (Tags.SequenceEqual(snapshot)) return; - Tags.Clear(); - foreach (var n in snapshot) Tags.Add(n); } // Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions". diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 5c99a1a..59d404d 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -31,7 +31,6 @@ public sealed partial class TasksIslandViewModel : ViewModelBase public ObservableCollection OverdueItems { get; } = new(); public ObservableCollection OpenItems { get; } = new(); public ObservableCollection CompletedItems { get; } = new(); - public ObservableCollection AllTags { get; } = new(); [ObservableProperty] private string _newTaskTitle = ""; [ObservableProperty] private TaskRowViewModel? _selectedTask; @@ -59,22 +58,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase _worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated; _worker.TaskMessageEvent += OnWorkerTaskMessage; _worker.ConnectionRestoredEvent += () => LoadForList(_currentList); - _ = RefreshAllTagsAsync(); } } - private async Task RefreshAllTagsAsync() - { - if (_worker is null) return; - try - { - var tags = await _worker.GetAllTagsAsync(); - AllTags.Clear(); - foreach (var t in tags) AllTags.Add(t); - } - catch { /* offline */ } - } - private void OnWorkerTaskMessage(string taskId, string line) { var row = Items.FirstOrDefault(r => r.Id == taskId); @@ -101,7 +87,6 @@ public sealed partial class TasksIslandViewModel : ViewModelBase var entity = await db.Tasks .Include(t => t.List) .Include(t => t.Worktree) - .Include(t => t.Tags) .FirstOrDefaultAsync(t => t.Id == taskId); var existing = Items.FirstOrDefault(r => r.Id == taskId); @@ -190,7 +175,6 @@ public sealed partial class TasksIslandViewModel : ViewModelBase var all = await db.Tasks .Include(t => t.List) .Include(t => t.Worktree) - .Include(t => t.Tags) .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt) .ToListAsync(ct); @@ -484,37 +468,14 @@ public sealed partial class TasksIslandViewModel : ViewModelBase catch { /* offline; broadcast won't fire */ } } - public async Task ToggleTagOnRowAsync(TaskRowViewModel row, string tagName) - { - if (_worker is null) return; - var name = tagName.Trim().ToLowerInvariant(); - if (name.Length == 0) return; - var current = row.Tags.ToList(); - var next = current.Contains(name) - ? current.Where(t => t != name).ToList() - : current.Append(name).ToList(); - try - { - await _worker.SetTaskTagsAsync(row.Id, next); - await RefreshAllTagsAsync(); - } - catch { } - } - [RelayCommand] private async Task SendToQueueAsync(TaskRowViewModel? row) { if (row is null || row.IsRunning) return; await using var db = await _dbFactory.CreateDbContextAsync(); - var entity = await db.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == row.Id); + var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id); if (entity is null) return; entity.Status = TaskStatus.Queued; - // Worker queue picker requires the "agent" tag — attach it on explicit enqueue. - if (!entity.Tags.Any(t => t.Name == "agent")) - { - var agentTag = await db.Tags.FirstOrDefaultAsync(t => t.Name == "agent"); - if (agentTag is not null) entity.Tags.Add(agentTag); - } await db.SaveChangesAsync(); row.Status = TaskStatus.Queued; if (_worker is not null) @@ -568,6 +529,14 @@ public sealed partial class TasksIslandViewModel : ViewModelBase TasksChanged?.Invoke(this, EventArgs.Empty); } + [RelayCommand] + private async Task CancelRunningTaskAsync(TaskRowViewModel? row) + { + if (row is null || !row.IsRunning || _worker is null) return; + try { await _worker.CancelTaskAsync(row.Id); } + catch { /* worker offline; the broadcast will reconcile when it returns */ } + } + public async Task SetScheduledForAsync(TaskRowViewModel row, DateTime? when) { if (row is null) return; diff --git a/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml b/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml index 3a03dc1..8951e3c 100644 --- a/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml @@ -42,13 +42,22 @@ - + - - - - @@ -147,46 +122,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + - - @@ -99,16 +97,19 @@ - - + - + @@ -116,7 +117,7 @@ - + @@ -167,21 +168,6 @@ - - - - - - - - - - - - - - - diff --git a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs index 1efae2b..3cc2024 100644 --- a/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs @@ -36,6 +36,12 @@ public partial class TaskRowView : UserControl await vm.RemoveFromQueueCommand.ExecuteAsync(row); } + private async void OnCancelExecutionClick(object? sender, RoutedEventArgs e) + { + if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm) + await vm.CancelRunningTaskCommand.ExecuteAsync(row); + } + private async void OnClearScheduleClick(object? sender, RoutedEventArgs e) { if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm) @@ -82,37 +88,6 @@ public partial class TaskRowView : UserControl await vm.SetStatusOnRowAsync(row, status); } - private void OnContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e) - { - if (DataContext is not TaskRowViewModel row || FindTasksVm() is not { } vm) return; - - // Build the union of all known tags + tags currently on this row, so a row's - // own tags stay reachable from the menu even if the global list is stale. - var rowTags = row.Tags.ToHashSet(); - var union = vm.AllTags.Concat(rowTags).Distinct().OrderBy(t => t).ToList(); - - TagsMenu.Items.Clear(); - if (union.Count == 0) - { - TagsMenu.Items.Add(new MenuItem { Header = "(no tags yet)", IsEnabled = false }); - return; - } - foreach (var name in union) - { - var prefix = rowTags.Contains(name) ? "✓ " : " "; - var item = new MenuItem { Header = prefix + name, Tag = name }; - item.Click += OnToggleTagClick; - TagsMenu.Items.Add(item); - } - } - - private async void OnToggleTagClick(object? sender, RoutedEventArgs e) - { - if (sender is not MenuItem mi || mi.Tag is not string name) return; - if (DataContext is not TaskRowViewModel row || FindTasksVm() is not { } vm) return; - await vm.ToggleTagOnRowAsync(row, name); - } - private void OnScheduleForClick(object? sender, RoutedEventArgs e) { if (DataContext is not TaskRowViewModel row) return; diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml index 5158462..4e599bc 100644 --- a/src/ClaudeDo.Ui/Views/MainWindow.axaml +++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml @@ -151,10 +151,10 @@ - + - + @@ -166,7 +166,7 @@ ResizeDirection="Columns" ResizeBehavior="PreviousAndNext"/> - + @@ -179,7 +179,7 @@ ResizeBehavior="PreviousAndNext" IsVisible="{Binding ShowDetails}"/> - diff --git a/src/ClaudeDo.Worker/External/ExternalMcpService.cs b/src/ClaudeDo.Worker/External/ExternalMcpService.cs index 720f819..4550988 100644 --- a/src/ClaudeDo.Worker/External/ExternalMcpService.cs +++ b/src/ClaudeDo.Worker/External/ExternalMcpService.cs @@ -11,8 +11,6 @@ namespace ClaudeDo.Worker.External; public sealed record TaskListDto(string Id, string Name, string? WorkingDir); -public sealed record TagDto(long Id, string Name); - public sealed record TaskDto( string Id, string ListId, @@ -32,7 +30,6 @@ public sealed class ExternalMcpService private readonly ListRepository _lists; private readonly QueueService _queue; private readonly HubBroadcaster _broadcaster; - private readonly TagRepository _tags; private readonly ITaskStateService _state; public ExternalMcpService( @@ -40,14 +37,12 @@ public sealed class ExternalMcpService ListRepository lists, QueueService queue, HubBroadcaster broadcaster, - TagRepository tags, ITaskStateService state) { _tasks = tasks; _lists = lists; _queue = queue; _broadcaster = broadcaster; - _tags = tags; _state = state; } @@ -91,14 +86,13 @@ public sealed class ExternalMcpService return ToDto(task); } - [McpServerTool, Description("Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution. Optional tags are attached on creation; missing tag names auto-create.")] + [McpServerTool, Description("Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution.")] public async Task AddTask( string listId, string title, string? description, string createdBy, bool queueImmediately, - IReadOnlyList? tags, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(listId)) @@ -124,9 +118,6 @@ public sealed class ExternalMcpService }; await _tasks.AddAsync(entity, cancellationToken); - if (tags is not null && tags.Count > 0) - await _tasks.SetTagsAsync(entity.Id, tags, cancellationToken); - if (queueImmediately) { // Routes through TaskStateService so the queue is woken automatically. @@ -140,13 +131,12 @@ public sealed class ExternalMcpService return ToDto(entity); } - [McpServerTool, Description("Update an existing task's title, description, commit type, and/or tags. Pass null to leave a field unchanged. Tags are replaced as a full set when non-null. Refuses if the task is currently Running.")] + [McpServerTool, Description("Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged. Refuses if the task is currently Running.")] public async Task UpdateTask( string taskId, string? title, string? description, string? commitType, - IReadOnlyList? tags, CancellationToken cancellationToken) { var task = await _tasks.GetByIdAsync(taskId, cancellationToken) @@ -159,9 +149,6 @@ public sealed class ExternalMcpService if (commitType is not null) task.CommitType = commitType; await _tasks.UpdateAsync(task, cancellationToken); - if (tags is not null) - await _tasks.SetTagsAsync(taskId, tags, cancellationToken); - var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!; await _broadcaster.TaskUpdated(taskId); return ToDto(reload); @@ -239,30 +226,6 @@ public sealed class ExternalMcpService await _broadcaster.TaskUpdated(taskId); } - [McpServerTool, Description("Replace the full tag set on an existing task. Missing tag names auto-create. Refuses if the task is Running.")] - public async Task SetTaskTags( - string taskId, - IReadOnlyList tags, - CancellationToken cancellationToken) - { - var task = await _tasks.GetByIdAsync(taskId, cancellationToken) - ?? throw new InvalidOperationException($"Task {taskId} not found."); - if (task.Status == TaskStatus.Running) - throw new InvalidOperationException("Cannot retag a running task. Cancel it first."); - - await _tasks.SetTagsAsync(taskId, tags, cancellationToken); - var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!; - await _broadcaster.TaskUpdated(taskId); - return ToDto(reload); - } - - [McpServerTool, Description("List all known tags. Useful for discovering existing tag names (including 'agent' which marks tasks for auto-execution) before tagging.")] - public async Task> ListTags(CancellationToken cancellationToken) - { - var tags = await _tags.GetAllAsync(cancellationToken); - return tags.Select(t => new TagDto(t.Id, t.Name)).ToList(); - } - private static TaskDto ToDto(TaskEntity t) => new( t.Id, t.ListId, diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 1f360c7..49b2dcc 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -331,41 +331,6 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub if (!result.Ok) throw new HubException(result.Reason ?? "set status failed"); } - public async Task SetTaskTags(string taskId, string[] tagNames) - { - await using var ctx = await _dbFactory.CreateDbContextAsync(); - var entity = await ctx.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == taskId); - if (entity is null) throw new HubException("task not found"); - - var desired = (tagNames ?? Array.Empty()) - .Select(n => n?.Trim().ToLowerInvariant() ?? "") - .Where(n => n.Length > 0) - .ToHashSet(); - - foreach (var t in entity.Tags.Where(t => !desired.Contains(t.Name)).ToList()) - entity.Tags.Remove(t); - - var existingByName = await ctx.Tags - .Where(t => desired.Contains(t.Name)) - .ToListAsync(); - foreach (var name in desired) - { - if (entity.Tags.Any(t => t.Name == name)) continue; - var tag = existingByName.FirstOrDefault(t => t.Name == name) - ?? new TagEntity { Name = name }; - if (tag.Id == 0) ctx.Tags.Add(tag); - entity.Tags.Add(tag); - } - await ctx.SaveChangesAsync(); - await _broadcaster.TaskUpdated(taskId); - } - - public async Task> GetAllTags() - { - await using var ctx = await _dbFactory.CreateDbContextAsync(); - return await ctx.Tags.OrderBy(t => t.Name).Select(t => t.Name).ToListAsync(); - } - public async Task UpdateTaskAgentSettings(UpdateTaskAgentSettingsDto dto) { using var ctx = _dbFactory.CreateDbContext(); diff --git a/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs b/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs index 94ca327..519e882 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningChainCoordinator.cs @@ -28,7 +28,6 @@ public sealed class PlanningChainCoordinator // chain leaves history alone but still reshapes the tail. // - Running children abort the operation — the chain cannot be reshaped while // one of its members is mid-flight. - // The "agent" tag is auto-attached to every child so the picker can claim them. // Returns the number of children placed in the chain. internal async Task SetupChainAsync(string parentTaskId, CancellationToken ct = default) { @@ -37,7 +36,6 @@ public sealed class PlanningChainCoordinator ?? throw new InvalidOperationException($"Task {parentTaskId} not found."); var children = await ctx.Tasks - .Include(t => t.Tags) .Where(t => t.ParentTaskId == parentTaskId) .OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt) .ToListAsync(ct); @@ -49,18 +47,6 @@ public sealed class PlanningChainCoordinator throw new InvalidOperationException( $"Child {running.Id} is running; cannot reshape chain."); - // Worker queue picker requires the "agent" tag — attach it so children are pickable. - var agentTag = await ctx.Tags.FirstOrDefaultAsync(t => t.Name == "agent", ct); - if (agentTag is not null) - { - foreach (var c in children) - { - if (!c.Tags.Any(t => t.Id == agentTag.Id)) - c.Tags.Add(agentTag); - } - await ctx.SaveChangesAsync(ct); - } - // Re-shape over Idle and Queued children only; leave Done/Failed/Cancelled // (terminal) results in place. var sequenceable = children diff --git a/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs b/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs index ecb7307..6bf9ffb 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningMcpService.cs @@ -8,7 +8,7 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.Planning; -public sealed record ChildTaskDto(string TaskId, string Title, string? Description, string Status, IReadOnlyList Tags); +public sealed record ChildTaskDto(string TaskId, string Title, string? Description, string Status); public sealed record CreatedChildDto(string TaskId, string Status); [McpServerToolType] @@ -41,12 +41,11 @@ public sealed class PlanningMcpService public async Task CreateChildTask( string title, string? description, - IReadOnlyList? tags, string? commitType, CancellationToken cancellationToken) { var ctx = _contextAccessor.Current; - var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, tags, commitType, cancellationToken); + var child = await _tasks.CreateChildAsync(ctx.ParentTaskId, title, description, commitType, cancellationToken); await BroadcastTaskUpdatedAsync(child.Id, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); return new CreatedChildDto(child.Id, child.Status.ToString()); @@ -58,24 +57,19 @@ public sealed class PlanningMcpService { var ctx = _contextAccessor.Current; var children = await _tasks.GetChildrenAsync(ctx.ParentTaskId, cancellationToken); - var list = new List(children.Count); - foreach (var c in children) - { - var tagList = await _tasks.GetTagsAsync(c.Id, cancellationToken); - list.Add(new ChildTaskDto(c.Id, c.Title, c.Description, c.Status.ToString(), tagList.Select(t => t.Name).ToList())); - } - return list; + return children + .Select(c => new ChildTaskDto(c.Id, c.Title, c.Description, c.Status.ToString())) + .ToList(); } private static readonly TaskStatus[] EditableStatuses = { TaskStatus.Idle, TaskStatus.Queued }; - [McpServerTool, Description("Update a child task in the active planning session. Can change title, description, tags (replaces the full set), commit type, and status. Status must be one of: Idle, Queued.")] + [McpServerTool, Description("Update a child task in the active planning session. Can change title, description, commit type, and status. Status must be one of: Idle, Queued.")] public async Task UpdateChildTask( string taskId, string? title, string? description, - IReadOnlyList? tags, string? commitType, string? status, CancellationToken cancellationToken) @@ -101,13 +95,12 @@ public sealed class PlanningMcpService newStatus = parsed; } - await _tasks.UpdateChildAsync(taskId, title, description, commitType, tags, newStatus, cancellationToken); + await _tasks.UpdateChildAsync(taskId, title, description, commitType, newStatus, cancellationToken); var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!; - var tagList = await _tasks.GetTagsAsync(reload.Id, cancellationToken); await BroadcastTaskUpdatedAsync(reload.Id, cancellationToken); await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken); - return new ChildTaskDto(reload.Id, reload.Title, reload.Description, reload.Status.ToString(), tagList.Select(t => t.Name).ToList()); + return new ChildTaskDto(reload.Id, reload.Title, reload.Description, reload.Status.ToString()); } [McpServerTool, Description("Delete a child task in the active planning session.")] diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 7dfde97..41cac0d 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -185,7 +185,6 @@ if (cfg.ExternalMcpPort > 0) sp.GetRequiredService>().CreateDbContext()); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); - externalBuilder.Services.AddScoped(); externalBuilder.Services.AddScoped(); externalBuilder.Services.AddMcpServer() .WithHttpTransport() diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index cdada3d..a40fe08 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -362,19 +362,14 @@ public sealed class TaskRunner TaskEntity task, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct) { AppSettingsEntity global; - bool isAgentTask; using (var ctx = _dbFactory.CreateDbContext()) { var settingsRepo = new AppSettingsRepository(ctx); global = await settingsRepo.GetAsync(ct); - - var taskRepo = new TaskRepository(ctx); - var tags = await taskRepo.GetEffectiveTagsAsync(task.Id, ct); - isAgentTask = tags.Any(t => string.Equals(t.Name, "agent", StringComparison.OrdinalIgnoreCase)); } var systemFile = PromptFiles.ReadOrNull(PromptKind.System); - var agentFile = isAgentTask ? PromptFiles.ReadOrNull(PromptKind.Agent) : null; + var agentFile = PromptFiles.ReadOrNull(PromptKind.Agent); var instructions = MergeInstructions( systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile); diff --git a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs index 13d74b5..6c0b41a 100644 --- a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpServiceTests.cs @@ -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(() => - 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(() => - 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(() => 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(() => - sut.SetTaskTags(task.Id, new[] { "manual" }, CancellationToken.None)); - } } diff --git a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs index 8349030..37de60a 100644 --- a/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Hub/PlanningHubTests.cs @@ -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); diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs index 3eed8bc..df25190 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningChainCoordinatorTests.cs @@ -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() { diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs index 5acbef4..d99f5f6 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningEndToEndTests.cs @@ -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; diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningMcpServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningMcpServiceTests.cs index 74b1ae8..0d74b09 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningMcpServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningMcpServiceTests.cs @@ -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(() => - 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(() => - 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(() => - 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(() => - 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); diff --git a/tests/ClaudeDo.Worker.Tests/Planning/PlanningSessionManagerTests.cs b/tests/ClaudeDo.Worker.Tests/Planning/PlanningSessionManagerTests.cs index 0f1c901..6fcb716 100644 --- a/tests/ClaudeDo.Worker.Tests/Planning/PlanningSessionManagerTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Planning/PlanningSessionManagerTests.cs @@ -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(() => _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); diff --git a/tests/ClaudeDo.Worker.Tests/Queue/QueuePickerTests.cs b/tests/ClaudeDo.Worker.Tests/Queue/QueuePickerTests.cs index 3326e0d..9b929cd 100644 --- a/tests/ClaudeDo.Worker.Tests/Queue/QueuePickerTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Queue/QueuePickerTests.cs @@ -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; diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryTests.cs index 20463a2..ee87d07 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/ListRepositoryTests.cs @@ -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); - } } diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs index a604ecf..b4c9647 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryOrphanGuardTests.cs @@ -68,7 +68,7 @@ public sealed class TaskRepositoryOrphanGuardTests : IDisposable await _tasks.AddAsync(parent); var ex = await Assert.ThrowsAsync( - () => _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); diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs index ddd33f8..bdfaae7 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryPlanningTests.cs @@ -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(() => - _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(async () => { diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs index d550613..d1201c6 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.cs @@ -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()); - - Assert.Empty(await _tasks.GetTagsAsync(task.Id)); - } } diff --git a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs index 70a1e20..f5ff9da 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceSlotGuardTests.cs @@ -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; } diff --git a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs index b7f1749..fdd938a 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/QueueServiceTests.cs @@ -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 SeedQueuedTask(string listId, DateTime? scheduledFor = null, DateTime? createdAt = null) diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs index 07b7c75..ab06d35 100644 --- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs @@ -40,8 +40,6 @@ sealed class FakeWorkerClient : IWorkerClient public Task GetListConfigAsync(string listId) => Task.FromResult(null); public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask; public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask; - public Task SetTaskTagsAsync(string taskId, IEnumerable tagNames) => Task.CompletedTask; - public Task> GetAllTagsAsync() => Task.FromResult(new List()); 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;