Files
ClaudeDo/tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs
T
mika kuns b6e5876b07 fix(ui): stop treating git worktrees and submodules as repos
.git as a file (worktrees, submodules) isn't a standalone repo — the
scanner was flooding the import list with worktree entries alongside
their real repo.
2026-09-02 09:59:12 +02:00

175 lines
5.1 KiB
C#

using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.Tests;
public sealed class RepoScannerTests : IDisposable
{
private readonly string _root =
Path.Combine(Path.GetTempPath(), "repo-scan-" + Guid.NewGuid().ToString("N"));
public RepoScannerTests() => Directory.CreateDirectory(_root);
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); } catch { }
}
private string MakeDir(string name)
{
var p = Path.Combine(_root, name);
Directory.CreateDirectory(p);
return p;
}
[Fact]
public void Scan_ReturnsSubfoldersWithGitDirectory()
{
var repo = MakeDir("repo-a");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo-a", result[0].Name);
Assert.Equal(repo, result[0].FullPath);
}
[Fact]
public void Scan_IgnoresWorktrees()
{
var repo = MakeDir("repo-a");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var worktree = MakeDir("repo-a-my-task");
File.WriteAllText(Path.Combine(worktree, ".git"), "gitdir: ../repo-a/.git/worktrees/my-task");
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo-a", result[0].Name);
}
[Fact]
public void Scan_IgnoresPlainFolders()
{
MakeDir("not-a-repo");
var result = RepoScanner.Scan(_root);
Assert.Empty(result);
}
[Fact]
public void Scan_FindsRepoAtDepthTwo()
{
var nested = MakeDir(Path.Combine("outer", "inner"));
Directory.CreateDirectory(Path.Combine(nested, ".git"));
// outer itself has no .git
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("inner", result[0].Name);
Assert.Equal(nested, result[0].FullPath);
}
[Fact]
public void Scan_FindsRepoAtDepthThree()
{
var nested = MakeDir(Path.Combine("customers", "projX", "repo"));
Directory.CreateDirectory(Path.Combine(nested, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo", result[0].Name);
Assert.Equal(nested, result[0].FullPath);
}
[Fact]
public void Scan_DoesNotDescendBelowAFoundRepo()
{
var repo = MakeDir("repo-a");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var submodule = Path.Combine(repo, "vendor", "nested-repo");
Directory.CreateDirectory(submodule);
Directory.CreateDirectory(Path.Combine(submodule, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo-a", result[0].Name);
}
[Fact]
public void Scan_SkipsNodeModules()
{
var nodeModules = MakeDir(Path.Combine("node_modules", "some-package"));
Directory.CreateDirectory(Path.Combine(nodeModules, ".git"));
var result = RepoScanner.Scan(_root);
Assert.Empty(result);
}
[Fact]
public void Scan_ReturnsEmptyForMissingFolder()
{
var result = RepoScanner.Scan(Path.Combine(_root, "does-not-exist"));
Assert.Empty(result);
}
[Fact]
public void Scan_ReturnsEmptyWhenRootIsAReparsePoint()
{
var repo = MakeDir("real-repo");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var junction = Path.Combine(_root, "junction-to-real-repo");
var mklink = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c mklink /J \"{junction}\" \"{repo}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
})!;
mklink.WaitForExit();
Assert.True(Directory.Exists(junction), "Failed to create test junction: " + mklink.StandardError.ReadToEnd());
var result = RepoScanner.Scan(junction);
Assert.Empty(result);
}
[Fact]
#pragma warning disable CA1416 // ACL manipulation is Windows-only; this test targets the Windows dev/CI environment
public void Scan_ContinuesAfterUnreadableSubdirectory()
{
var repo = MakeDir("repo-a");
Directory.CreateDirectory(Path.Combine(repo, ".git"));
var locked = new DirectoryInfo(MakeDir("locked"));
var denyRule = new System.Security.AccessControl.FileSystemAccessRule(
Environment.UserName,
System.Security.AccessControl.FileSystemRights.ListDirectory,
System.Security.AccessControl.AccessControlType.Deny);
var acl = locked.GetAccessControl();
acl.AddAccessRule(denyRule);
try
{
locked.SetAccessControl(acl);
var result = RepoScanner.Scan(_root);
Assert.Single(result);
Assert.Equal("repo-a", result[0].Name);
}
finally
{
acl.RemoveAccessRule(denyRule);
locked.SetAccessControl(acl);
}
}
#pragma warning restore CA1416
}