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.
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Git;
|
||||
using ClaudeDo.Worker.Planning;
|
||||
using ClaudeDo.Worker.State;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.State;
|
||||
@@ -22,6 +24,9 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
private readonly TaskStateServiceBuilder.Built _built;
|
||||
private readonly ITaskStateService _sut;
|
||||
private readonly string _listId;
|
||||
private readonly List<GitRepoFixture> _repos = new();
|
||||
|
||||
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
|
||||
|
||||
public TaskStateServiceTests()
|
||||
{
|
||||
@@ -41,7 +46,19 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var r in _repos) r.Dispose();
|
||||
_db.Dispose();
|
||||
}
|
||||
|
||||
private async Task SetListWorkingDirAsync(string workingDir)
|
||||
{
|
||||
await using var ctx = _factory.CreateDbContext();
|
||||
var list = await ctx.Lists.FirstAsync(l => l.Id == _listId);
|
||||
list.WorkingDir = workingDir;
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task<string> SeedTaskAsync(
|
||||
TaskStatus status,
|
||||
@@ -98,6 +115,74 @@ public sealed class TaskStateServiceTests : IDisposable
|
||||
Assert.Contains(_built.Hub.Proxy.Calls, c => c.Method == "TaskUpdated");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_NoWorkingDir_QueuesNormally_NoBaseDirtyWarning()
|
||||
{
|
||||
// The list created in the constructor has no WorkingDir -- covers the
|
||||
// "no valid repo" acceptance case: queuing must not throw or block.
|
||||
var id = await SeedTaskAsync(TaskStatus.Idle);
|
||||
|
||||
var result = await _sut.EnqueueAsync(id, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Null(result.BaseDirty);
|
||||
Assert.Equal(TaskStatus.Queued, await GetStatusAsync(id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_CleanBaseRepo_NoBaseDirtyWarning()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = new GitRepoFixture();
|
||||
_repos.Add(repo);
|
||||
await SetListWorkingDirAsync(repo.RepoDir);
|
||||
var id = await SeedTaskAsync(TaskStatus.Idle);
|
||||
|
||||
var result = await _sut.EnqueueAsync(id, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.Null(result.BaseDirty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_UntrackedFileInBaseRepo_ReturnsBaseDirtyWarning()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = new GitRepoFixture();
|
||||
_repos.Add(repo);
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "new-file.txt"), "not committed");
|
||||
await SetListWorkingDirAsync(repo.RepoDir);
|
||||
var id = await SeedTaskAsync(TaskStatus.Idle);
|
||||
|
||||
var result = await _sut.EnqueueAsync(id, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.NotNull(result.BaseDirty);
|
||||
Assert.Equal(0, result.BaseDirty!.ModifiedCount);
|
||||
Assert.Equal(1, result.BaseDirty!.UntrackedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_ModifiedFileInBaseRepo_ReturnsBaseDirtyWarning()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var repo = new GitRepoFixture();
|
||||
_repos.Add(repo);
|
||||
File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "changed on disk");
|
||||
await SetListWorkingDirAsync(repo.RepoDir);
|
||||
var id = await SeedTaskAsync(TaskStatus.Idle);
|
||||
|
||||
var result = await _sut.EnqueueAsync(id, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.NotNull(result.BaseDirty);
|
||||
Assert.Equal(1, result.BaseDirty!.ModifiedCount);
|
||||
Assert.Equal(0, result.BaseDirty!.UntrackedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnqueueAsync_ManualTask_Rejected_AndStaysIdle()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user