204 lines
7.9 KiB
C#
204 lines
7.9 KiB
C#
using ClaudeDo.Data.Git;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Services;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Services;
|
|
|
|
public class WorktreeMaintenanceServiceTests : IDisposable
|
|
{
|
|
private readonly List<DbFixture> _dbs = new();
|
|
private readonly List<GitRepoFixture> _repos = new();
|
|
|
|
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
|
|
|
|
private DbFixture NewDb() { var d = new DbFixture(); _dbs.Add(d); return d; }
|
|
private GitRepoFixture NewRepo() { var r = new GitRepoFixture(); _repos.Add(r); return r; }
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var d in _dbs) try { d.Dispose(); } catch { }
|
|
foreach (var r in _repos) try { r.Dispose(); } catch { }
|
|
}
|
|
|
|
private static (ListEntity list, TaskEntity task) MakeEntities(string workingDir, ClaudeDo.Data.Models.TaskStatus status = ClaudeDo.Data.Models.TaskStatus.Done)
|
|
{
|
|
var list = new ListEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
Name = "test",
|
|
WorkingDir = workingDir,
|
|
DefaultCommitType = "feat",
|
|
CreatedAt = DateTime.UtcNow,
|
|
};
|
|
var task = MakeTaskForList(list.Id, status);
|
|
return (list, task);
|
|
}
|
|
|
|
private static TaskEntity MakeTaskForList(string listId, ClaudeDo.Data.Models.TaskStatus status)
|
|
=> new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "t",
|
|
Description = null,
|
|
Status = status,
|
|
CreatedAt = DateTime.UtcNow,
|
|
};
|
|
|
|
private static async Task<string> CreateWorktreeAsync(GitService git, string repoDir, string taskId)
|
|
{
|
|
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
|
var baseCommit = await git.RevParseHeadAsync(repoDir);
|
|
await git.WorktreeAddAsync(repoDir, $"test/{taskId}", wtPath, baseCommit);
|
|
return wtPath;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CleanupFinished_Removes_Merged_And_Discarded_But_Skips_Active()
|
|
{
|
|
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
|
|
|
var repo = NewRepo();
|
|
var git = new GitService();
|
|
var db = NewDb();
|
|
|
|
var (list, activeTask) = MakeEntities(repo.RepoDir);
|
|
var mergedTask = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Done);
|
|
var discardedTask = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Done);
|
|
|
|
var activeWt = await CreateWorktreeAsync(git, repo.RepoDir, activeTask.Id);
|
|
var mergedWt = await CreateWorktreeAsync(git, repo.RepoDir, mergedTask.Id);
|
|
var discardedWt = await CreateWorktreeAsync(git, repo.RepoDir, discardedTask.Id);
|
|
|
|
using (var ctx = db.CreateContext())
|
|
{
|
|
await new ListRepository(ctx).AddAsync(list);
|
|
var taskRepo = new TaskRepository(ctx);
|
|
await taskRepo.AddAsync(activeTask);
|
|
await taskRepo.AddAsync(mergedTask);
|
|
await taskRepo.AddAsync(discardedTask);
|
|
var wtRepo = new WorktreeRepository(ctx);
|
|
await wtRepo.AddAsync(new WorktreeEntity
|
|
{
|
|
TaskId = activeTask.Id, Path = activeWt, BranchName = $"test/{activeTask.Id}",
|
|
BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
|
|
});
|
|
await wtRepo.AddAsync(new WorktreeEntity
|
|
{
|
|
TaskId = mergedTask.Id, Path = mergedWt, BranchName = $"test/{mergedTask.Id}",
|
|
BaseCommit = repo.BaseCommit, State = WorktreeState.Merged, CreatedAt = DateTime.UtcNow,
|
|
});
|
|
await wtRepo.AddAsync(new WorktreeEntity
|
|
{
|
|
TaskId = discardedTask.Id, Path = discardedWt, BranchName = $"test/{discardedTask.Id}",
|
|
BaseCommit = repo.BaseCommit, State = WorktreeState.Discarded, CreatedAt = DateTime.UtcNow,
|
|
});
|
|
}
|
|
|
|
var svc = new WorktreeMaintenanceService(
|
|
db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);
|
|
|
|
var result = await svc.CleanupFinishedAsync();
|
|
|
|
Assert.Equal(2, result.Removed);
|
|
Assert.True(Directory.Exists(activeWt));
|
|
Assert.False(Directory.Exists(mergedWt));
|
|
Assert.False(Directory.Exists(discardedWt));
|
|
|
|
using var checkCtx = db.CreateContext();
|
|
var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
|
|
Assert.Single(remaining);
|
|
Assert.Equal(activeTask.Id, remaining[0].TaskId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetAll_Blocked_When_Running_Task_Exists()
|
|
{
|
|
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.ResetAllAsync();
|
|
|
|
Assert.True(result.Blocked);
|
|
Assert.Equal(1, result.RunningTasks);
|
|
Assert.Equal(0, result.Removed);
|
|
Assert.True(Directory.Exists(wt));
|
|
|
|
// Cleanup
|
|
try { await git.WorktreeRemoveAsync(repo.RepoDir, wt, force: true); } catch { }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ResetAll_Removes_All_When_No_Running_Tasks()
|
|
{
|
|
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
|
|
|
var repo = NewRepo();
|
|
var git = new GitService();
|
|
var db = NewDb();
|
|
|
|
var (list, t1) = MakeEntities(repo.RepoDir, status: ClaudeDo.Data.Models.TaskStatus.Done);
|
|
var t2 = MakeTaskForList(list.Id, ClaudeDo.Data.Models.TaskStatus.Manual);
|
|
|
|
var wt1 = await CreateWorktreeAsync(git, repo.RepoDir, t1.Id);
|
|
var wt2 = await CreateWorktreeAsync(git, repo.RepoDir, t2.Id);
|
|
|
|
using (var ctx = db.CreateContext())
|
|
{
|
|
await new ListRepository(ctx).AddAsync(list);
|
|
var taskRepo = new TaskRepository(ctx);
|
|
await taskRepo.AddAsync(t1);
|
|
await taskRepo.AddAsync(t2);
|
|
var wtRepo = new WorktreeRepository(ctx);
|
|
await wtRepo.AddAsync(new WorktreeEntity
|
|
{
|
|
TaskId = t1.Id, Path = wt1, BranchName = $"test/{t1.Id}",
|
|
BaseCommit = repo.BaseCommit, State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
|
|
});
|
|
await wtRepo.AddAsync(new WorktreeEntity
|
|
{
|
|
TaskId = t2.Id, Path = wt2, BranchName = $"test/{t2.Id}",
|
|
BaseCommit = repo.BaseCommit, State = WorktreeState.Kept, CreatedAt = DateTime.UtcNow,
|
|
});
|
|
}
|
|
|
|
var svc = new WorktreeMaintenanceService(
|
|
db.CreateFactory(), git, NullLogger<WorktreeMaintenanceService>.Instance);
|
|
|
|
var result = await svc.ResetAllAsync();
|
|
|
|
Assert.False(result.Blocked);
|
|
Assert.Equal(2, result.Removed);
|
|
Assert.Equal(2, result.TasksAffected);
|
|
Assert.False(Directory.Exists(wt1));
|
|
Assert.False(Directory.Exists(wt2));
|
|
|
|
using var checkCtx = db.CreateContext();
|
|
var remaining = await new WorktreeRepository(checkCtx).GetAllAsync();
|
|
Assert.Empty(remaining);
|
|
}
|
|
}
|