From 89f6b836ba378f75b4372df4d964b2c5f193abfa Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 19 May 2026 09:29:27 +0200 Subject: [PATCH] feat(worktrees): allow CleanupFinishedAsync to filter by list Co-Authored-By: Claude Sonnet 4.6 --- .../Worktrees/WorktreeMaintenanceService.cs | 19 ++++--- .../WorktreeMaintenanceServiceTests.cs | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs b/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs index a6d11ce..ee4ddde 100644 --- a/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs +++ b/src/ClaudeDo.Worker/Worktrees/WorktreeMaintenanceService.cs @@ -24,16 +24,19 @@ public sealed class WorktreeMaintenanceService _logger = logger; } - public async Task CleanupFinishedAsync(CancellationToken ct = default) + public async Task CleanupFinishedAsync(string? listId = null, CancellationToken ct = default) { using var context = _dbFactory.CreateDbContext(); - var rows = await (from w in context.Worktrees - join t in context.Tasks on w.TaskId equals t.Id - join l in context.Lists on t.ListId equals l.Id - where w.State == WorktreeState.Merged || w.State == WorktreeState.Discarded - select new WorktreeRow(w.TaskId, w.Path, w.BranchName, l.WorkingDir)) - .AsNoTracking() - .ToListAsync(ct); + var query = from w in context.Worktrees + join t in context.Tasks on w.TaskId equals t.Id + join l in context.Lists on t.ListId equals l.Id + where w.State == WorktreeState.Merged || w.State == WorktreeState.Discarded + select new { Row = new WorktreeRow(w.TaskId, w.Path, w.BranchName, l.WorkingDir), ListId = t.ListId }; + + if (!string.IsNullOrEmpty(listId)) + query = query.Where(x => x.ListId == listId); + + var rows = await query.AsNoTracking().Select(x => x.Row).ToListAsync(ct); int removed = 0; foreach (var row in rows) diff --git a/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs b/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs index 0bb203f..3a4a8b7 100644 --- a/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Services/WorktreeMaintenanceServiceTests.cs @@ -200,4 +200,54 @@ public class WorktreeMaintenanceServiceTests : IDisposable var remaining = await new WorktreeRepository(checkCtx).GetAllAsync(); Assert.Empty(remaining); } + + [Fact] + public async Task CleanupFinished_With_ListId_Only_Removes_That_Lists_Rows() + { + if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; } + + var repo = NewRepo(); + var git = new GitService(); + var db = NewDb(); + + var (listA, taskA) = MakeEntities(repo.RepoDir); + var (listB, taskB) = MakeEntities(repo.RepoDir); + + var wtA = await CreateWorktreeAsync(git, repo.RepoDir, taskA.Id); + var wtB = await CreateWorktreeAsync(git, repo.RepoDir, taskB.Id); + + using (var ctx = db.CreateContext()) + { + await new ListRepository(ctx).AddAsync(listA); + await new ListRepository(ctx).AddAsync(listB); + var taskRepo = new TaskRepository(ctx); + await taskRepo.AddAsync(taskA); + await taskRepo.AddAsync(taskB); + var wtRepo = new WorktreeRepository(ctx); + await wtRepo.AddAsync(new WorktreeEntity + { + TaskId = taskA.Id, Path = wtA, BranchName = $"test/{taskA.Id}", + BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow, + }); + await wtRepo.AddAsync(new WorktreeEntity + { + TaskId = taskB.Id, Path = wtB, BranchName = $"test/{taskB.Id}", + BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow, + }); + } + + var svc = new WorktreeMaintenanceService( + db.CreateFactory(), git, NullLogger.Instance); + + var result = await svc.CleanupFinishedAsync(listA.Id, CancellationToken.None); + + Assert.Equal(1, result.Removed); + Assert.False(Directory.Exists(wtA)); + Assert.True(Directory.Exists(wtB)); + + using var checkCtx = db.CreateContext(); + var remaining = await new WorktreeRepository(checkCtx).GetAllAsync(); + Assert.Single(remaining); + Assert.Equal(taskB.Id, remaining[0].TaskId); + } }