feat(ui): planning commands and expand/collapse in TasksIslandViewModel

- Add IWorkerClient interface; WorkerClient implements it
- TasksIslandViewModel accepts IWorkerClient? and gains OpenPlanningSession,
  ResumePlanningSession, DiscardPlanningSession, FinalizePlanningSession,
  and ToggleExpand commands
- Regroup() is hierarchy-aware: children of collapsed planning parents are hidden
- InternalsVisibleTo ClaudeDo.Worker.Tests for Regroup()
- 4 new unit tests covering collapse/expand and guard logic

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-23 18:51:22 +02:00
parent 00608401aa
commit 309f84b388
5 changed files with 219 additions and 5 deletions

View File

@@ -0,0 +1,129 @@
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.UiVm;
// ── Fake worker client ────────────────────────────────────────────────────────
sealed class FakeWorkerClient : IWorkerClient
{
public int StartPlanningCalls { get; private set; }
public int ResumePlanningCalls { get; private set; }
public int DiscardPlanningCalls { get; private set; }
public int FinalizePlanningCalls { get; private set; }
public int WakeQueueCalls { get; private set; }
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) { DiscardPlanningCalls++; return Task.CompletedTask; }
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
}
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
file static class VmFactory
{
// Minimal SQLite :memory: factory — never actually called in these tests
// (we seed Items directly), but required by the VM constructor.
private static IDbContextFactory<ClaudeDoDbContext> NullDbFactory()
{
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite("DataSource=:memory:")
.Options;
return new NullDbContextFactory(opts);
}
private sealed class NullDbContextFactory(DbContextOptions<ClaudeDoDbContext> opts)
: IDbContextFactory<ClaudeDoDbContext>
{
public ClaudeDoDbContext CreateDbContext() => new(opts);
}
public static (TasksIslandViewModel vm, FakeWorkerClient worker) Create(
IEnumerable<TaskRowViewModel> rows)
{
var worker = new FakeWorkerClient();
var vm = new TasksIslandViewModel(NullDbFactory(), worker);
foreach (var r in rows)
vm.Items.Add(r);
vm.Regroup();
return (vm, worker);
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
public class TasksIslandViewModelPlanningTests
{
private static TaskRowViewModel MakeRow(string id, TaskStatus status, string? parentId = null, int sortOrder = 0)
=> new TaskRowViewModel { Id = id, Status = status, ParentTaskId = parentId };
[Fact]
public void ToggleExpand_CollapsesChildrenOfPlanningParent()
{
var parent = MakeRow("p1", TaskStatus.Planning);
var child1 = MakeRow("c1", TaskStatus.Draft, "p1");
var child2 = MakeRow("c2", TaskStatus.Draft, "p1");
var (vm, _) = VmFactory.Create([parent, child1, child2]);
// Initially expanded — children visible in OpenItems
Assert.Contains(child1, vm.OpenItems);
Assert.Contains(child2, vm.OpenItems);
// Collapse the parent
vm.ToggleExpandCommand.Execute(parent);
// Children should no longer appear
Assert.DoesNotContain(child1, vm.OpenItems);
Assert.DoesNotContain(child2, vm.OpenItems);
// Parent still present
Assert.Contains(parent, vm.OpenItems);
}
[Fact]
public async Task OpenPlanningSession_IgnoresNonManualRow()
{
var row = MakeRow("t1", TaskStatus.Queued);
var (vm, worker) = VmFactory.Create([row]);
await ((IAsyncRelayCommand<TaskRowViewModel?>)vm.OpenPlanningSessionCommand).ExecuteAsync(row);
Assert.Equal(0, worker.StartPlanningCalls);
}
[Fact]
public async Task OpenPlanningSession_CallsWorkerForManualRow()
{
var row = MakeRow("t1", TaskStatus.Manual);
var (vm, worker) = VmFactory.Create([row]);
await ((IAsyncRelayCommand<TaskRowViewModel?>)vm.OpenPlanningSessionCommand).ExecuteAsync(row);
Assert.Equal(1, worker.StartPlanningCalls);
}
[Fact]
public void ToggleExpand_ExpandsCollapsedParentAgain()
{
var parent = MakeRow("p1", TaskStatus.Planned);
var child = MakeRow("c1", TaskStatus.Draft, "p1");
var (vm, _) = VmFactory.Create([parent, child]);
// Collapse
vm.ToggleExpandCommand.Execute(parent);
Assert.DoesNotContain(child, vm.OpenItems);
// Re-expand
vm.ToggleExpandCommand.Execute(parent);
Assert.Contains(child, vm.OpenItems);
}
}