feat(mission-control): sidebar shows queued+running, no auto-monitor seeding
- TaskStarted no longer creates a monitor pane; Panes stays empty unless a ConPTY session is explicitly opened - SeedActive no longer called on construction or ConnectionRestored - EnsureMonitor / SeedActive kept compiling and functional (internal), just unused - RefreshQueueAsync now loads Queued + Running tasks, Running sorted first - QueuedTaskViewModel gains IsRunning + OpenInAppCommand (IRelayCommand) - Sidebar row is a Button; running rows get RunningTint overlay + "Running" label - HasQueued is true whenever any queued or running task exists - New localization keys: missionControl.running (en + de) - Tests updated: 12 monitor-machinery tests use EnsureMonitor directly; 3 new acceptance-criteria tests (no-auto-pane, running-first sort, row click)
This commit is contained in:
@@ -49,8 +49,10 @@ public class MissionControlViewModelTests : IDisposable
|
||||
private MissionControlViewModel BuildVm(StubWorkerClient worker)
|
||||
=> new MissionControlViewModel(new TestDbFactory(NewContext), worker);
|
||||
|
||||
// ── acceptance criterion (a): TaskStarted must NOT add a pane ──────────────
|
||||
|
||||
[Fact]
|
||||
public void TwoStarts_CreateTwoMonitors_ColumnCountTwo()
|
||||
public void TaskStarted_DoesNotAddPane()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
@@ -58,18 +60,140 @@ public class MissionControlViewModelTests : IDisposable
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
worker.RaiseTaskStarted("slot-2", "t2", DateTime.UtcNow);
|
||||
|
||||
Assert.Empty(vm.Monitors);
|
||||
Assert.Empty(vm.Panes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Construction_DoesNotSeedMonitorsEvenWithActiveTasks()
|
||||
{
|
||||
var worker = new SeededFakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
Assert.Empty(vm.Monitors);
|
||||
Assert.Empty(vm.Panes);
|
||||
}
|
||||
|
||||
private sealed class SeededFakeWorker : StubWorkerClient
|
||||
{
|
||||
public override IReadOnlyList<ActiveTask> GetActiveTasks()
|
||||
=> new[] { new ActiveTask("slot-1", "seed1", DateTime.UtcNow) };
|
||||
}
|
||||
|
||||
// ── acceptance criterion (b): RefreshQueueAsync returns Running rows first ─
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshQueueAsync_RunningFirst_WithIsRunningSet()
|
||||
{
|
||||
await using (var db = NewContext())
|
||||
{
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
||||
// Running task has higher SortOrder — must still appear first
|
||||
db.Tasks.Add(new TaskEntity { Id = "r1", ListId = "L1", Title = "running-task", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 10 });
|
||||
db.Tasks.Add(new TaskEntity { Id = "q1", ListId = "L1", Title = "queued-task", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
await vm.RefreshQueueAsync();
|
||||
|
||||
Assert.True(vm.HasQueued);
|
||||
Assert.Equal(2, vm.Queued.Count);
|
||||
|
||||
Assert.Equal("r1", vm.Queued[0].Id);
|
||||
Assert.True(vm.Queued[0].IsRunning);
|
||||
|
||||
Assert.Equal("q1", vm.Queued[1].Id);
|
||||
Assert.False(vm.Queued[1].IsRunning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasQueued_TrueWhenOnlyRunningTasksExist()
|
||||
{
|
||||
await using (var db = NewContext())
|
||||
{
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
||||
db.Tasks.Add(new TaskEntity { Id = "r1", ListId = "L1", Title = "running", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
await vm.RefreshQueueAsync();
|
||||
|
||||
Assert.True(vm.HasQueued);
|
||||
}
|
||||
|
||||
// ── acceptance criterion (c): row click invokes OpenInApp ──────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task Row_Click_InvokesOpenInApp()
|
||||
{
|
||||
await using (var db = NewContext())
|
||||
{
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
||||
db.Tasks.Add(new TaskEntity { Id = "q1", ListId = "L1", Title = "clickable", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
string? opened = null;
|
||||
vm.OpenInApp = id => opened = id;
|
||||
|
||||
await vm.RefreshQueueAsync();
|
||||
vm.Queued[0].OpenInAppCommand!.Execute(null);
|
||||
|
||||
Assert.Equal("q1", opened);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Row_Click_RunningTask_InvokesOpenInApp()
|
||||
{
|
||||
await using (var db = NewContext())
|
||||
{
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
||||
db.Tasks.Add(new TaskEntity { Id = "r1", ListId = "L1", Title = "running", Status = TaskStatus.Running, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
string? opened = null;
|
||||
vm.OpenInApp = id => opened = id;
|
||||
|
||||
await vm.RefreshQueueAsync();
|
||||
vm.Queued[0].OpenInAppCommand!.Execute(null);
|
||||
|
||||
Assert.Equal("r1", opened);
|
||||
}
|
||||
|
||||
// ── monitor machinery tests (EnsureMonitor still works when called directly) ─
|
||||
|
||||
[Fact]
|
||||
public void EnsureMonitor_TwoCalls_CreateTwoMonitors_ColumnCountTwo()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
vm.EnsureMonitor("t1");
|
||||
vm.EnsureMonitor("t2");
|
||||
|
||||
Assert.Equal(2, vm.Monitors.Count);
|
||||
Assert.Equal(2, vm.ColumnCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DuplicateStart_DoesNotAddSecondMonitor()
|
||||
public void EnsureMonitor_DuplicateCall_DoesNotAddSecondMonitor()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
vm.EnsureMonitor("t1");
|
||||
|
||||
Assert.Equal(1, vm.Monitors.Count);
|
||||
}
|
||||
@@ -80,7 +204,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
||||
|
||||
Assert.Equal(1, vm.Monitors.Count);
|
||||
@@ -93,8 +217,8 @@ public class MissionControlViewModelTests : IDisposable
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
worker.RaiseTaskStarted("slot-2", "t2", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
vm.EnsureMonitor("t2");
|
||||
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
||||
|
||||
vm.ClearFinishedCommand.Execute(null);
|
||||
@@ -104,22 +228,6 @@ public class MissionControlViewModelTests : IDisposable
|
||||
Assert.Equal(1, vm.ColumnCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeedsFromActiveTasksOnConstruction()
|
||||
{
|
||||
var worker = new SeededFakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
Assert.Equal(1, vm.Monitors.Count);
|
||||
Assert.Equal("seed1", vm.Monitors[0].SubscribedTaskId);
|
||||
}
|
||||
|
||||
private sealed class SeededFakeWorker : StubWorkerClient
|
||||
{
|
||||
public override IReadOnlyList<ActiveTask> GetActiveTasks()
|
||||
=> new[] { new ActiveTask("slot-1", "seed1", DateTime.UtcNow) };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenInApp_PropagatesToMonitors_AndCommandInvokesHook()
|
||||
{
|
||||
@@ -129,7 +237,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
string? revealed = null;
|
||||
vm.OpenInApp = id => revealed = id;
|
||||
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
|
||||
vm.Monitors[0].OpenInAppCommand.Execute(null);
|
||||
|
||||
@@ -145,7 +253,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
Action? reDock = null;
|
||||
vm.ShowDetached = (m, rd) => { detached = m; reDock = rd; };
|
||||
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
var monitor = vm.Monitors[0];
|
||||
|
||||
monitor.DetachCommand.Execute(null);
|
||||
@@ -162,7 +270,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
worker.RaiseTaskFinished("slot-1", "t1", "waiting_for_review", DateTime.UtcNow);
|
||||
|
||||
Assert.True(vm.Monitors[0].IsWaitingForReview);
|
||||
@@ -177,7 +285,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
using var vm = BuildVm(worker);
|
||||
Action? reDock = null;
|
||||
vm.ShowDetached = (m, rd) => reDock = rd;
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
var monitor = vm.Monitors[0];
|
||||
|
||||
monitor.DetachCommand.Execute(null);
|
||||
@@ -195,7 +303,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
vm.ShowDetached = (m, rd) => { };
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
var monitor = vm.Monitors[0];
|
||||
|
||||
var closeRequested = false;
|
||||
@@ -210,9 +318,9 @@ public class MissionControlViewModelTests : IDisposable
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
worker.RaiseTaskStarted("s1", "t1", DateTime.UtcNow);
|
||||
worker.RaiseTaskStarted("s2", "t2", DateTime.UtcNow);
|
||||
worker.RaiseTaskStarted("s3", "t3", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
vm.EnsureMonitor("t2");
|
||||
vm.EnsureMonitor("t3");
|
||||
|
||||
vm.MoveMonitor(vm.Monitors[0], vm.Monitors[2]); // move t1 to t3's slot
|
||||
Assert.Equal(new[] { "t2", "t3", "t1" }, vm.Monitors.Select(m => m.SubscribedTaskId).ToArray());
|
||||
@@ -320,7 +428,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
var worker = new FakeWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
worker.RaiseTaskStarted("slot-1", "t1", DateTime.UtcNow);
|
||||
vm.EnsureMonitor("t1");
|
||||
await vm.OpenConPtySessionAsync("t2");
|
||||
|
||||
Assert.Equal(2, vm.Panes.Count);
|
||||
|
||||
Reference in New Issue
Block a user