Merge branch 'claudedo/9f963a215bb34ca4a28050d4f39178d3'

This commit is contained in:
mika kuns
2026-08-10 15:19:57 +02:00
23 changed files with 481 additions and 32 deletions
@@ -0,0 +1,135 @@
using ClaudeDo.Data.Git;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
namespace ClaudeDo.Worker.Tests.Git;
public sealed class BaseDirtyCheckerTests : IDisposable
{
private readonly List<GitRepoFixture> _repos = new();
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
public void Dispose()
{
foreach (var r in _repos) r.Dispose();
}
private GitRepoFixture CreateRepo()
{
var repo = new GitRepoFixture();
_repos.Add(repo);
return repo;
}
private static BaseDirtyChecker CreateSut() =>
new(new GitService(), NullLogger<BaseDirtyChecker>.Instance);
[Fact]
public async Task CheckAsync_NullWorkingDir_ReturnsNull()
{
var sut = CreateSut();
Assert.Null(await sut.CheckAsync(null, default));
Assert.Null(await sut.CheckAsync("", default));
Assert.Null(await sut.CheckAsync(" ", default));
}
[Fact]
public async Task CheckAsync_NotAGitRepo_ReturnsNull()
{
var dir = Path.Combine(Path.GetTempPath(), $"claudedo_notgit_{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
try
{
var sut = CreateSut();
Assert.Null(await sut.CheckAsync(dir, default));
}
finally
{
Directory.Delete(dir, true);
}
}
[Fact]
public async Task CheckAsync_CleanRepo_ReturnsNull()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var sut = CreateSut();
Assert.Null(await sut.CheckAsync(repo.RepoDir, default));
}
[Fact]
public async Task CheckAsync_UntrackedFile_ReturnsUntrackedCountOnly()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
File.WriteAllText(Path.Combine(repo.RepoDir, "scratch.txt"), "new");
var sut = CreateSut();
var warning = await sut.CheckAsync(repo.RepoDir, default);
Assert.NotNull(warning);
Assert.Equal(0, warning!.ModifiedCount);
Assert.Equal(1, warning.UntrackedCount);
}
[Fact]
public async Task CheckAsync_ModifiedTrackedFile_ReturnsModifiedCountOnly()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "edited");
var sut = CreateSut();
var warning = await sut.CheckAsync(repo.RepoDir, default);
Assert.NotNull(warning);
Assert.Equal(1, warning!.ModifiedCount);
Assert.Equal(0, warning.UntrackedCount);
}
[Fact]
public async Task CheckAsync_ModifiedAndUntracked_ReportsBothSeparately()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "edited");
File.WriteAllText(Path.Combine(repo.RepoDir, "scratch.txt"), "new");
var sut = CreateSut();
var warning = await sut.CheckAsync(repo.RepoDir, default);
Assert.NotNull(warning);
Assert.Equal(1, warning!.ModifiedCount);
Assert.Equal(1, warning.UntrackedCount);
}
// Proves the TTL cache collapses repeated checks against the same working dir into a
// single underlying `git status` call -- the acceptance requirement that a batch queue
// op over many tasks in one list must not shell out once per task. A second check
// immediately after a repo mutation still returns the first (stale) answer instead of
// re-running git.
[Fact]
public async Task CheckAsync_RepeatedCallsWithinTtl_ReuseCachedResult()
{
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
var repo = CreateRepo();
var sut = CreateSut();
var first = await sut.CheckAsync(repo.RepoDir, default);
Assert.Null(first);
File.WriteAllText(Path.Combine(repo.RepoDir, "scratch.txt"), "new");
var second = await sut.CheckAsync(repo.RepoDir, default);
Assert.Null(second);
}
}