Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Repositories/TaskRepositoryTests.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

225 lines
7.7 KiB
C#

using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Tests.Infrastructure;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.Repositories;
public sealed class TaskRepositoryTests : IDisposable
{
private readonly DbFixture _db = new();
private readonly ClaudeDoDbContext _ctx;
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
private readonly TagRepository _tags;
public TaskRepositoryTests()
{
_ctx = _db.CreateContext();
_tasks = new TaskRepository(_ctx);
_lists = new ListRepository(_ctx);
_tags = new TagRepository(_ctx);
}
public void Dispose()
{
_ctx.Dispose();
_db.Dispose();
}
private async Task<string> CreateListAsync(string? id = null)
{
var listId = id ?? Guid.NewGuid().ToString();
await _lists.AddAsync(new ListEntity
{
Id = listId,
Name = "Test List",
CreatedAt = DateTime.UtcNow,
});
return listId;
}
private TaskEntity MakeTask(string listId, TaskStatus status = TaskStatus.Queued, DateTime? createdAt = null, DateTime? scheduledFor = null) => new()
{
Id = Guid.NewGuid().ToString(),
ListId = listId,
Title = "Test Task",
Description = "A description",
Status = status,
ScheduledFor = scheduledFor,
CreatedAt = createdAt ?? DateTime.UtcNow,
CommitType = "feat",
};
[Fact]
public async Task AddAsync_Roundtrips_AllFields()
{
var listId = await CreateListAsync();
var entity = new TaskEntity
{
Id = Guid.NewGuid().ToString(),
ListId = listId,
Title = "My Task",
Description = "Desc",
Status = TaskStatus.Queued,
ScheduledFor = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc),
Result = "some result",
LogPath = "/tmp/log.ndjson",
CreatedAt = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
StartedAt = new DateTime(2026, 1, 1, 1, 0, 0, DateTimeKind.Utc),
FinishedAt = new DateTime(2026, 1, 1, 2, 0, 0, DateTimeKind.Utc),
CommitType = "feat",
};
await _tasks.AddAsync(entity);
var loaded = await _tasks.GetByIdAsync(entity.Id);
Assert.NotNull(loaded);
Assert.Equal(entity.Id, loaded.Id);
Assert.Equal(entity.ListId, loaded.ListId);
Assert.Equal(entity.Title, loaded.Title);
Assert.Equal(entity.Description, loaded.Description);
Assert.Equal(entity.Status, loaded.Status);
Assert.Equal(entity.ScheduledFor!.Value.Date, loaded.ScheduledFor!.Value.Date);
Assert.Equal(entity.Result, loaded.Result);
Assert.Equal(entity.LogPath, loaded.LogPath);
Assert.Equal(entity.CommitType, loaded.CommitType);
}
[Fact]
public async Task GetNextQueuedAgentTaskAsync_Returns_OldestWithAgentTag_ViaTaskTag()
{
var listId = await CreateListAsync();
var agentTagId = await _tags.GetOrCreateAsync("agent");
var older = MakeTask(listId, createdAt: DateTime.UtcNow.AddMinutes(-10));
var newer = MakeTask(listId, createdAt: DateTime.UtcNow);
await _tasks.AddAsync(older);
await _tasks.AddAsync(newer);
await _tasks.AddTagAsync(older.Id, agentTagId);
await _tasks.AddTagAsync(newer.Id, agentTagId);
var picked = await _tasks.GetNextQueuedAgentTaskAsync(DateTime.UtcNow);
Assert.NotNull(picked);
Assert.Equal(older.Id, picked.Id);
}
[Fact]
public async Task GetNextQueuedAgentTaskAsync_Returns_TaskWithAgentTag_ViaListTag()
{
var listId = await CreateListAsync();
var agentTagId = await _tags.GetOrCreateAsync("agent");
await _lists.AddTagAsync(listId, agentTagId);
var task = MakeTask(listId);
await _tasks.AddAsync(task);
var picked = await _tasks.GetNextQueuedAgentTaskAsync(DateTime.UtcNow);
Assert.NotNull(picked);
Assert.Equal(task.Id, picked.Id);
}
[Fact]
public async Task GetNextQueuedAgentTaskAsync_ReturnsNull_WhenNoAgentTag()
{
var listId = await CreateListAsync();
var task = MakeTask(listId);
await _tasks.AddAsync(task);
var picked = await _tasks.GetNextQueuedAgentTaskAsync(DateTime.UtcNow);
Assert.Null(picked);
}
[Fact]
public async Task GetNextQueuedAgentTaskAsync_Skips_FutureScheduledFor()
{
var listId = await CreateListAsync();
var agentTagId = await _tags.GetOrCreateAsync("agent");
var task = MakeTask(listId, scheduledFor: DateTime.UtcNow.AddHours(1));
await _tasks.AddAsync(task);
await _tasks.AddTagAsync(task.Id, agentTagId);
var picked = await _tasks.GetNextQueuedAgentTaskAsync(DateTime.UtcNow);
Assert.Null(picked);
}
[Fact]
public async Task Transitions_MarkRunning_ThenMarkDone()
{
var listId = await CreateListAsync();
var task = MakeTask(listId);
await _tasks.AddAsync(task);
var startedAt = DateTime.UtcNow;
await _tasks.MarkRunningAsync(task.Id, startedAt);
var running = await _tasks.GetByIdAsync(task.Id);
Assert.Equal(TaskStatus.Running, running!.Status);
Assert.NotNull(running.StartedAt);
var finishedAt = DateTime.UtcNow;
await _tasks.MarkDoneAsync(task.Id, finishedAt, "All good");
var done = await _tasks.GetByIdAsync(task.Id);
Assert.Equal(TaskStatus.Done, done!.Status);
Assert.Equal("All good", done.Result);
Assert.NotNull(done.FinishedAt);
}
[Fact]
public async Task FlipAllRunningToFailedAsync_FlipsOnlyRunningRows()
{
var listId = await CreateListAsync();
var running1 = MakeTask(listId, status: TaskStatus.Running);
var running2 = MakeTask(listId, status: TaskStatus.Running);
var queued = MakeTask(listId, status: TaskStatus.Queued);
var done = MakeTask(listId, status: TaskStatus.Done);
await _tasks.AddAsync(running1);
await _tasks.AddAsync(running2);
await _tasks.AddAsync(queued);
await _tasks.AddAsync(done);
var flipped = await _tasks.FlipAllRunningToFailedAsync("worker restart");
Assert.Equal(2, flipped);
var r1 = await _tasks.GetByIdAsync(running1.Id);
Assert.Equal(TaskStatus.Failed, r1!.Status);
Assert.StartsWith("[stale] ", r1.Result);
var r2 = await _tasks.GetByIdAsync(running2.Id);
Assert.Equal(TaskStatus.Failed, r2!.Status);
var q = await _tasks.GetByIdAsync(queued.Id);
Assert.Equal(TaskStatus.Queued, q!.Status);
var d = await _tasks.GetByIdAsync(done.Id);
Assert.Equal(TaskStatus.Done, d!.Status);
}
[Fact]
public async Task GetEffectiveTagsAsync_Returns_Union_Of_ListTags_And_TaskTags()
{
var listId = await CreateListAsync();
var agentTagId = await _tags.GetOrCreateAsync("agent");
var manualTagId = await _tags.GetOrCreateAsync("manual");
var codeTagId = await _tags.GetOrCreateAsync("code");
await _lists.AddTagAsync(listId, agentTagId);
var task = MakeTask(listId);
await _tasks.AddAsync(task);
await _tasks.AddTagAsync(task.Id, manualTagId);
await _tasks.AddTagAsync(task.Id, codeTagId);
var effective = await _tasks.GetEffectiveTagsAsync(task.Id);
var names = effective.Select(t => t.Name).OrderBy(n => n).ToList();
Assert.Equal(3, names.Count);
Assert.Contains("agent", names);
Assert.Contains("code", names);
Assert.Contains("manual", names);
}
}