Private
Public Access
0
0
Files
manual_slop/tests/test_mcp_tool_specs.py
T
ed 779d504c70 refactor(mcp_tool_specs): delete redundant AGENT_TOOL_NAMES; use tool_names() at consumer sites
AGENT_TOOL_NAMES was a hardcoded snapshot of mcp_tool_specs.tool_names()
in src/models.py. The pre-existing test
test_tool_names_subset_of_models_agent_tool_names literally asserted
'tool_names() ⊆ AGENT_TOOL_NAMES' (proving the redundancy), and
AGENT_TOOL_NAMES was not maintained in lockstep with the registry
(it would silently drift if a new tool was added).

This commit:
 1. Deletes AGENT_TOOL_NAMES from src/models.py (replaced by an
    explanatory comment in the Constants section).
 2. Updates 3 consumer sites in src/app_controller.py:
    - 'for t in models.AGENT_TOOL_NAMES' -> 'for t in mcp_tool_specs.tool_names()'
    - (in 2 methods: __init__ + a setter)
 3. Updates 2 test sites in tests/test_arch_boundary_phase2.py:
    - 'from src.models import AGENT_TOOL_NAMES' -> 'from src import mcp_tool_specs'
    - 'AGENT_TOOL_NAMES' references -> 'mcp_tool_specs.tool_names()'
 4. Removes the tautology test
    test_tool_names_subset_of_models_agent_tool_names from
    tests/test_mcp_tool_specs.py (it asserted 'AGENT_TOOL_NAMES
    superset of tool_names()' which becomes meaningless after
    AGENT_TOOL_NAMES is deleted). Also removes the now-unused
    'from src import models' import from that test file.

Verification: VC9
  git grep 'AGENT_TOOL_NAMES' -- 'src/*.py' 'tests/*.py'  # 0 hits
  from src import mcp_tool_specs
  mcp_tool_specs.tool_names()  # returns the canonical 45 tools
  from src.app_controller import AppController  # uses the new path

Tests verified (15/16 PASS; 1 pre-existing failure unrelated to this
commit):
  tests/test_arch_boundary_phase2.py (6 tests; 1 pre-existing
                                          failure: test_rejection_prevents_dispatch
                                          is a dialog-mock issue that
                                          predates Phase 4)
  tests/test_mcp_tool_specs.py (10 tests; the tautology test was removed;
                                          the remaining 10 pass)
2026-06-26 10:19:39 -04:00

113 lines
3.9 KiB
Python

"""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
CONVENTION: 1-space indentation. NO COMMENTS.
"""
from __future__ import annotations
import pytest
from src import mcp_tool_specs
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_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'