using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Modals; using Microsoft.EntityFrameworkCore; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Ui.Tests.Services; // Dedup: five copy-pasted "open this path in the shell" implementations replaced by ShellOpen. // Process.Start must never fire in tests, so only the "nothing to open" paths are covered here — // an existing file/directory would actually launch the OS shell handler. public class ShellOpenTests { private sealed class NoopWorkerClient : StubWorkerClient { } [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void Path_BlankInput_ReturnsFalseWithNoError(string? input) { var (ok, error) = ShellOpen.Path(input); Assert.False(ok); Assert.Null(error); } [Fact] public void Path_NonExistentPath_ReturnsFalseWithNoError_DoesNotThrow() { var missing = System.IO.Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_{Guid.NewGuid():N}"); var (ok, error) = ShellOpen.Path(missing); Assert.False(ok); Assert.Null(error); } [Fact] public void ListsIslandViewModel_OpenInExplorer_NonExistentDir_StaysSilent() { var dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_lists_{Guid.NewGuid():N}.db"); try { var opts = new DbContextOptionsBuilder().UseSqlite($"Data Source={dbPath}").Options; using (var ctx = new ClaudeDoDbContext(opts)) ctx.Database.EnsureCreated(); var factory = new TestDbFactory(() => new ClaudeDoDbContext(opts)); var vm = new ListsIslandViewModel(factory); string? reportedError = null; vm.ErrorReported += msg => reportedError = msg; var row = new ListNavItemViewModel { Id = "L1", Kind = ListKind.User, WorkingDir = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_dir_{Guid.NewGuid():N}"), }; vm.OpenInExplorerCommand.Execute(row); Assert.Null(reportedError); } finally { try { File.Delete(dbPath); } catch { } try { File.Delete(dbPath + "-wal"); } catch { } try { File.Delete(dbPath + "-shm"); } catch { } } } [Fact] public void TasksIslandViewModel_OpenTaskWorktree_NonExistentPath_StaysSilent() { var dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_tasks_{Guid.NewGuid():N}.db"); try { var opts = new DbContextOptionsBuilder().UseSqlite($"Data Source={dbPath}").Options; using (var ctx = new ClaudeDoDbContext(opts)) ctx.Database.EnsureCreated(); var factory = new TestDbFactory(() => new ClaudeDoDbContext(opts)); var vm = new TasksIslandViewModel(factory, new NoopWorkerClient()); string? reportedError = null; vm.ErrorReported += msg => reportedError = msg; var row = new TaskRowViewModel { Id = "task-1", Status = TaskStatus.Idle, WorktreePath = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_wt_{Guid.NewGuid():N}"), }; vm.OpenTaskWorktreeCommand.Execute(row); Assert.Null(reportedError); } finally { try { File.Delete(dbPath); } catch { } try { File.Delete(dbPath + "-wal"); } catch { } try { File.Delete(dbPath + "-shm"); } catch { } } } [Fact] public void WorktreesOverviewModalViewModel_OpenInExplorer_NonExistentPath_StaysSilent() { var vm = new WorktreesOverviewModalViewModel(new NoopWorkerClient(), () => null!, new MergeCoordinator()); string? reportedError = null; vm.ErrorReported += msg => reportedError = msg; var row = new WorktreeOverviewRowViewModel { TaskId = "task-1", TaskTitle = "Task 1", TaskStatus = TaskStatus.Idle, State = WorktreeState.Active, Path = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_row_{Guid.NewGuid():N}"), }; vm.OpenInExplorerCommand.Execute(row); Assert.Null(reportedError); } [Fact] public void AboutModalViewModel_OpenPath_NonExistentPath_StaysSilent() { var vm = new AboutModalViewModel(); string? reportedError = null; vm.ErrorReported += msg => reportedError = msg; var missing = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_about_{Guid.NewGuid():N}"); vm.OpenPathCommand.Execute(missing); Assert.Null(reportedError); } [Fact] public void MergeSectionViewModel_OpenWorktree_NonExistentPath_StaysSilent() { var vm = new MergeSectionViewModel(new NoopWorkerClient(), services: null!); string? reportedError = null; vm.ErrorReported += msg => reportedError = msg; var missing = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_merge_{Guid.NewGuid():N}"); vm.SyncWorktree(missing, null, null, "Active", null); vm.OpenWorktreeCommand.Execute(null); Assert.Null(reportedError); } private sealed class TestDbFactory : IDbContextFactory { private readonly Func _create; public TestDbFactory(Func create) => _create = create; public ClaudeDoDbContext CreateDbContext() => _create(); } }