test(worker): assert the full external MCP tool-name inventory

Guardrail for the upcoming ExternalMcpService.cs topic split: proves the
move is name-preserving by pinning the complete set of registered tool
names (computed via the SDK's own McpServerTool.Create, not a
reimplementation of the naming rule) before any file is moved.
This commit is contained in:
Mika Kuns
2026-09-02 13:02:10 +02:00
parent 651ddbc31c
commit a0c277100e
@@ -84,4 +84,57 @@ public sealed class ExternalMcpToolSchemaTests
Assert.True(hasHint, "No external tool description carries McpToolDocs.TaskNumberHint."); 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<T>() 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<Type, object>();
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);
}
} }