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; // Deleting a row with a live ConPTY session would orphan the claude process in Mission Control // (WorkerHub.DeleteTask only blocks Status == Running, an interactive session stays Idle) — the // row command must gate on HasInteractiveSession the same way SendToQueue already does. Separately, // removing the selected row (in-UI delete or an externally-driven delta removal) must clear // SelectedTask, otherwise the detail pane keeps showing a deleted task with live buttons. public class TasksIslandDeleteGuardTests : IDisposable { private readonly string _dbPath; public TasksIslandDeleteGuardTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_delete_guard_{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() .UseSqlite($"Data Source={_dbPath}") .Options; return new ClaudeDoDbContext(opts); } private sealed class RecordingWorkerClient : StubWorkerClient { public override bool IsConnected => true; public string? DeletedTaskId { get; private set; } public override Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId) { DeletedTaskId = taskId; return Task.FromResult<(bool, string?)>((true, null)); } } [Fact] public async Task DeleteTaskCommand_RowHasInteractiveSession_NoOpAndNoHubCall() { var worker = new RecordingWorkerClient(); var vm = new TasksIslandViewModel(new TestDbFactory(NewContext), worker); var row = new TaskRowViewModel { Id = "T1", Status = TaskStatus.Idle, HasInteractiveSession = true }; vm.Items.Add(row); await vm.DeleteTaskCommand.ExecuteAsync(row); Assert.Null(worker.DeletedTaskId); Assert.Contains(row, vm.Items); } [Fact] public async Task DeleteTaskCommand_RemovesSelectedRow_ClearsSelection() { var worker = new RecordingWorkerClient(); var vm = new TasksIslandViewModel(new TestDbFactory(NewContext), worker); var row = new TaskRowViewModel { Id = "T2", Status = TaskStatus.Idle }; vm.Items.Add(row); vm.SelectFrom(row, "test"); await vm.DeleteTaskCommand.ExecuteAsync(row); Assert.Equal("T2", worker.DeletedTaskId); Assert.DoesNotContain(row, vm.Items); Assert.Null(vm.SelectedTask); } [Fact] public async Task DeltaRemoval_OfSelectedRow_ClearsSelection() { await using (var db = NewContext()) { db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow }); db.Tasks.Add(new TaskEntity { Number = TestTaskNumbers.Next(), Id = "T3", ListId = "L1", Title = "Deleted elsewhere", Status = TaskStatus.Idle, CreatedAt = DateTime.UtcNow, SortOrder = 0, }); await db.SaveChangesAsync(); } var worker = new RecordingWorkerClient(); var vm = new TasksIslandViewModel(new TestDbFactory(NewContext), worker); vm.LoadForList(new ListNavItemViewModel { Id = "user:L1", Name = "Work", Kind = ListKind.User }); if (vm.LoadTask is { } lt) await lt; Assert.True(await vm.SelectByIdAsync("T3")); Assert.NotNull(vm.SelectedTask); await using (var db = NewContext()) { db.Tasks.Remove(await db.Tasks.FirstAsync(t => t.Id == "T3")); await db.SaveChangesAsync(); } worker.RaiseTaskUpdated("T3"); var deadline = DateTime.UtcNow.AddSeconds(3); while (DateTime.UtcNow < deadline && vm.Items.Any(r => r.Id == "T3")) await Task.Delay(25); Assert.DoesNotContain(vm.Items, r => r.Id == "T3"); Assert.Null(vm.SelectedTask); } }