test(worker): cover External MCP worktree/git tools
Add error-path + git-backed happy-path tests for the five previously untested ExternalMcpService tools: GetTaskWorktree, GetTaskDiff, MergeTask (dry-run + not-Done guard), ListWorktrees, CleanupTaskWorktree. Git-backed cases skip when git is unavailable. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,10 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
private readonly ListRepository _lists;
|
||||
private readonly ExternalFakeHubContext _hub = new();
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly List<GitRepoFixture> _repos = new();
|
||||
private readonly List<(string repoDir, string wtPath)> _worktreeCleanups = new();
|
||||
|
||||
private static bool GitAvailable => GitRepoFixture.IsGitAvailable();
|
||||
|
||||
public ExternalMcpServiceTests()
|
||||
{
|
||||
@@ -65,7 +69,34 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
_broadcaster = new HubBroadcaster(_hub);
|
||||
}
|
||||
|
||||
public void Dispose() { _ctx.Dispose(); _db.Dispose(); }
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var (repoDir, wtPath) in _worktreeCleanups)
|
||||
{
|
||||
try { GitRepoFixture.RunGit(repoDir, "worktree", "remove", "--force", wtPath); } catch { }
|
||||
}
|
||||
foreach (var r in _repos) r.Dispose();
|
||||
_ctx.Dispose();
|
||||
_db.Dispose();
|
||||
}
|
||||
|
||||
private async Task<(TaskEntity task, ListEntity list, WorktreeContext wt)> SeedWorktreeAsync(
|
||||
TaskStatus status = TaskStatus.Done)
|
||||
{
|
||||
var repo = new GitRepoFixture();
|
||||
_repos.Add(repo);
|
||||
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
var list = new ListEntity { Id = listId, Name = "L", WorkingDir = repo.RepoDir, CreatedAt = DateTime.UtcNow };
|
||||
await _lists.AddAsync(list);
|
||||
var task = await SeedTaskAsync(listId, status: status);
|
||||
|
||||
var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
|
||||
var mgr = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
|
||||
var wt = await mgr.CreateAsync(task, list, CancellationToken.None);
|
||||
_worktreeCleanups.Add((repo.RepoDir, wt.WorktreePath));
|
||||
return (task, list, wt);
|
||||
}
|
||||
|
||||
private async Task<string> SeedListAsync(string name = "L")
|
||||
{
|
||||
@@ -412,4 +443,156 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var dto = await svc.SetMyDay(id, false, null, CancellationToken.None);
|
||||
Assert.False(dto.IsMyDay);
|
||||
}
|
||||
|
||||
// ── GetTaskWorktree ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskWorktree_NoWorktree_Throws()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.GetTaskWorktree(task.Id, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskWorktree_ReturnsBranchAndBaseCommit()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, _, wt) = await SeedWorktreeAsync();
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var info = await sut.GetTaskWorktree(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(wt.BranchName, info.Branch);
|
||||
Assert.Equal(wt.BaseCommit, info.BaseCommit);
|
||||
Assert.Equal(0, info.Ahead);
|
||||
Assert.False(info.IsDirty);
|
||||
}
|
||||
|
||||
// ── GetTaskDiff ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskDiff_NoWorktree_Throws()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.GetTaskDiff(task.Id, false, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTaskDiff_AfterCommit_ListsChangedFile()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, list, wt) = await SeedWorktreeAsync();
|
||||
File.WriteAllText(Path.Combine(wt.WorktreePath, "added.txt"), "content");
|
||||
|
||||
var cfg = new WorkerConfig { WorktreeRootStrategy = "sibling" };
|
||||
var mgr = new WorktreeManager(new GitService(), _db.CreateFactory(), cfg, NullLogger<WorktreeManager>.Instance);
|
||||
await mgr.CommitIfChangedAsync(wt, task, list, CancellationToken.None);
|
||||
|
||||
var sut = BuildSut(CreateQueue());
|
||||
var diff = await sut.GetTaskDiff(task.Id, false, CancellationToken.None);
|
||||
|
||||
Assert.Contains("added.txt", diff.Files);
|
||||
Assert.False(diff.Truncated);
|
||||
}
|
||||
|
||||
// ── MergeTask ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task MergeTask_NotDone_Throws()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.Idle);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.MergeTask(task.Id, "main", true, false, CancellationToken.None));
|
||||
Assert.Contains("Done", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeTask_DryRun_DoesNotMerge()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, _, _) = await SeedWorktreeAsync(TaskStatus.Done);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.MergeTask(task.Id, "main", true, dryRun: true, CancellationToken.None);
|
||||
|
||||
Assert.False(result.Merged);
|
||||
Assert.Null(result.MergeCommit);
|
||||
}
|
||||
|
||||
// ── ListWorktrees ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ListWorktrees_Empty_ReturnsEmpty()
|
||||
{
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var rows = await sut.ListWorktrees(CancellationToken.None);
|
||||
|
||||
Assert.Empty(rows);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListWorktrees_ReturnsCreatedWorktree()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, _, _) = await SeedWorktreeAsync();
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var rows = await sut.ListWorktrees(CancellationToken.None);
|
||||
|
||||
Assert.Contains(rows, r => r.TaskId == task.Id);
|
||||
}
|
||||
|
||||
// ── CleanupTaskWorktree ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupTaskWorktree_NotFound_Throws()
|
||||
{
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.CleanupTaskWorktree("does-not-exist", false, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupTaskWorktree_Running_Throws()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, _, _) = await SeedWorktreeAsync(TaskStatus.Running);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => sut.CleanupTaskWorktree(task.Id, false, CancellationToken.None));
|
||||
Assert.Contains("running", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CleanupTaskWorktree_CleanWorktree_Removes()
|
||||
{
|
||||
if (!GitAvailable) { Assert.True(true, "git not available -- skipping"); return; }
|
||||
|
||||
var (task, _, wt) = await SeedWorktreeAsync(TaskStatus.Done);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var result = await sut.CleanupTaskWorktree(task.Id, false, CancellationToken.None);
|
||||
|
||||
Assert.True(result.Removed);
|
||||
Assert.False(Directory.Exists(wt.WorktreePath));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user