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.
136 lines
4.2 KiB
C#
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);
|
|
}
|
|
}
|