Merge claudedo/107e62d083e44436b9074c16d14e2edc
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using ClaudeDo.Worker.Git;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Git;
|
||||
|
||||
public class WorktreeRootResolverTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveSiblingRoot_NoTrailingSeparator_ReturnsParent()
|
||||
{
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo_no_sep");
|
||||
|
||||
var root = WorktreeRootResolver.ResolveSiblingRoot(workingDir);
|
||||
|
||||
Assert.Equal(Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar), root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSiblingRoot_WithTrailingSeparator_StillReturnsParent()
|
||||
{
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo_with_sep") + Path.DirectorySeparatorChar;
|
||||
|
||||
var root = WorktreeRootResolver.ResolveSiblingRoot(workingDir);
|
||||
|
||||
Assert.Equal(Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar), root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSiblingRoot_WithTrailingAltSeparator_StillReturnsParent()
|
||||
{
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo_with_alt_sep") + "/";
|
||||
|
||||
var root = WorktreeRootResolver.ResolveSiblingRoot(workingDir);
|
||||
|
||||
Assert.Equal(Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar), root);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveSiblingRoot_DriveRoot_Throws()
|
||||
{
|
||||
var driveRoot = Path.GetPathRoot(Path.GetTempPath())!;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => WorktreeRootResolver.ResolveSiblingRoot(driveRoot));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureOutsideWorkingDir_PathOutside_DoesNotThrow()
|
||||
{
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo");
|
||||
var worktreePath = Path.Combine(Path.GetTempPath(), ".claudedo-worktrees", "slug", "task1");
|
||||
|
||||
WorktreeRootResolver.EnsureOutsideWorkingDir(worktreePath, workingDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureOutsideWorkingDir_PathInsideWorkingDir_Throws()
|
||||
{
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo");
|
||||
var worktreePath = Path.Combine(workingDir, ".claudedo-worktrees", "slug", "task1");
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => WorktreeRootResolver.EnsureOutsideWorkingDir(worktreePath, workingDir));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureOutsideWorkingDir_PathEqualsWorkingDir_Throws()
|
||||
{
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo");
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => WorktreeRootResolver.EnsureOutsideWorkingDir(workingDir, workingDir));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureOutsideWorkingDir_SiblingWithSharedPrefix_DoesNotThrow()
|
||||
{
|
||||
// "cd_repo-other" is NOT under "cd_repo" even though it shares a string prefix;
|
||||
// a naive StartsWith(workingDir) check would incorrectly reject this.
|
||||
var workingDir = Path.Combine(Path.GetTempPath(), "cd_repo");
|
||||
var worktreePath = Path.Combine(Path.GetTempPath(), "cd_repo-other", "task1");
|
||||
|
||||
WorktreeRootResolver.EnsureOutsideWorkingDir(worktreePath, workingDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Lifecycle;
|
||||
|
||||
public sealed class LegacyWorktreeFolderRecoveryTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
private readonly List<string> _tempDirs = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
foreach (var d in _tempDirs)
|
||||
{
|
||||
try { Directory.Delete(d, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private string MakeWorkingDir()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"claudedo_legacy_wt_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
_tempDirs.Add(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAffectedListNames_FolderInsideWorkingDir_ReturnsListName()
|
||||
{
|
||||
var wd = MakeWorkingDir();
|
||||
Directory.CreateDirectory(Path.Combine(wd, ".claudedo-worktrees"));
|
||||
var list = new ListEntity { Id = "1", Name = "Bandel.Hub", WorkingDir = wd, CreatedAt = DateTime.UtcNow };
|
||||
|
||||
var affected = LegacyWorktreeFolderRecovery.FindAffectedListNames(new[] { list });
|
||||
|
||||
Assert.Equal(new[] { "Bandel.Hub" }, affected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAffectedListNames_NoLegacyFolder_ReturnsEmpty()
|
||||
{
|
||||
var wd = MakeWorkingDir();
|
||||
var list = new ListEntity { Id = "1", Name = "Clean", WorkingDir = wd, CreatedAt = DateTime.UtcNow };
|
||||
|
||||
var affected = LegacyWorktreeFolderRecovery.FindAffectedListNames(new[] { list });
|
||||
|
||||
Assert.Empty(affected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindAffectedListNames_NullWorkingDir_IsSkipped()
|
||||
{
|
||||
var list = new ListEntity { Id = "1", Name = "NoRepo", WorkingDir = null, CreatedAt = DateTime.UtcNow };
|
||||
|
||||
var affected = LegacyWorktreeFolderRecovery.FindAffectedListNames(new[] { list });
|
||||
|
||||
Assert.Empty(affected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_NoLists_DoesNotThrow()
|
||||
{
|
||||
var sut = new LegacyWorktreeFolderRecovery(_db.CreateFactory(), NullLogger<LegacyWorktreeFolderRecovery>.Instance);
|
||||
|
||||
await sut.StartAsync(CancellationToken.None);
|
||||
await sut.StopAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,30 @@ public sealed class PlanningSessionManagerTests : IDisposable
|
||||
Assert.NotNull(loaded.PlanningSessionToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_TrailingSeparatorOnWorkingDir_PlacesWorktreeOutsideRepo()
|
||||
{
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
var wd = Path.Combine(Path.GetTempPath(), $"cd_wd_{Guid.NewGuid():N}");
|
||||
GitRepoFixture.InitRepoWithInitialCommit(wd);
|
||||
await _lists.AddAsync(new ListEntity
|
||||
{
|
||||
Id = listId,
|
||||
Name = "Test",
|
||||
WorkingDir = wd + Path.DirectorySeparatorChar,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
var parent = await SeedManualTaskAsync(listId);
|
||||
|
||||
var ctx = await _sut.StartAsync(parent.Id, CancellationToken.None);
|
||||
|
||||
var wdFull = Path.GetFullPath(wd);
|
||||
Assert.False(
|
||||
ctx.WorktreePath.StartsWith(wdFull + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase),
|
||||
$"worktree path {ctx.WorktreePath} should not be nested inside the repo dir {wdFull}");
|
||||
Assert.True(Directory.Exists(ctx.WorktreePath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_TaskNotManual_Throws()
|
||||
{
|
||||
|
||||
@@ -189,6 +189,26 @@ public class WorktreeManagerTests : IDisposable
|
||||
Assert.DoesNotContain(task.Id, worktreeList);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_TrailingSeparatorOnWorkingDir_PlacesWorktreeOutsideRepo()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = CreateRepo();
|
||||
var workingDirWithSeparator = repo.RepoDir + Path.DirectorySeparatorChar;
|
||||
var (task, list) = MakeEntities(workingDirWithSeparator);
|
||||
var (mgr, db) = await CreateManagerAsync(task, list);
|
||||
|
||||
var ctx = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, ctx.WorktreePath));
|
||||
|
||||
var repoDirFull = Path.GetFullPath(repo.RepoDir);
|
||||
Assert.False(
|
||||
ctx.WorktreePath.StartsWith(repoDirFull + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase),
|
||||
$"worktree path {ctx.WorktreePath} should not be nested inside the repo dir {repoDirFull}");
|
||||
Assert.True(Directory.Exists(ctx.WorktreePath));
|
||||
}
|
||||
|
||||
private static (TaskEntity task, ListEntity list) MakeEntities(string workingDir)
|
||||
{
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
|
||||
Reference in New Issue
Block a user