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>
178 lines
5.6 KiB
C#
178 lines
5.6 KiB
C#
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Repositories;
|
|
|
|
public sealed class TaskRunRepositoryTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
private readonly ClaudeDoDbContext _ctx;
|
|
private readonly TaskRunRepository _runs;
|
|
private readonly string _taskId;
|
|
|
|
public TaskRunRepositoryTests()
|
|
{
|
|
_ctx = _db.CreateContext();
|
|
_runs = new TaskRunRepository(_ctx);
|
|
|
|
// Seed a list and task for all tests
|
|
var lists = new ListRepository(_ctx);
|
|
var tasks = new TaskRepository(_ctx);
|
|
var listId = Guid.NewGuid().ToString();
|
|
lists.AddAsync(new ListEntity
|
|
{
|
|
Id = listId,
|
|
Name = "Test List",
|
|
CreatedAt = DateTime.UtcNow,
|
|
}).GetAwaiter().GetResult();
|
|
|
|
_taskId = Guid.NewGuid().ToString();
|
|
tasks.AddAsync(new TaskEntity
|
|
{
|
|
Id = _taskId,
|
|
ListId = listId,
|
|
Title = "Test Task",
|
|
Status = Data.Models.TaskStatus.Queued,
|
|
CreatedAt = DateTime.UtcNow,
|
|
CommitType = "feat",
|
|
}).GetAwaiter().GetResult();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_ctx.Dispose();
|
|
_db.Dispose();
|
|
}
|
|
|
|
private TaskRunEntity MakeRun(int runNumber, bool isRetry = false) => new()
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
TaskId = _taskId,
|
|
RunNumber = runNumber,
|
|
IsRetry = isRetry,
|
|
Prompt = $"Do something (run {runNumber})",
|
|
StartedAt = DateTime.UtcNow,
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Add_And_GetById_Roundtrips()
|
|
{
|
|
var entity = new TaskRunEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
TaskId = _taskId,
|
|
RunNumber = 1,
|
|
SessionId = "sess-abc",
|
|
IsRetry = false,
|
|
Prompt = "Fix the bug",
|
|
ResultMarkdown = "All done",
|
|
StructuredOutputJson = """{"ok":true}""",
|
|
ErrorMarkdown = null,
|
|
ExitCode = 0,
|
|
TurnCount = 5,
|
|
TokensIn = 1000,
|
|
TokensOut = 2000,
|
|
LogPath = "/tmp/run1.ndjson",
|
|
StartedAt = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
|
FinishedAt = new DateTime(2026, 1, 1, 0, 5, 0, DateTimeKind.Utc),
|
|
};
|
|
|
|
await _runs.AddAsync(entity);
|
|
var loaded = await _runs.GetByIdAsync(entity.Id);
|
|
|
|
Assert.NotNull(loaded);
|
|
Assert.Equal(entity.Id, loaded.Id);
|
|
Assert.Equal(entity.TaskId, loaded.TaskId);
|
|
Assert.Equal(entity.RunNumber, loaded.RunNumber);
|
|
Assert.Equal(entity.SessionId, loaded.SessionId);
|
|
Assert.Equal(entity.IsRetry, loaded.IsRetry);
|
|
Assert.Equal(entity.Prompt, loaded.Prompt);
|
|
Assert.Equal(entity.ResultMarkdown, loaded.ResultMarkdown);
|
|
Assert.Equal(entity.StructuredOutputJson, loaded.StructuredOutputJson);
|
|
Assert.Null(loaded.ErrorMarkdown);
|
|
Assert.Equal(entity.ExitCode, loaded.ExitCode);
|
|
Assert.Equal(entity.TurnCount, loaded.TurnCount);
|
|
Assert.Equal(entity.TokensIn, loaded.TokensIn);
|
|
Assert.Equal(entity.TokensOut, loaded.TokensOut);
|
|
Assert.Equal(entity.LogPath, loaded.LogPath);
|
|
Assert.Equal(entity.StartedAt, loaded.StartedAt);
|
|
Assert.Equal(entity.FinishedAt, loaded.FinishedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByTaskId_Returns_Ordered_By_RunNumber()
|
|
{
|
|
var run3 = MakeRun(3);
|
|
var run1 = MakeRun(1);
|
|
var run2 = MakeRun(2);
|
|
|
|
await _runs.AddAsync(run3);
|
|
await _runs.AddAsync(run1);
|
|
await _runs.AddAsync(run2);
|
|
|
|
var runs = await _runs.GetByTaskIdAsync(_taskId);
|
|
|
|
Assert.Equal(3, runs.Count);
|
|
Assert.Equal(1, runs[0].RunNumber);
|
|
Assert.Equal(2, runs[1].RunNumber);
|
|
Assert.Equal(3, runs[2].RunNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLatestByTaskId_Returns_Highest_RunNumber()
|
|
{
|
|
var run1 = MakeRun(1);
|
|
var run2 = MakeRun(2);
|
|
|
|
await _runs.AddAsync(run1);
|
|
await _runs.AddAsync(run2);
|
|
|
|
var latest = await _runs.GetLatestByTaskIdAsync(_taskId);
|
|
|
|
Assert.NotNull(latest);
|
|
Assert.Equal(run2.Id, latest.Id);
|
|
Assert.Equal(2, latest.RunNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_Persists_Completion_Fields()
|
|
{
|
|
var run = MakeRun(1);
|
|
await _runs.AddAsync(run);
|
|
|
|
run.SessionId = "sess-xyz";
|
|
run.ResultMarkdown = "Task completed";
|
|
run.StructuredOutputJson = """{"status":"done"}""";
|
|
run.ErrorMarkdown = null;
|
|
run.ExitCode = 0;
|
|
run.TurnCount = 12;
|
|
run.TokensIn = 5000;
|
|
run.TokensOut = 8000;
|
|
run.FinishedAt = new DateTime(2026, 6, 1, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
await _runs.UpdateAsync(run);
|
|
var loaded = await _runs.GetByIdAsync(run.Id);
|
|
|
|
Assert.NotNull(loaded);
|
|
Assert.Equal("sess-xyz", loaded.SessionId);
|
|
Assert.Equal("Task completed", loaded.ResultMarkdown);
|
|
Assert.Equal("""{"status":"done"}""", loaded.StructuredOutputJson);
|
|
Assert.Null(loaded.ErrorMarkdown);
|
|
Assert.Equal(0, loaded.ExitCode);
|
|
Assert.Equal(12, loaded.TurnCount);
|
|
Assert.Equal(5000, loaded.TokensIn);
|
|
Assert.Equal(8000, loaded.TokensOut);
|
|
Assert.Equal(new DateTime(2026, 6, 1, 12, 0, 0, DateTimeKind.Utc), loaded.FinishedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetLatestByTaskId_Returns_Null_When_No_Runs()
|
|
{
|
|
var latest = await _runs.GetLatestByTaskIdAsync(Guid.NewGuid().ToString());
|
|
|
|
Assert.Null(latest);
|
|
}
|
|
}
|