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>
This commit is contained in:
mika kuns
2026-04-16 08:59:24 +02:00
parent b7be52a623
commit 36484ed45a
18 changed files with 479 additions and 232 deletions

View File

@@ -1,3 +1,4 @@
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
@@ -24,19 +25,19 @@ public class WorktreeManagerTests : IDisposable
return f;
}
private async Task<(WorktreeManager mgr, WorktreeRepository wtRepo)> CreateManagerAsync(
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.
var listRepo = new ListRepository(db.Factory);
var taskRepo = new TaskRepository(db.Factory);
using var seedCtx = db.CreateContext();
var listRepo = new ListRepository(seedCtx);
var taskRepo = new TaskRepository(seedCtx);
await listRepo.AddAsync(list);
await taskRepo.AddAsync(task);
var wtRepo = new WorktreeRepository(db.Factory);
var cfg = new WorkerConfig
{
WorktreeRootStrategy = strategy,
@@ -45,8 +46,8 @@ public class WorktreeManagerTests : IDisposable
cfg.CentralWorktreeRoot = centralRoot;
var mgr = new WorktreeManager(
new GitService(), wtRepo, cfg, NullLogger<WorktreeManager>.Instance);
return (mgr, wtRepo);
new GitService(), db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
return (mgr, db);
}
[Fact]
@@ -56,7 +57,7 @@ public class WorktreeManagerTests : IDisposable
var repo = CreateRepo();
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, wtRepo) = await CreateManagerAsync(task, list);
var (mgr, db) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
@@ -66,6 +67,8 @@ public class WorktreeManagerTests : IDisposable
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);
@@ -80,7 +83,7 @@ public class WorktreeManagerTests : IDisposable
var repo = CreateRepo();
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, wtRepo) = await CreateManagerAsync(task, list);
var (mgr, db) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
@@ -88,6 +91,8 @@ public class WorktreeManagerTests : IDisposable
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);
}
@@ -99,7 +104,7 @@ public class WorktreeManagerTests : IDisposable
var repo = CreateRepo();
var (task, list) = MakeEntities(repo.RepoDir);
var (mgr, wtRepo) = await CreateManagerAsync(task, list);
var (mgr, db) = await CreateManagerAsync(task, list);
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
@@ -109,6 +114,8 @@ public class WorktreeManagerTests : IDisposable
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);
@@ -129,20 +136,24 @@ public class WorktreeManagerTests : IDisposable
var db = new DbFixture();
_dbFixtures.Add(db);
var listRepo = new ListRepository(db.Factory);
var taskRepo = new TaskRepository(db.Factory);
await listRepo.AddAsync(list);
await taskRepo.AddAsync(task);
using (var seedCtx = db.CreateContext())
{
var listRepo = new ListRepository(seedCtx);
var taskRepo = new TaskRepository(seedCtx);
await listRepo.AddAsync(list);
await taskRepo.AddAsync(task);
}
var wtRepo = new WorktreeRepository(db.Factory);
var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
var mgr = new WorktreeManager(
new GitService(), wtRepo, cfg, NullLogger<WorktreeManager>.Instance);
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);
}