Files
ClaudeDo/tests/ClaudeDo.Worker.Tests/Git/BaseDirtyCheckerTests.cs
T
mika kuns d3155e6868 feat(queue): warn when the base branch has uncommitted changes
Queuing (update_task_status, batch_update_task_status) and run_task_now
now surface a non-blocking baseDirty warning (separate modified/untracked
counts) when the list's working dir has uncommitted changes at enqueue
time, since a new worktree forks from the commit tip and silently misses
them. BaseDirtyChecker caches per working dir for a few seconds so a
batch queue over many tasks in one list only shells out to git once. The
UI surfaces the same warning via the footer error strip on queue actions.
2026-08-10 14:30:33 +02:00

136 lines
4.2 KiB
C#

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);
}
}