refactor(claude-do): refactor(worker): move the merge and worktree MCP tools out

`src/ClaudeDo.Worker/External/ExternalMcpService.cs` ist **1897 Zeilen** (nachgezählt 2026-09-02; die Datei ist seit dem Anlegen dieses Tasks von 1696 weiter gewachsen): ~30 DTO-Records oben plus ~25 Tool-Methoden — während **13** Geschwisterdateien `*McpTools.cs` im selben Ordner das Topic-Split-Muster schon tragen (`ListMcpTools`, `ConfigMcpTools`, `BatchMcpTools`, `NotesMcpTools`, `QueueStateMc

ClaudeDo-Task: 1862e160-ffde-4a50-aa03-bb7251f56088
This commit is contained in:
Mika Kuns
2026-09-02 13:02:10 +02:00
parent a0c277100e
commit 7a0b97df59
13 changed files with 4018 additions and 2301 deletions
+136
View File
@@ -0,0 +1,136 @@
import re, sys, json
path = r"C:\Private\.claudedo-worktrees\claude-do\1862e160-ffde-4a50-aa03-bb7251f56088\tests\ClaudeDo.Worker.Tests\External\ExternalMcpServiceTests.cs"
with open(path, encoding='utf-8') as f:
lines = f.readlines()
# Find all [Fact] blocks: (start_idx, end_idx, name) where start_idx is the [Fact] line (0-based)
# and end_idx is the line with the closing brace (inclusive), 0-based.
blocks = {}
i = 0
n = len(lines)
while i < n:
if lines[i].strip() == '[Fact]':
fact_start = i
# next line should be method signature
sig_line = lines[i+1]
m = re.search(r'public async Task (\w+)\(', sig_line)
assert m, f"no method name at line {i+2}: {sig_line}"
name = m.group(1)
# find opening brace - it's on the line after signature (possibly signature spans multiple lines)
j = i + 1
# advance until we find a line that is just '{' (with indentation)
while lines[j].strip() != '{':
j += 1
brace_line = j
depth = 0
k = brace_line
while True:
depth += lines[k].count('{') - lines[k].count('}')
if depth == 0:
break
k += 1
end_idx = k
blocks[name] = (fact_start, end_idx)
i = end_idx + 1
else:
i += 1
print(f"Found {len(blocks)} [Fact] blocks", file=sys.stderr)
merge_names = """MergeTask_NotDone_Throws
MergeTask_DryRun_DoesNotMerge
MergeTask_WaitingForReview_WithoutFlag_Throws
MergeTask_WaitingForReview_WithFlag_DryRun_Succeeds
MergeTask_WaitingForReview_WithFlag_MarksTaskDone
MergeTask_LeaveConflictsInTree_LeavesMarkersAndKeepsRepoMidMerge
MergeTask_ConflictWithoutLeaveFlag_AbortsCleanly
ContinueMerge_AfterResolvingMarkers_MergesAndSetsDone
ContinueMerge_SlowVerifyCommand_ReportsProgressWellBeforeIdleTimeout
ContinueMerge_MarkersStillPresent_ReportsConflicts
AbortMerge_RestoresCleanTreeAndKeepsWaitingForReview
ContinueMerge_NoMergeInProgress_Throws
AbortMerge_NoMergeInProgress_Throws
GetMergeConflicts_ReportsHunksWithStartLineAndDiff3Base
GetMergeConflicts_NoMergeInProgress_ReturnsEmpty
ResolveConflictHunk_OursTheirsAndText_ResolveEachHunkAndTrackRemainingCount
ResolveConflictHunk_LeavesOtherHunksUntouchedUntilContinueMerge
ResolveConflictHunk_UnknownFileOrOutOfRangeIndex_Throws
RevertMerge_NotMerged_Throws
RevertMerge_AfterCleanMerge_RevertsAndReturnsTaskToWaitingForReview
RevertMerge_ConflictingRevert_ReturnsRevertedFalseWithConflictsAndAbortsCleanly
ContinueMerge_ParentUnitMergeConflict_RoutesToOrchestratorAndCompletes
PreviewMerge_CleanMerge_ReturnsCleanStatusAndDoesNotChangeRepo
PreviewMerge_EmptyWorktree_ReturnsIsEmptyTrue
PreviewMerge_WorktreeLessHandlerTask_EmptyRange_ReturnsIsEmptyTrue
PreviewMerge_WorktreeLessHandlerTask_NonEmptyRange_ReturnsIsEmptyFalse
PreviewMerge_ConflictingBranches_ReturnsConflictStatusAndDoesNotChangeRepo
PreviewMerge_NoWorktree_Throws
PreviewMerge_MissingWorkingDir_ThrowsClearError
PreviewMerge_VerifyCommandConfigured_RunsInScratchWorktreeAndDoesNotTouchTarget
PreviewMerge_TargetChangedSameFileSinceFork_ReportsStaleFiles
PreviewMerge_TargetBehindButUnrelatedFile_ReportsNoStaleFiles
PreviewMerge_WorktreeLessHandlerTask_ReportsNoStaleFiles
PreviewMergeSet_TwoTasksSharedFile_ReportsOverlap
PreviewMergeSet_TaskFilesAreProperSubsetOfAnother_ReportsSubset
PreviewMergeSet_SingleTaskId_ReturnsEmptyOverlap
PreviewMergeSet_ReportsProgressPerTask
PreviewMergeSet_OneTaskWithoutWorktree_ReportsErrorWithoutAbortingOthers
PreviewMergeSet_MixedEmptyAndNonEmpty_ReportsIsEmptyPerEntry
PreviewMergeSet_DefaultRunVerifyFalse_NeverInvokesRunnerEvenWithCommandConfigured
PreviewMergeSet_RunVerifyTrue_InvokesRunnerPerTask""".split()
worktree_names = """GetTaskWorktree_NoWorktree_Throws
GetTaskWorktree_ReturnsBranchAndBaseCommit
GetTaskWorktree_BeforeMerge_MergeCommitIsNull
GetTaskWorktree_AfterMerge_ExposesMergeCommit
GetTaskDiff_NoWorktree_Throws
GetTaskDiff_AfterCommit_ListsChangedFile
GetTaskDiff_WithPathsFilter_ReturnsOnlyRequestedFile
GetTaskDiff_StatMode_WithPathsFilter_ReturnsOnlyRequestedFile
GetTaskDiff_WorktreeLessHandlerTask_UsesHandlerCommitRangeOverListWorkingDir
GetTaskDiff_WorktreeLessHandlerTask_StatMode_UsesHandlerCommitRange
GetTaskDiff_ReportsProgressWhileDiffing
ListWorktrees_Empty_ReturnsEmpty
ListWorktrees_ReturnsCreatedWorktree
ListWorktrees_ReportsProgressWhileSurveying
CleanupTaskWorktree_NotFound_Throws
CleanupTaskWorktree_Running_Throws
CleanupTaskWorktree_CleanWorktree_Removes
CleanupTaskWorktree_ReportsProgressWhileRemoving""".split()
missing_merge = [n for n in merge_names if n not in blocks]
missing_worktree = [n for n in worktree_names if n not in blocks]
print("missing merge:", missing_merge, file=sys.stderr)
print("missing worktree:", missing_worktree, file=sys.stderr)
overlap = set(merge_names) & set(worktree_names)
print("overlap:", overlap, file=sys.stderr)
def extract(names):
out = []
for name in names:
s, e = blocks[name]
out.append(''.join(lines[s:e+1]))
return out
with open(r"C:\Private\.claudedo-worktrees\claude-do\1862e160-ffde-4a50-aa03-bb7251f56088\tests\ClaudeDo.Worker.Tests\External\_merge_bodies.txt", 'w', encoding='utf-8') as f:
f.write('\n'.join(extract(merge_names)))
with open(r"C:\Private\.claudedo-worktrees\claude-do\1862e160-ffde-4a50-aa03-bb7251f56088\tests\ClaudeDo.Worker.Tests\External\_worktree_bodies.txt", 'w', encoding='utf-8') as f:
f.write('\n'.join(extract(worktree_names)))
# Build set of all line ranges to remove (sorted), then write remainder file for inspection
all_names = merge_names + worktree_names
ranges = sorted([blocks[n] for n in all_names])
# merge overlapping/adjacent not needed; just mark lines
remove_lines = set()
for s, e in ranges:
for x in range(s, e+1):
remove_lines.add(x)
remainder = [lines[i] for i in range(n) if i not in remove_lines]
with open(r"C:\Private\.claudedo-worktrees\claude-do\1862e160-ffde-4a50-aa03-bb7251f56088\tests\ClaudeDo.Worker.Tests\External\_remainder.cs", 'w', encoding='utf-8') as f:
f.writelines(remainder)
print(f"Total lines removed: {len(remove_lines)}", file=sys.stderr)
print(f"Remainder line count: {len(remainder)}", file=sys.stderr)