"""Tests for src/mcp_tool_specs.py Phase 1 of any_type_componentization_20260621. Verifies: - 45 ToolSpec instances are registered - get_tool_spec(name) dispatches correctly - tool_names() returns the expected set - get_tool_schemas() returns the expected list - ToolParameter / ToolSpec dataclasses have correct frozen=True semantics - to_dict() round-trip preserves the legacy dict shape - Cross-module invariant: tool_names() == models.AGENT_TOOL_NAMES subset CONVENTION: 1-space indentation. NO COMMENTS. """ from __future__ import annotations import pytest from src import mcp_tool_specs from src import models EXPECTED_TOOLS: set[str] = { 'py_remove_def', 'py_add_def', 'py_move_def', 'py_region_wrap', 'read_file', 'list_directory', 'search_files', 'get_file_summary', 'py_get_skeleton', 'py_get_code_outline', 'ts_c_get_skeleton', 'ts_cpp_get_skeleton', 'ts_c_get_code_outline', 'ts_cpp_get_code_outline', 'ts_c_get_definition', 'ts_cpp_get_definition', 'ts_c_get_signature', 'ts_cpp_get_signature', 'ts_c_update_definition', 'ts_cpp_update_definition', 'get_file_slice', 'set_file_slice', 'edit_file', 'py_get_definition', 'py_update_definition', 'py_get_signature', 'py_set_signature', 'py_get_class_summary', 'py_get_var_declaration', 'py_set_var_declaration', 'get_git_diff', 'web_search', 'fetch_url', 'get_ui_performance', 'py_find_usages', 'py_get_imports', 'py_check_syntax', 'py_get_hierarchy', 'py_get_docstring', 'get_tree', 'bd_create', 'bd_update', 'bd_list', 'bd_ready', 'derive_code_path', } def test_module_loads_with_45_registrations() -> None: assert len(mcp_tool_specs._REGISTRY) == 45 def test_tool_names_set_matches_expected_45() -> None: names = mcp_tool_specs.tool_names() assert len(names) == 45 assert names == EXPECTED_TOOLS def test_get_tool_spec_returns_correct_instance() -> None: spec = mcp_tool_specs.get_tool_spec('py_remove_def') assert spec.name == 'py_remove_def' assert 'Excises' in spec.description or 'class or function' in spec.description assert len(spec.parameters) >= 2 path_param = next((p for p in spec.parameters if p.name == 'path'), None) assert path_param is not None assert path_param.required is True assert path_param.type == 'string' def test_get_tool_spec_raises_for_unknown_name() -> None: with pytest.raises(KeyError): mcp_tool_specs.get_tool_spec('nonexistent_tool_xyz') def test_get_tool_schemas_returns_all_specs() -> None: schemas = mcp_tool_specs.get_tool_schemas() assert len(schemas) == 45 assert all(isinstance(s, mcp_tool_specs.ToolSpec) for s in schemas) def test_tool_spec_is_frozen() -> None: spec = mcp_tool_specs.get_tool_spec('read_file') with pytest.raises(Exception): spec.name = 'mutated' def test_tool_parameter_is_frozen() -> None: spec = mcp_tool_specs.get_tool_spec('read_file') param = spec.parameters[0] with pytest.raises(Exception): param.name = 'mutated' def test_to_dict_round_trip_preserves_shape() -> None: spec = mcp_tool_specs.get_tool_spec('py_remove_def') d = spec.to_dict() assert d['name'] == 'py_remove_def' assert 'description' in d assert d['parameters']['type'] == 'object' assert 'path' in d['parameters']['properties'] assert 'name' in d['parameters']['properties'] assert 'path' in d['parameters']['required'] assert 'name' in d['parameters']['required'] def test_tool_parameter_to_dict_includes_enum() -> None: spec = mcp_tool_specs.get_tool_spec('py_add_def') anchor_param = next((p for p in spec.parameters if p.name == 'anchor_type'), None) assert anchor_param is not None assert anchor_param.enum is not None assert 'before' in anchor_param.enum d = anchor_param.to_dict() assert 'enum' in d assert 'before' in d['enum'] def test_tool_names_subset_of_models_agent_tool_names() -> None: """Cross-module invariant: every MCP tool is also an agent tool.""" native_names = mcp_tool_specs.tool_names() agent_names = set(models.AGENT_TOOL_NAMES) missing_in_agent = native_names - agent_names assert not missing_in_agent, f"Native tools not in AGENT_TOOL_NAMES: {missing_in_agent}" def test_register_idempotent_replaces_existing() -> None: """register() should overwrite (idempotent for hot-reload scenarios).""" from src.mcp_tool_specs import ToolSpec, ToolParameter, register custom = ToolSpec(name='read_file', description='custom', parameters=(ToolParameter(name='x', type='string', description='x'),)) register(custom) assert mcp_tool_specs.get_tool_spec('read_file').description == 'custom'