chore(claude-do): merge [A4] WeeklyReport, DailyPrep und Planning-Aktionen auf Opera
ClaudeDo-Task: 7c6f2f66-8cd7-4abb-8151-aed42ae739b5
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class PrepPanelViewModelTests
|
||||
{
|
||||
private sealed class Worker : StubWorkerClient
|
||||
{
|
||||
public TaskCompletionSource<bool> Gate { get; } = new();
|
||||
public bool Throw { get; set; }
|
||||
|
||||
public override Task<bool> RunDailyPrepNowAsync() =>
|
||||
Throw ? Task.FromException<bool>(new InvalidOperationException("boom")) : Gate.Task;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlanDay_sets_PrepOperation_IsRunning_before_the_started_broadcast_arrives()
|
||||
{
|
||||
var worker = new Worker();
|
||||
var vm = new PrepPanelViewModel(worker);
|
||||
|
||||
var plan = vm.PlanDayCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.True(vm.PrepOperation.IsRunning);
|
||||
Assert.False(vm.IsPrepRunning); // the broadcast hasn't fired yet
|
||||
|
||||
worker.Gate.SetResult(true);
|
||||
await plan;
|
||||
|
||||
Assert.False(vm.PrepOperation.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlanDayCommand_CanExecute_is_locked_while_running()
|
||||
{
|
||||
var worker = new Worker();
|
||||
var vm = new PrepPanelViewModel(worker);
|
||||
|
||||
var plan = vm.PlanDayCommand.ExecuteAsync(null);
|
||||
Assert.False(vm.PlanDayCommand.CanExecute(null));
|
||||
Assert.False(vm.IsPlanDayEnabled);
|
||||
|
||||
worker.Gate.SetResult(true);
|
||||
await plan;
|
||||
|
||||
Assert.True(vm.PlanDayCommand.CanExecute(null));
|
||||
Assert.True(vm.IsPlanDayEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlanDay_ends_the_operation_when_the_worker_call_throws()
|
||||
{
|
||||
var worker = new Worker { Throw = true };
|
||||
var vm = new PrepPanelViewModel(worker);
|
||||
|
||||
await vm.PlanDayCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(vm.PrepOperation.IsRunning);
|
||||
Assert.True(vm.PlanDayCommand.CanExecute(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
// PlanningOperation backs the shared feedback surface for the two planning actions triggered
|
||||
// from the task-row context menu (which closes the instant a click lands, so there is nothing
|
||||
// per-row left to anchor an indicator to) — see docs/superpowers/plans/2026-08-11-operation-feedback.md, A4.
|
||||
public class TasksIslandPlanningOperationTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public TasksIslandPlanningOperationTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_planning_op_{Guid.NewGuid():N}.db");
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_dbPath); } catch { }
|
||||
try { File.Delete(_dbPath + "-wal"); } catch { }
|
||||
try { File.Delete(_dbPath + "-shm"); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(opts);
|
||||
}
|
||||
|
||||
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||
}
|
||||
|
||||
private sealed class GatedWorkerClient : StubWorkerClient
|
||||
{
|
||||
public override bool IsConnected => true;
|
||||
public TaskCompletionSource<object?> QueueGate { get; } = new();
|
||||
public TaskCompletionSource<object?> FinalizeGate { get; } = new();
|
||||
public bool ThrowOnQueue { get; set; }
|
||||
public bool ThrowOnFinalize { get; set; }
|
||||
|
||||
public override async Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default)
|
||||
{
|
||||
if (ThrowOnQueue) throw new InvalidOperationException("boom");
|
||||
await QueueGate.Task;
|
||||
}
|
||||
|
||||
public override async Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default)
|
||||
{
|
||||
if (ThrowOnFinalize) throw new InvalidOperationException("boom");
|
||||
await FinalizeGate.Task;
|
||||
}
|
||||
}
|
||||
|
||||
private TasksIslandViewModel BuildViewModel(GatedWorkerClient worker) =>
|
||||
new(new TestDbFactory(NewContext), worker);
|
||||
|
||||
private static TaskRowViewModel Row(string id) => new() { Id = id, Status = TaskStatus.Idle };
|
||||
|
||||
[Fact]
|
||||
public async Task QueuePlanningSubtasks_sets_PlanningOperation_IsRunning_while_pending()
|
||||
{
|
||||
var worker = new GatedWorkerClient();
|
||||
var vm = BuildViewModel(worker);
|
||||
var row = Row("parent-1");
|
||||
|
||||
var run = vm.QueuePlanningSubtasksCommand.ExecuteAsync(row);
|
||||
Assert.True(vm.PlanningOperation.IsRunning);
|
||||
|
||||
worker.QueueGate.SetResult(null);
|
||||
await run;
|
||||
|
||||
Assert.False(vm.PlanningOperation.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FinalizePlanningSession_sets_PlanningOperation_IsRunning_while_pending()
|
||||
{
|
||||
var worker = new GatedWorkerClient();
|
||||
var vm = BuildViewModel(worker);
|
||||
var row = Row("parent-2");
|
||||
|
||||
var run = vm.FinalizePlanningSessionCommand.ExecuteAsync(row);
|
||||
Assert.True(vm.PlanningOperation.IsRunning);
|
||||
|
||||
worker.FinalizeGate.SetResult(null);
|
||||
await run;
|
||||
|
||||
Assert.False(vm.PlanningOperation.IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlanningCommands_CanExecute_is_locked_while_either_operation_runs()
|
||||
{
|
||||
var worker = new GatedWorkerClient();
|
||||
var vm = BuildViewModel(worker);
|
||||
var row = Row("parent-3");
|
||||
|
||||
var run = vm.QueuePlanningSubtasksCommand.ExecuteAsync(row);
|
||||
Assert.False(vm.QueuePlanningSubtasksCommand.CanExecute(row));
|
||||
Assert.False(vm.FinalizePlanningSessionCommand.CanExecute(row));
|
||||
|
||||
worker.QueueGate.SetResult(null);
|
||||
await run;
|
||||
|
||||
Assert.True(vm.QueuePlanningSubtasksCommand.CanExecute(row));
|
||||
Assert.True(vm.FinalizePlanningSessionCommand.CanExecute(row));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueuePlanningSubtasks_ends_the_operation_when_the_worker_call_throws()
|
||||
{
|
||||
var worker = new GatedWorkerClient { ThrowOnQueue = true };
|
||||
var vm = BuildViewModel(worker);
|
||||
var row = Row("parent-4");
|
||||
|
||||
await vm.QueuePlanningSubtasksCommand.ExecuteAsync(row);
|
||||
|
||||
Assert.False(vm.PlanningOperation.IsRunning);
|
||||
Assert.True(vm.QueuePlanningSubtasksCommand.CanExecute(row));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class WeeklyReportModalViewModelTests
|
||||
{
|
||||
private sealed class Worker : StubWorkerClient
|
||||
{
|
||||
public TaskCompletionSource<string> Gate { get; } = new();
|
||||
public bool Throw { get; set; }
|
||||
|
||||
public override Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end) =>
|
||||
Throw ? Task.FromException<string>(new InvalidOperationException("boom")) : Gate.Task;
|
||||
}
|
||||
|
||||
private static WeeklyReportModalViewModel Build(Worker worker)
|
||||
{
|
||||
var vm = new WeeklyReportModalViewModel(worker)
|
||||
{
|
||||
StartDate = new DateTime(2026, 6, 1),
|
||||
EndDate = new DateTime(2026, 6, 3),
|
||||
};
|
||||
return vm;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_sets_IsRunning_while_pending_and_clears_it_after()
|
||||
{
|
||||
var worker = new Worker();
|
||||
var vm = Build(worker);
|
||||
|
||||
var generate = vm.GenerateCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.True(vm.GenerateOperation.IsRunning);
|
||||
|
||||
worker.Gate.SetResult("# report");
|
||||
await generate;
|
||||
|
||||
Assert.False(vm.GenerateOperation.IsRunning);
|
||||
Assert.Equal("# report", vm.ReportMarkdown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateCommand_CanExecute_is_locked_while_running()
|
||||
{
|
||||
var worker = new Worker();
|
||||
var vm = Build(worker);
|
||||
|
||||
var generate = vm.GenerateCommand.ExecuteAsync(null);
|
||||
Assert.False(vm.GenerateCommand.CanExecute(null));
|
||||
|
||||
worker.Gate.SetResult("# report");
|
||||
await generate;
|
||||
|
||||
Assert.True(vm.GenerateCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Generate_ends_the_operation_when_the_worker_call_throws()
|
||||
{
|
||||
var worker = new Worker { Throw = true };
|
||||
var vm = Build(worker);
|
||||
|
||||
await vm.GenerateCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.False(vm.GenerateOperation.IsRunning);
|
||||
Assert.True(vm.GenerateCommand.CanExecute(null));
|
||||
Assert.False(string.IsNullOrWhiteSpace(vm.StatusMessage));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user