- 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)
593 lines
19 KiB
C#
593 lines
19 KiB
C#
using System.Linq;
|
|
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Ui.Services;
|
|
using ClaudeDo.Ui.ViewModels;
|
|
using ClaudeDo.Ui.ViewModels.Islands;
|
|
using ClaudeDo.Ui.ViewModels.MissionControl;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Xunit;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
|
|
|
public class MissionControlViewModelTests : IDisposable
|
|
{
|
|
private readonly string _dbPath;
|
|
|
|
public MissionControlViewModelTests()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_mc_test_{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 FakeWorker : StubWorkerClient { }
|
|
|
|
private MissionControlViewModel BuildVm(StubWorkerClient worker)
|
|
=> new MissionControlViewModel(new TestDbFactory(NewContext), worker);
|
|
|
|
// ── acceptance criterion (a): TaskStarted must NOT add a pane ──────────────
|
|
|
|
[Fact]
|
|
public void TaskStarted_DoesNotAddPane()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
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 EnsureMonitor_DuplicateCall_DoesNotAddSecondMonitor()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
vm.EnsureMonitor("t1");
|
|
vm.EnsureMonitor("t1");
|
|
|
|
Assert.Equal(1, vm.Monitors.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Finish_KeepsPane_AndFlipsState()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
vm.EnsureMonitor("t1");
|
|
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
|
|
|
Assert.Equal(1, vm.Monitors.Count);
|
|
Assert.True(vm.Monitors[0].IsDone);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClearFinished_RemovesTerminalMonitors()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
vm.EnsureMonitor("t1");
|
|
vm.EnsureMonitor("t2");
|
|
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
|
|
|
vm.ClearFinishedCommand.Execute(null);
|
|
|
|
Assert.Equal(1, vm.Monitors.Count);
|
|
Assert.Equal("t2", vm.Monitors[0].SubscribedTaskId);
|
|
Assert.Equal(1, vm.ColumnCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void OpenInApp_PropagatesToMonitors_AndCommandInvokesHook()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
string? revealed = null;
|
|
vm.OpenInApp = id => revealed = id;
|
|
|
|
vm.EnsureMonitor("t1");
|
|
|
|
vm.Monitors[0].OpenInAppCommand.Execute(null);
|
|
|
|
Assert.Equal("t1", revealed);
|
|
}
|
|
|
|
[Fact]
|
|
public void Detach_RemovesFromGrid_ThenReDockRestores()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
TaskMonitorViewModel? detached = null;
|
|
Action? reDock = null;
|
|
vm.ShowDetached = (m, rd) => { detached = m; reDock = rd; };
|
|
|
|
vm.EnsureMonitor("t1");
|
|
var monitor = vm.Monitors[0];
|
|
|
|
monitor.DetachCommand.Execute(null);
|
|
Assert.Empty(vm.Monitors);
|
|
Assert.Same(monitor, detached);
|
|
|
|
reDock!.Invoke();
|
|
Assert.Single(vm.Monitors);
|
|
Assert.Same(monitor, vm.Monitors[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void ClearFinished_AlsoRemoves_WaitingForReview()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
vm.EnsureMonitor("t1");
|
|
worker.RaiseTaskFinished("slot-1", "t1", "waiting_for_review", DateTime.UtcNow);
|
|
|
|
Assert.True(vm.Monitors[0].IsWaitingForReview);
|
|
vm.ClearFinishedCommand.Execute(null);
|
|
Assert.Empty(vm.Monitors);
|
|
}
|
|
|
|
[Fact]
|
|
public void Detach_SetsIsDetached_AndReDockClearsIt()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
Action? reDock = null;
|
|
vm.ShowDetached = (m, rd) => reDock = rd;
|
|
vm.EnsureMonitor("t1");
|
|
var monitor = vm.Monitors[0];
|
|
|
|
monitor.DetachCommand.Execute(null);
|
|
Assert.True(monitor.IsDetached);
|
|
Assert.Empty(vm.Monitors);
|
|
|
|
reDock!.Invoke();
|
|
Assert.False(monitor.IsDetached);
|
|
Assert.Single(vm.Monitors);
|
|
}
|
|
|
|
[Fact]
|
|
public void DetachCommand_WhenDetached_RequestsWindowClose()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
vm.ShowDetached = (m, rd) => { };
|
|
vm.EnsureMonitor("t1");
|
|
var monitor = vm.Monitors[0];
|
|
|
|
var closeRequested = false;
|
|
monitor.DetachCommand.Execute(null); // detach (IsDetached = true)
|
|
monitor.CloseWindowRequested = () => closeRequested = true;
|
|
monitor.DetachCommand.Execute(null); // now acts as re-dock
|
|
Assert.True(closeRequested);
|
|
}
|
|
|
|
[Fact]
|
|
public void MoveMonitor_ReordersCollection()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
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());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Queue_ReflectsQueuedTasks_InSortOrder()
|
|
{
|
|
await SeedQueueAsync();
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.RefreshQueueAsync();
|
|
|
|
Assert.True(vm.HasQueued);
|
|
Assert.Equal(new[] { "first", "second" }, vm.Queued.Select(q => q.Title).ToArray());
|
|
}
|
|
|
|
private async Task SeedQueueAsync()
|
|
{
|
|
await using var db = NewContext();
|
|
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
|
db.Tasks.Add(new TaskEntity { Id = "q2", ListId = "L1", Title = "second", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 1 });
|
|
db.Tasks.Add(new TaskEntity { Id = "q1", ListId = "L1", Title = "first", Status = TaskStatus.Queued, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
|
db.Tasks.Add(new TaskEntity { Id = "idle1", ListId = "L1", Title = "idle", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 2 });
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EnqueueTaskAsync_SetsTaskQueued_AndShowsInStrip()
|
|
{
|
|
await using (var db = NewContext())
|
|
{
|
|
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
|
db.Tasks.Add(new TaskEntity { Id = "idleTask", ListId = "L1", Title = "Do the thing", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.EnqueueTaskAsync("idleTask");
|
|
|
|
Assert.True(vm.HasQueued);
|
|
Assert.Contains(vm.Queued, q => q.Id == "idleTask");
|
|
|
|
await using var verify = NewContext();
|
|
var entity = await verify.Tasks.FirstAsync(t => t.Id == "idleTask");
|
|
Assert.Equal(TaskStatus.Queued, entity.Status);
|
|
}
|
|
|
|
private sealed class ThrowingLaunchSpecWorker : StubWorkerClient
|
|
{
|
|
public override Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
|
=> throw new InvalidOperationException("no worktree");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenConPtySessionAsync_AddsPane_ToConPtySessionsAndPanes()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.OpenConPtySessionAsync("t1");
|
|
|
|
Assert.Single(vm.ConPtySessions);
|
|
Assert.Equal("t1", vm.ConPtySessions[0].TaskId);
|
|
Assert.Single(vm.Panes);
|
|
Assert.Same(vm.ConPtySessions[0], vm.Panes[0]);
|
|
Assert.True(vm.HasPanes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenConPtySessionAsync_SameTaskTwice_DoesNotDuplicate()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.OpenConPtySessionAsync("t1");
|
|
await vm.OpenConPtySessionAsync("t1");
|
|
|
|
Assert.Single(vm.ConPtySessions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenConPtySessionAsync_WorkerThrows_RaisesErrorReported_PaneShowsFailure()
|
|
{
|
|
var worker = new ThrowingLaunchSpecWorker();
|
|
using var vm = BuildVm(worker);
|
|
string? error = null;
|
|
vm.ErrorReported += msg => error = msg;
|
|
|
|
await vm.OpenConPtySessionAsync("t1");
|
|
|
|
// The tile is shown up-front (spinner) and keeps the failure inline rather than vanishing.
|
|
var pane = Assert.Single(vm.ConPtySessions);
|
|
Assert.NotNull(pane.Terminal.StartError);
|
|
Assert.False(pane.Terminal.IsStarting);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Panes_CombinesMonitorsThenConPtySessions_InOrder()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
vm.EnsureMonitor("t1");
|
|
await vm.OpenConPtySessionAsync("t2");
|
|
|
|
Assert.Equal(2, vm.Panes.Count);
|
|
Assert.IsType<TaskMonitorViewModel>(vm.Panes[0]);
|
|
Assert.IsType<ConPtyPaneViewModel>(vm.Panes[1]);
|
|
Assert.Equal(2, vm.ColumnCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CloseConPtySession_RemovesFromConPtySessionsAndPanes()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
await vm.OpenConPtySessionAsync("t1");
|
|
var pane = vm.ConPtySessions[0];
|
|
|
|
pane.CloseCommand.Execute(null);
|
|
|
|
Assert.Empty(vm.ConPtySessions);
|
|
Assert.Empty(vm.Panes);
|
|
}
|
|
|
|
private sealed class ThrowingAdHocLaunchSpecWorker : StubWorkerClient
|
|
{
|
|
public override Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
|
=> throw new InvalidOperationException("bad directory");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAdHocConPtySessionAsync_AddsPane_TitleFromDirectoryLeaf()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.OpenAdHocConPtySessionAsync(Path.Combine("C:", "Some", "Path", "MyProject"));
|
|
|
|
Assert.Single(vm.ConPtySessions);
|
|
Assert.Null(vm.ConPtySessions[0].TaskId);
|
|
Assert.Equal("MyProject", vm.ConPtySessions[0].DisplayTitle);
|
|
Assert.Single(vm.Panes);
|
|
Assert.Same(vm.ConPtySessions[0], vm.Panes[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAdHocConPtySessionAsync_TwoOpens_NeverDeduped_YieldsTwoPanes()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
var dir = Path.Combine("C:", "Some", "Path");
|
|
await vm.OpenAdHocConPtySessionAsync(dir);
|
|
await vm.OpenAdHocConPtySessionAsync(dir);
|
|
|
|
Assert.Equal(2, vm.ConPtySessions.Count);
|
|
Assert.Equal(2, vm.Panes.Count);
|
|
Assert.All(vm.ConPtySessions, s => Assert.Null(s.TaskId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenAdHocConPtySessionAsync_WorkerThrows_RaisesErrorReported_PaneShowsFailure()
|
|
{
|
|
var worker = new ThrowingAdHocLaunchSpecWorker();
|
|
using var vm = BuildVm(worker);
|
|
string? error = null;
|
|
vm.ErrorReported += msg => error = msg;
|
|
|
|
await vm.OpenAdHocConPtySessionAsync(Path.Combine("C:", "Some", "Path"));
|
|
|
|
var pane = Assert.Single(vm.ConPtySessions);
|
|
Assert.NotNull(pane.Terminal.StartError);
|
|
Assert.False(pane.Terminal.IsStarting);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CloseConPtySession_RemovesAdHocPane_FromConPtySessionsAndPanes()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
await vm.OpenAdHocConPtySessionAsync(Path.Combine("C:", "Some", "Path"));
|
|
var pane = vm.ConPtySessions[0];
|
|
|
|
pane.CloseCommand.Execute(null);
|
|
|
|
Assert.Empty(vm.ConPtySessions);
|
|
Assert.Empty(vm.Panes);
|
|
}
|
|
|
|
private sealed class ThrowingMergeHelperLaunchSpecWorker : StubWorkerClient
|
|
{
|
|
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
|
=> throw new InvalidOperationException("spec failed");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenMergeHelperConPtySessionAsync_AddsAdHocPane()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1", "t2" });
|
|
|
|
Assert.Single(vm.ConPtySessions);
|
|
Assert.Null(vm.ConPtySessions[0].TaskId);
|
|
Assert.Single(vm.Panes);
|
|
Assert.Same(vm.ConPtySessions[0], vm.Panes[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenMergeHelperConPtySessionAsync_TwoRuns_NeverDeduped()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
|
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
|
|
|
Assert.Equal(2, vm.ConPtySessions.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenMergeHelperConPtySessionAsync_EmptySelection_NoPane()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
await vm.OpenMergeHelperConPtySessionAsync("L1", Array.Empty<string>());
|
|
|
|
Assert.Empty(vm.ConPtySessions);
|
|
Assert.Empty(vm.Panes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OpenMergeHelperConPtySessionAsync_WorkerThrows_RaisesErrorReported_PaneShowsFailure()
|
|
{
|
|
var worker = new ThrowingMergeHelperLaunchSpecWorker();
|
|
using var vm = BuildVm(worker);
|
|
string? error = null;
|
|
vm.ErrorReported += msg => error = msg;
|
|
|
|
await vm.OpenMergeHelperConPtySessionAsync("L1", new[] { "t1" });
|
|
|
|
var pane = Assert.Single(vm.ConPtySessions);
|
|
Assert.NotNull(pane.Terminal.StartError);
|
|
Assert.False(pane.Terminal.IsStarting);
|
|
Assert.NotNull(error);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToggleLayoutCommand_FlipsIsFocusMode()
|
|
{
|
|
var worker = new FakeWorker();
|
|
using var vm = BuildVm(worker);
|
|
|
|
Assert.False(vm.IsFocusMode);
|
|
vm.ToggleLayoutCommand.Execute(null);
|
|
Assert.True(vm.IsFocusMode);
|
|
vm.ToggleLayoutCommand.Execute(null);
|
|
Assert.False(vm.IsFocusMode);
|
|
}
|
|
}
|