using ClaudeDo.Data; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Data.Tests; // A list working dir stored with a trailing separator ("C:\repo\") breaks every consumer that // puts it on a Windows command line: the token's closing quote is escaped by the backslash, so // "C:\repo\" parses as C:\repo" and the arg (or every arg after it) is corrupted. Observed twice // -- ConPTY list handlers (2026-08-06) and "Open in terminal" (2026-08-10). Normalizing on write // is the only place that covers all writers (UI, repo import, hub, MCP). public sealed class WorkingDirNormalizationTests : IDisposable { private readonly string _dbPath; private readonly ClaudeDoDbContext _ctx; public WorkingDirNormalizationTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_workdir_{Guid.NewGuid():N}.db"); var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={_dbPath}") .Options; _ctx = new ClaudeDoDbContext(options); _ctx.Database.EnsureCreated(); } public void Dispose() { _ctx.Dispose(); try { File.Delete(_dbPath); } catch { } try { File.Delete(_dbPath + "-wal"); } catch { } try { File.Delete(_dbPath + "-shm"); } catch { } } [Theory] [InlineData(@"C:\Dev\Tests\StaplerTracking\", @"C:\Dev\Tests\StaplerTracking")] [InlineData(@"C:\Dev\Tests\StaplerTracking\\", @"C:\Dev\Tests\StaplerTracking")] [InlineData("C:/Dev/Tests/StaplerTracking/", "C:/Dev/Tests/StaplerTracking")] [InlineData(@"C:\Dev\Tests\StaplerTracking", @"C:\Dev\Tests\StaplerTracking")] public void TrimTrailingSeparator_strips_trailing_separators(string input, string expected) => Assert.Equal(expected, Paths.TrimTrailingSeparator(input)); // A bare drive root IS its trailing separator -- "C:" without it means "current dir on C:", // a different location. Same for the unix root. [Theory] [InlineData(@"C:\")] [InlineData("/")] public void TrimTrailingSeparator_leaves_a_bare_root_alone(string root) => Assert.Equal(root, Paths.TrimTrailingSeparator(root)); [Theory] [InlineData(null)] [InlineData("")] [InlineData(" ")] public void TrimTrailingSeparator_passes_blank_through(string? input) => Assert.Equal(input, Paths.TrimTrailingSeparator(input)); [Fact] public async Task AddAsync_normalizes_a_trailing_separator() { var repo = new ListRepository(_ctx); await repo.AddAsync(new ListEntity { Id = "l1", Name = "Stapler Tracking", WorkingDir = @"C:\Dev\Tests\StaplerTracking\", CreatedAt = DateTime.UtcNow, }); var stored = await repo.GetByIdAsync("l1"); Assert.Equal(@"C:\Dev\Tests\StaplerTracking", stored!.WorkingDir); } [Fact] public async Task UpdateAsync_normalizes_a_trailing_separator() { var repo = new ListRepository(_ctx); await repo.AddAsync(new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow }); var entity = await repo.GetByIdAsync("l1"); entity!.WorkingDir = @"C:\Dev\Repos\Bandel.Hub\"; await repo.UpdateAsync(entity); var stored = await repo.GetByIdAsync("l1"); Assert.Equal(@"C:\Dev\Repos\Bandel.Hub", stored!.WorkingDir); } [Fact] public async Task AddAsync_leaves_a_null_working_dir_null() { var repo = new ListRepository(_ctx); await repo.AddAsync(new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow }); var stored = await repo.GetByIdAsync("l1"); Assert.Null(stored!.WorkingDir); } }