using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.MissionControl; using Xunit; namespace ClaudeDo.Ui.Tests.ViewModels.MissionControl; public class ConPtyPaneViewModelTests { private static ConPtyPaneViewModel NewTaskPane( Func> descriptorFactory, string taskId = "t1") => new(taskId, "Some Task", descriptorFactory); private static Task NeverCompletes() => new TaskCompletionSource().Task; private static Task Failing(string message = "boom") => Task.FromException(new InvalidOperationException(message)); // ── CanSubmitForReview gating (fix: don't offer Submit for Review on a dead/starting pane) ── [Fact] public void CanSubmitForReview_False_WhileStarting() { using var pane = NewTaskPane(NeverCompletes); pane.Start(); Assert.True(pane.Terminal.IsStarting); Assert.False(pane.SubmitForReviewCommand.CanExecute(null)); } [Fact] public void CanSubmitForReview_False_AfterLaunchFailure() { using var pane = NewTaskPane(() => Failing()); pane.Start(); Assert.NotNull(pane.Terminal.StartError); Assert.True(pane.Terminal.HasExited); Assert.False(pane.SubmitForReviewCommand.CanExecute(null)); } [Fact] public void CanSubmitForReview_True_WhenRunning() { using var pane = NewTaskPane(NeverCompletes); SetRunning(pane); Assert.True(pane.SubmitForReviewCommand.CanExecute(null)); } [Fact] public void CanSubmitForReview_False_ForAdHocPane_EvenWhileRunning() { using var pane = ConPtyPaneViewModel.CreateAdHoc("Ad hoc", NeverCompletes); SetRunning(pane); Assert.False(pane.IsTaskBased); Assert.False(pane.SubmitForReviewCommand.CanExecute(null)); } [Fact] public void CanSubmitForReview_False_WhileSubmitPending() { using var pane = NewTaskPane(NeverCompletes); SetRunning(pane); Assert.True(pane.SubmitForReviewCommand.CanExecute(null)); pane.IsSubmitPending = true; Assert.False(pane.SubmitForReviewCommand.CanExecute(null)); pane.IsSubmitPending = false; Assert.True(pane.SubmitForReviewCommand.CanExecute(null)); } // ── Retry (fix: a failed launch used to permanently occupy the TaskId dedupe slot) ────────── [Fact] public void RetryCommand_Disabled_BeforeAndWhileStarting() { using var pane = NewTaskPane(NeverCompletes); Assert.False(pane.RetryCommand.CanExecute(null)); pane.Start(); Assert.False(pane.RetryCommand.CanExecute(null)); // still starting, no failure yet } [Fact] public void RetryCommand_Enabled_AfterLaunchFailure() { using var pane = NewTaskPane(() => Failing()); pane.Start(); Assert.True(pane.RetryCommand.CanExecute(null)); } [Fact] public void Retry_ReplacesTerminal_AndRefetchesDescriptor() { var callCount = 0; Func> factory = () => { callCount++; return callCount == 1 ? Failing() : NeverCompletes(); }; using var pane = NewTaskPane(factory); pane.Start(); Assert.True(pane.RetryCommand.CanExecute(null)); var terminalBeforeRetry = pane.Terminal; pane.RetryCommand.Execute(null); Assert.Equal(2, callCount); Assert.NotSame(terminalBeforeRetry, pane.Terminal); Assert.Null(pane.Terminal.StartError); Assert.False(pane.Terminal.HasExited); Assert.False(pane.RetryCommand.CanExecute(null)); } [Fact] public void Retry_FailsAgain_StillOffersRetry() { using var pane = NewTaskPane(() => Failing()); pane.Start(); Assert.True(pane.RetryCommand.CanExecute(null)); pane.RetryCommand.Execute(null); Assert.NotNull(pane.Terminal.StartError); Assert.True(pane.RetryCommand.CanExecute(null)); } private static void SetRunning(ConPtyPaneViewModel pane) => pane.Terminal.IsRunning = true; }