feat(data): add TaskRunRepository with CRUD and query methods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
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 TaskRunRepository _runs;
|
||||
private readonly string _taskId;
|
||||
|
||||
public TaskRunRepositoryTests()
|
||||
{
|
||||
_runs = new TaskRunRepository(_db.Factory);
|
||||
|
||||
// Seed a list and task for all tests
|
||||
var lists = new ListRepository(_db.Factory);
|
||||
var tasks = new TaskRepository(_db.Factory);
|
||||
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() => _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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user