feat(worktrees): add ForceRemoveAsync for targeted removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-19 09:34:32 +02:00
parent ce30d01b72
commit b095a29f97
2 changed files with 131 additions and 0 deletions

View File

@@ -370,4 +370,111 @@ public class WorktreeMaintenanceServiceTests : IDisposable
Assert.Single(rows);
Assert.False(rows[0].PathExistsOnDisk);
}
[Fact]
public async Task ForceRemove_Removes_Active_Worktree()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = NewRepo();
var git = new GitService();
var db = NewDb();
var (list, task) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Done);
var wt = await CreateWorktreeAsync(git, repo.RepoDir, task.Id);
using (var ctx = db.CreateContext())
{
await new ListRepository(ctx).AddAsync(list);
await new TaskRepository(ctx).AddAsync(task);
await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
{
TaskId = task.Id, Path = wt, BranchName = $"test/{task.Id}",
BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
});
}
var svc = new WorktreeMaintenanceService(
db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);
var result = await svc.ForceRemoveAsync(task.Id, CancellationToken.None);
Assert.True(result.Removed);
Assert.Null(result.Reason);
Assert.False(Directory.Exists(wt));
using var checkCtx = db.CreateContext();
var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
Assert.Empty(remaining);
}
[Fact]
public async Task ForceRemove_Blocked_When_Task_Running()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = NewRepo();
var git = new GitService();
var db = NewDb();
var (list, task) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Running);
var wt = await CreateWorktreeAsync(git, repo.RepoDir, task.Id);
using (var ctx = db.CreateContext())
{
await new ListRepository(ctx).AddAsync(list);
await new TaskRepository(ctx).AddAsync(task);
await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
{
TaskId = task.Id, Path = wt, BranchName = $"test/{task.Id}",
BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
});
}
var svc = new WorktreeMaintenanceService(
db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);
var result = await svc.ForceRemoveAsync(task.Id, CancellationToken.None);
Assert.False(result.Removed);
Assert.Equal("task is currently running", result.Reason);
Assert.True(Directory.Exists(wt));
try { await git.WorktreeRemoveAsync(repo.RepoDir, wt, force: true); } catch { }
}
[Fact]
public async Task ForceRemove_Removes_Phantom_Row()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = NewRepo();
var git = new GitService();
var db = NewDb();
var (list, task) = MakeEntities(repo.RepoDir);
var phantomPath = Path.Combine(Path.GetTempPath(), $"wt_phantom_{Guid.NewGuid():N}");
using (var ctx = db.CreateContext())
{
await new ListRepository(ctx).AddAsync(list);
await new TaskRepository(ctx).AddAsync(task);
await new WorktreeRepository(ctx).AddAsync(new WorktreeEntity
{
TaskId = task.Id, Path = phantomPath, BranchName = $"test/{task.Id}-phantom",
BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
});
}
var svc = new WorktreeMaintenanceService(
db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);
var result = await svc.ForceRemoveAsync(task.Id, CancellationToken.None);
Assert.True(result.Removed);
using var checkCtx = db.CreateContext();
var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
Assert.Empty(remaining);
}
}