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:
@@ -154,10 +154,9 @@ Voraussetzung: Gitea-Release unter `git.kuns.dev/releases/ClaudeDo` mit `ClaudeD
|
|||||||
- `tests/.../Runner/ClaudeProcessSmokeTest.cs` existiert nicht. Real-CLI-Test als `[Fact(Skip=...)]`, nur lokal bei `CLAUDE_AUTHENTICATED=1`.
|
- `tests/.../Runner/ClaudeProcessSmokeTest.cs` existiert nicht. Real-CLI-Test als `[Fact(Skip=...)]`, nur lokal bei `CLAUDE_AUTHENTICATED=1`.
|
||||||
- **Aufwand:** klein.
|
- **Aufwand:** klein.
|
||||||
|
|
||||||
### 5.4 ExternalMcpService-Tests 🟡
|
### 5.4 ExternalMcpService-Tests ✅
|
||||||
- Service exponiert jetzt **18 Tools** (war 11): `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `UpdateTask`, `UpdateTaskStatus`, `ReviewTask`, `RunTaskNow`, `CancelTask`, `DeleteTask`, `GetTaskStatusValues`, `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ListWorktrees`, `CleanupTaskWorktree`, `GetDailyPrepCandidates`, `SetMyDay`.
|
- Service exponiert **18 Tools** (war 11): `ListTaskLists`, `ListTasks`, `GetTask`, `AddTask`, `UpdateTask`, `UpdateTaskStatus`, `ReviewTask`, `RunTaskNow`, `CancelTask`, `DeleteTask`, `GetTaskStatusValues`, `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ListWorktrees`, `CleanupTaskWorktree`, `GetDailyPrepCandidates`, `SetMyDay`.
|
||||||
- `ExternalMcpServiceTests.cs` hat 14 Tests, plus 4 Sibling-Dateien (`ConfigMcpToolsTests`, `LifecycleMcpToolsTests`, `ListMcpToolsTests`, `RunHistoryMcpToolsTests`).
|
- `ExternalMcpServiceTests.cs` hat jetzt 26 Tests (war 14): die zuvor ungetesteten Worktree/Git-Tools (`GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ListWorktrees`, `CleanupTaskWorktree`) haben je Error-Pfad + git-gestützten Happy-Path (skippen wenn git fehlt). Plus 4 Sibling-Dateien (`ConfigMcpToolsTests`, `LifecycleMcpToolsTests`, `ListMcpToolsTests`, `RunHistoryMcpToolsTests`).
|
||||||
- **Ungetestet im External-Ordner:** `GetTaskWorktree`, `GetTaskDiff`, `MergeTask`, `ListWorktrees`, `CleanupTaskWorktree` — je Happy-Path + Error-Pfad ergänzen.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
private readonly ListRepository _lists;
|
private readonly ListRepository _lists;
|
||||||
private readonly ExternalFakeHubContext _hub = new();
|
private readonly ExternalFakeHubContext _hub = new();
|
||||||
private readonly HubBroadcaster _broadcaster;
|
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()
|
public ExternalMcpServiceTests()
|
||||||
{
|
{
|
||||||
@@ -65,7 +69,34 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
|||||||
_broadcaster = new HubBroadcaster(_hub);
|
_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")
|
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);
|
var dto = await svc.SetMyDay(id, false, null, CancellationToken.None);
|
||||||
Assert.False(dto.IsMyDay);
|
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