Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs
mika kuns 36484ed45a feat(worker,ui): wire EF Core into DI and update all consumers to IDbContextFactory
Worker and App Program.cs: replace SqliteConnectionFactory+SchemaInitializer
with AddDbContextFactory<ClaudeDoDbContext> + Database.Migrate(). Repos
changed from AddSingleton to AddScoped.

All singleton services (QueueService, StaleTaskRecovery, WorktreeManager,
TaskRunner) and singleton ViewModels (MainWindowViewModel, TaskDetailViewModel,
TaskListViewModel, TaskEditorViewModel) now take IDbContextFactory<ClaudeDoDbContext>
and create short-lived contexts per operation.

Test infrastructure: DbFixture now uses EF migrations instead of SchemaInitializer;
all test classes create contexts via DbFixture.CreateContext().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 08:59:24 +02:00

198 lines
6.9 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
namespace ClaudeDo.Worker.Tests.Runner;
public class WorktreeManagerTests : IDisposable
{
private readonly List<GitRepoFixture> _fixtures = new();
private readonly List<DbFixture> _dbFixtures = new();
private readonly List<string> _tempDirs = new();
private readonly List<(string repoDir, string wtPath)> _worktreeCleanups = new();
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
private GitRepoFixture CreateRepo()
{
var f = new GitRepoFixture();
_fixtures.Add(f);
return f;
}
private async Task<(WorktreeManager mgr, DbFixture db)> CreateManagerAsync(
TaskEntity task, ListEntity list, string strategy = "sibling", string? centralRoot = null)
{
var db = new DbFixture();
_dbFixtures.Add(db);
// Seed the DB with list and task so FK constraints pass.
using var seedCtx = db.CreateContext();
var listRepo = new ListRepository(seedCtx);
var taskRepo = new TaskRepository(seedCtx);
await listRepo.AddAsync(list);
await taskRepo.AddAsync(task);
var cfg = new WorkerConfig
{
WorktreeRootStrategy = strategy,
};
if (centralRoot is not null)
cfg.CentralWorktreeRoot = centralRoot;
var mgr = new WorktreeManager(
new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
return (mgr, db);
}
[Fact]
public async Task CreateAsync_Succeeds_InGitRepo()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, db) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
Assert.NotNull(ctx);
Assert.True(Directory.Exists(ctx.WorktreePath));
Assert.Equal($"claudedo/{task.Id.Replace("-", "")}", ctx.BranchName);
Assert.Equal(repo.BaseCommit, ctx.BaseCommit);
using var readCtx = db.CreateContext();
var wtRepo = new WorktreeRepository(readCtx);
var row = await wtRepo.GetByTaskIdAsync(task.Id);
Assert.NotNull(row);
Assert.Equal(WorktreeState.Active, row!.State);
Assert.Equal(ctx.BaseCommit, row.BaseCommit);
Assert.Null(row.HeadCommit);
}
[Fact]
public async Task CommitIfChangedAsync_NoChanges_HeadCommitStaysNull()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, db) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
var committed = await mgr.CommitIfChangedAsync(ctx, task, list, CancellationToken.None);
Assert.False(committed);
using var readCtx = db.CreateContext();
var wtRepo = new WorktreeRepository(readCtx);
var row = await wtRepo.GetByTaskIdAsync(task.Id);
Assert.Null(row!.HeadCommit);
}
[Fact]
public async Task CommitIfChangedAsync_WithNewFile_HeadCommitSet()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, db) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
File.WriteAllText(Path.Combine(ctx.WorktreePath, "hello.txt"), "hello world");
var committed = await mgr.CommitIfChangedAsync(ctx, task, list, CancellationToken.None);
Assert.True(committed);
using var readCtx = db.CreateContext();
var wtRepo = new WorktreeRepository(readCtx);
var row = await wtRepo.GetByTaskIdAsync(task.Id);
Assert.NotNull(row!.HeadCommit);
Assert.NotEqual(ctx.BaseCommit, row.HeadCommit);
Assert.NotNull(row.DiffStat);
Assert.Contains("hello.txt", row.DiffStat);
}
[Fact]
public async Task CreateAsync_NonGitDir_Throws_NoRow()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var tmpDir = Path.Combine(Path.GetTempPath(), $"claudedo_nogit_{Guid.NewGuid():N}");
Directory.CreateDirectory(tmpDir);
_tempDirs.Add(tmpDir);
var (task, list) = MakeEntities(tmpDir);
var db = new DbFixture();
_dbFixtures.Add(db);
using (var seedCtx = db.CreateContext())
{
var listRepo = new ListRepository(seedCtx);
var taskRepo = new TaskRepository(seedCtx);
await listRepo.AddAsync(list);
await taskRepo.AddAsync(task);
}
var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
var mgr = new WorktreeManager(
new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => mgr.CreateAsync(task, list, CancellationToken.None));
Assert.Contains("not a git repository", ex.Message);
using var readCtx = db.CreateContext();
var wtRepo = new WorktreeRepository(readCtx);
var row = await wtRepo.GetByTaskIdAsync(task.Id);
Assert.Null(row);
}
private static (TaskEntity task, ListEntity list) MakeEntities(string workingDir)
{
var listId = Guid.NewGuid().ToString();
var taskId = Guid.NewGuid().ToString();
var task = new TaskEntity
{
Id = taskId,
ListId = listId,
Title = "test task",
Description = "a description",
CommitType = "chore",
CreatedAt = DateTime.UtcNow,
};
var list = new ListEntity
{
Id = listId,
Name = "Test List",
WorkingDir = workingDir,
CreatedAt = DateTime.UtcNow,
};
return (task, list);
}
public void Dispose()
{
foreach (var (repoDir, wtPath) in _worktreeCleanups)
{
try { GitRepoFixture.RunGit(repoDir, "worktree", "remove", "--force", wtPath); } catch { }
}
foreach (var f in _fixtures) f.Dispose();
foreach (var db in _dbFixtures) db.Dispose();
foreach (var d in _tempDirs)
{
try { Directory.Delete(d, true); } catch { }
}
}
}