feat(worker): Online Inbox sync engine (Phase 1)
Optional, opt-in (online_inbox.enabled, default false → zero network). Worker-side reconcile loop: pull web-created tasks down as Idle, push the list catalog and the Idle backlog mirror up. Auth behind IOnlineAuthProvider (StaticTokenAuthProvider default; ZitadelAuthProvider stubbed for Phase 2). DPAPI refresh-token store. 35 tests, no real network/Zitadel/Claude. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
167
tests/ClaudeDo.Worker.Tests/Online/OnlineBacklogTests.cs
Normal file
167
tests/ClaudeDo.Worker.Tests/Online/OnlineBacklogTests.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Online;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Online;
|
||||
|
||||
public sealed class OnlineBacklogTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
private readonly ClaudeDoDbContext _ctx;
|
||||
private readonly TaskRepository _tasks;
|
||||
private readonly ListRepository _lists;
|
||||
private string _listId = null!;
|
||||
|
||||
public OnlineBacklogTests()
|
||||
{
|
||||
_ctx = _db.CreateContext();
|
||||
_tasks = new TaskRepository(_ctx);
|
||||
_lists = new ListRepository(_ctx);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ctx.Dispose();
|
||||
_db.Dispose();
|
||||
}
|
||||
|
||||
private async Task SeedListAsync()
|
||||
{
|
||||
_listId = Guid.NewGuid().ToString();
|
||||
await _lists.AddAsync(new ListEntity { Id = _listId, Name = "Test", CreatedAt = DateTime.UtcNow });
|
||||
}
|
||||
|
||||
private TaskEntity Make(
|
||||
TaskStatus status = TaskStatus.Idle,
|
||||
string? parentId = null,
|
||||
PlanningPhase planning = PlanningPhase.None,
|
||||
string? blockedById = null) => new()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
ListId = _listId,
|
||||
Title = "T",
|
||||
Status = status,
|
||||
ParentTaskId = parentId,
|
||||
PlanningPhase = planning,
|
||||
BlockedByTaskId = blockedById,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task CurrentAsync_Returns_OnlyIdleBacklogItems()
|
||||
{
|
||||
await SeedListAsync();
|
||||
|
||||
var idle = Make(TaskStatus.Idle);
|
||||
var queued = Make(TaskStatus.Queued);
|
||||
var running = Make(TaskStatus.Running);
|
||||
var done = Make(TaskStatus.Done);
|
||||
var failed = Make(TaskStatus.Failed);
|
||||
|
||||
// Idle but with parent → planning child
|
||||
var parent = Make(TaskStatus.Idle);
|
||||
await _tasks.AddAsync(parent);
|
||||
var child = Make(TaskStatus.Idle, parentId: parent.Id);
|
||||
|
||||
// Idle but with PlanningPhase
|
||||
var planningParent = Make(TaskStatus.Idle, planning: PlanningPhase.Active);
|
||||
|
||||
// Idle but blocked
|
||||
await _tasks.AddAsync(idle);
|
||||
var blocker = Make(TaskStatus.Idle);
|
||||
await _tasks.AddAsync(blocker);
|
||||
var blocked = Make(TaskStatus.Idle, blockedById: blocker.Id);
|
||||
|
||||
await _tasks.AddAsync(queued);
|
||||
await _tasks.AddAsync(running);
|
||||
await _tasks.AddAsync(done);
|
||||
await _tasks.AddAsync(failed);
|
||||
await _tasks.AddAsync(child);
|
||||
await _tasks.AddAsync(planningParent);
|
||||
await _tasks.AddAsync(blocked);
|
||||
|
||||
var mirror = await OnlineBacklog.CurrentAsync(_tasks);
|
||||
|
||||
// Only the plain idle task and the blocker (which itself is plain idle) should appear
|
||||
var ids = mirror.Select(m => m.Id).ToHashSet();
|
||||
Assert.Contains(idle.Id, ids);
|
||||
Assert.Contains(blocker.Id, ids);
|
||||
Assert.Contains(parent.Id, ids); // parent with no parent itself is a backlog item
|
||||
|
||||
Assert.DoesNotContain(queued.Id, ids);
|
||||
Assert.DoesNotContain(running.Id, ids);
|
||||
Assert.DoesNotContain(done.Id, ids);
|
||||
Assert.DoesNotContain(failed.Id, ids);
|
||||
Assert.DoesNotContain(child.Id, ids);
|
||||
Assert.DoesNotContain(planningParent.Id, ids);
|
||||
Assert.DoesNotContain(blocked.Id, ids);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsBacklogItem_Predicate_FiltersCorrectly()
|
||||
{
|
||||
Assert.True(OnlineBacklog.IsBacklogItem(new TaskEntity
|
||||
{
|
||||
Id = "1", ListId = "l", Title = "T",
|
||||
Status = TaskStatus.Idle, ParentTaskId = null,
|
||||
PlanningPhase = PlanningPhase.None, BlockedByTaskId = null,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}));
|
||||
Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity
|
||||
{
|
||||
Id = "2", ListId = "l", Title = "T",
|
||||
Status = TaskStatus.Queued, ParentTaskId = null,
|
||||
PlanningPhase = PlanningPhase.None, BlockedByTaskId = null,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}));
|
||||
Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity
|
||||
{
|
||||
Id = "3", ListId = "l", Title = "T",
|
||||
Status = TaskStatus.Idle, ParentTaskId = "p",
|
||||
PlanningPhase = PlanningPhase.None, BlockedByTaskId = null,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}));
|
||||
Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity
|
||||
{
|
||||
Id = "4", ListId = "l", Title = "T",
|
||||
Status = TaskStatus.Idle, ParentTaskId = null,
|
||||
PlanningPhase = PlanningPhase.Active, BlockedByTaskId = null,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}));
|
||||
Assert.False(OnlineBacklog.IsBacklogItem(new TaskEntity
|
||||
{
|
||||
Id = "5", ListId = "l", Title = "T",
|
||||
Status = TaskStatus.Idle, ParentTaskId = null,
|
||||
PlanningPhase = PlanningPhase.None, BlockedByTaskId = "b",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CurrentAsync_EmptyDb_ReturnsEmpty()
|
||||
{
|
||||
await SeedListAsync();
|
||||
var mirror = await OnlineBacklog.CurrentAsync(_tasks);
|
||||
Assert.Empty(mirror);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CurrentAsync_MapsFieldsCorrectly()
|
||||
{
|
||||
await SeedListAsync();
|
||||
var task = Make(TaskStatus.Idle);
|
||||
task.Description = "desc";
|
||||
await _tasks.AddAsync(task);
|
||||
|
||||
var mirror = await OnlineBacklog.CurrentAsync(_tasks);
|
||||
|
||||
Assert.Single(mirror);
|
||||
Assert.Equal(task.Id, mirror[0].Id);
|
||||
Assert.Equal(_listId, mirror[0].ListId);
|
||||
Assert.Equal("T", mirror[0].Title);
|
||||
Assert.Equal("desc", mirror[0].Description);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user