344 lines
12 KiB
C#
344 lines
12 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 ResetToManualAsync_ClearsResultFields_AndSetsStatusManual()
|
|
{
|
|
var listId = await CreateListAsync();
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = "T",
|
|
CreatedAt = DateTime.UtcNow,
|
|
Status = TaskStatus.Failed,
|
|
StartedAt = DateTime.UtcNow.AddMinutes(-5),
|
|
FinishedAt = DateTime.UtcNow,
|
|
Result = "boom",
|
|
CommitType = "feat",
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
|
|
await _tasks.ResetToManualAsync(task.Id);
|
|
|
|
using var readCtx = _db.CreateContext();
|
|
var after = await new TaskRepository(readCtx).GetByIdAsync(task.Id);
|
|
Assert.NotNull(after);
|
|
Assert.Equal(TaskStatus.Manual, after!.Status);
|
|
Assert.Null(after.StartedAt);
|
|
Assert.Null(after.FinishedAt);
|
|
Assert.Null(after.Result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AddAsync_AssignsDense_PerList_SortOrder()
|
|
{
|
|
var listA = await CreateListAsync();
|
|
var listB = await CreateListAsync();
|
|
|
|
var a0 = MakeTask(listA); await _tasks.AddAsync(a0);
|
|
var a1 = MakeTask(listA); await _tasks.AddAsync(a1);
|
|
var b0 = MakeTask(listB); await _tasks.AddAsync(b0);
|
|
var a2 = MakeTask(listA); await _tasks.AddAsync(a2);
|
|
|
|
var reloadA0 = await _tasks.GetByIdAsync(a0.Id);
|
|
var reloadA1 = await _tasks.GetByIdAsync(a1.Id);
|
|
var reloadA2 = await _tasks.GetByIdAsync(a2.Id);
|
|
var reloadB0 = await _tasks.GetByIdAsync(b0.Id);
|
|
|
|
Assert.Equal(0, reloadA0!.SortOrder);
|
|
Assert.Equal(1, reloadA1!.SortOrder);
|
|
Assert.Equal(2, reloadA2!.SortOrder);
|
|
Assert.Equal(0, reloadB0!.SortOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetByListIdAsync_OrdersBy_SortOrder()
|
|
{
|
|
var listId = await CreateListAsync();
|
|
var t0 = MakeTask(listId); await _tasks.AddAsync(t0);
|
|
var t1 = MakeTask(listId); await _tasks.AddAsync(t1);
|
|
var t2 = MakeTask(listId); await _tasks.AddAsync(t2);
|
|
|
|
await _tasks.ReorderAsync(listId, new[] { t2.Id, t0.Id, t1.Id });
|
|
|
|
var list = await _tasks.GetByListIdAsync(listId);
|
|
Assert.Equal(new[] { t2.Id, t0.Id, t1.Id }, list.Select(t => t.Id).ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReorderAsync_Renumbers_Dense()
|
|
{
|
|
var listId = await CreateListAsync();
|
|
var t0 = MakeTask(listId); await _tasks.AddAsync(t0);
|
|
var t1 = MakeTask(listId); await _tasks.AddAsync(t1);
|
|
var t2 = MakeTask(listId); await _tasks.AddAsync(t2);
|
|
|
|
await _tasks.ReorderAsync(listId, new[] { t1.Id, t2.Id, t0.Id });
|
|
|
|
var r0 = await _tasks.GetByIdAsync(t0.Id);
|
|
var r1 = await _tasks.GetByIdAsync(t1.Id);
|
|
var r2 = await _tasks.GetByIdAsync(t2.Id);
|
|
|
|
Assert.Equal(2, r0!.SortOrder);
|
|
Assert.Equal(0, r1!.SortOrder);
|
|
Assert.Equal(1, r2!.SortOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReorderAsync_IgnoresIds_FromOtherLists()
|
|
{
|
|
var listA = await CreateListAsync();
|
|
var listB = await CreateListAsync();
|
|
var a0 = MakeTask(listA); await _tasks.AddAsync(a0);
|
|
var b0 = MakeTask(listB); await _tasks.AddAsync(b0);
|
|
|
|
// b0 does not belong to listA and should not be renumbered there.
|
|
await _tasks.ReorderAsync(listA, new[] { b0.Id, a0.Id });
|
|
|
|
var reloadB = await _tasks.GetByIdAsync(b0.Id);
|
|
Assert.Equal(0, reloadB!.SortOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetNextQueuedAgentTaskAsync_Picks_ByUserSortOrder()
|
|
{
|
|
var listId = await CreateListAsync();
|
|
var agentTagId = await _tags.GetOrCreateAsync("agent");
|
|
await _lists.AddTagAsync(listId, agentTagId);
|
|
|
|
// created in order first, second; then user reorders to put second on top.
|
|
var first = MakeTask(listId, createdAt: DateTime.UtcNow.AddMinutes(-10));
|
|
var second = MakeTask(listId, createdAt: DateTime.UtcNow);
|
|
await _tasks.AddAsync(first);
|
|
await _tasks.AddAsync(second);
|
|
|
|
await _tasks.ReorderAsync(listId, new[] { second.Id, first.Id });
|
|
|
|
var picked = await _tasks.GetNextQueuedAgentTaskAsync(DateTime.UtcNow);
|
|
Assert.NotNull(picked);
|
|
Assert.Equal(second.Id, picked!.Id);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|