diff --git a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpToolSchemaTests.cs b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpToolSchemaTests.cs index fd95dc2a..9f6ba4ee 100644 --- a/tests/ClaudeDo.Worker.Tests/External/ExternalMcpToolSchemaTests.cs +++ b/tests/ClaudeDo.Worker.Tests/External/ExternalMcpToolSchemaTests.cs @@ -84,4 +84,57 @@ public sealed class ExternalMcpToolSchemaTests Assert.True(hasHint, "No external tool description carries McpToolDocs.TaskNumberHint."); } + + // Full inventory of registered external tool names, computed via the SDK's own + // McpServerTool.Create (the same call Program.cs's WithTools() makes internally) rather + // than reimplementing the PascalCase-to-snake_case conversion — so this only breaks on an + // actual rename/add/remove, not on a divergent reimplementation of the naming rule. Used to + // prove that splitting ExternalMcpService.cs into topic files (MergeMcpTools, WorktreeMcpTools, + // ...) is a byte-identical rename-preserving move: the set below must not change across that split. + private static readonly string[] ExpectedToolNames = + [ + "list_task_lists", "list_tasks", "get_task", "add_task", "update_task", "add_subtask", + "update_task_status", "review_task", "run_task_now", "cancel_task", "delete_task", + "get_task_worktree", "get_task_diff", "merge_task", "continue_merge", "verify_merges", + "abort_merge", "get_merge_conflicts", "resolve_conflict_hunk", "preview_merge", + "preview_merge_set", "revert_merge", "list_worktrees", "cleanup_task_worktree", + "continue_task", "get_daily_prep_candidates", "set_my_day", + "batch_get_tasks", "batch_add_tasks", "batch_update_task_status", "batch_cancel_tasks", + "batch_delete_tasks", "batch_set_my_day", "batch_cleanup_task_worktrees", + "create_list", "update_list", "delete_list", + "get_list_config", "set_list_config", "set_task_config", "get_task_config", "get_effective_run_config", + "list_runs", "get_run", "get_task_log", + "list_agents", + "reset_failed_task", "submit_task_for_review", + "handoff_list_handler", + "get_app_settings", + "wait_for_task_change", + "get_queue_state", + "add_task_attachment", "list_task_attachments", "remove_task_attachment", + "save_finding", + "add_note", + ]; + + [Fact] + public void ExternalToolNames_MatchExpectedInventory() + { + // A throwaway, never-constructed instance per declaring type — McpServerTool.Create needs a + // non-null target reference to bind the method to, but naming/schema derivation never + // touches instance state, so skipping the real constructor (and its DI dependencies) is safe. + var targets = new Dictionary(); + var actual = ExternalToolMethods() + .Select(m => + { + var target = targets.TryGetValue(m.DeclaringType!, out var t) + ? t + : targets[m.DeclaringType!] = System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(m.DeclaringType!); + return McpServerTool.Create(m, target).ProtocolTool.Name; + }) + .OrderBy(n => n, StringComparer.Ordinal) + .ToList(); + + var expected = ExpectedToolNames.OrderBy(n => n, StringComparer.Ordinal).ToList(); + + Assert.Equal(expected, actual); + } }