"""Step definitions for features/builtin_adapter.feature.""" from __future__ import annotations from typing import Any from behave import given, then, when from cleveragents.domain.models.core.tool import ResourceSlot from cleveragents.mcp.adapter import MCPToolAdapter from cleveragents.tool.builtins import ( ALL_FILE_TOOLS, ALL_GIT_TOOLS, ALL_SUBPLAN_TOOLS, BuiltinAdapter, ) from cleveragents.tool.runtime import ToolSpec __all__: list[str] = [] # --------------------------------------------------------------------------- # Givens # --------------------------------------------------------------------------- @given("a builtin adapter") def step_given_builtin_adapter(context: Any) -> None: context.builtin_adapter = BuiltinAdapter() @given('an MCP tool input schema with a "{param_name}" property') def step_given_mcp_schema(context: Any, param_name: str) -> None: context.mcp_tool_name = f"test-tool-{param_name}" context.mcp_input_schema = { "type": "object", "properties": { param_name: {"type": "string", "description": f"The {param_name}"}, }, "required": [param_name], } @given("an MCP tool with an empty input schema") def step_given_empty_schema(context: Any) -> None: context.mcp_tool_name = "test-tool-empty" context.mcp_input_schema = {} @given("an MCP tool with an input schema missing the properties key") def step_given_no_properties_schema(context: Any) -> None: context.mcp_tool_name = "test-tool-no-props" context.mcp_input_schema = {"type": "object"} @given('an MCP tool with both "file_path" and "filepath" properties') def step_given_duplicate_file_params(context: Any) -> None: context.mcp_tool_name = "test-tool-dup-files" context.mcp_input_schema = { "type": "object", "properties": { "file_path": {"type": "string"}, "filepath": {"type": "string"}, }, } # --------------------------------------------------------------------------- # Whens # --------------------------------------------------------------------------- @when("I call discover on the builtin adapter") def step_when_discover(context: Any) -> None: context.discovered_tools = context.builtin_adapter.discover() @when("I call register on the builtin adapter") def step_when_register(context: Any) -> None: context.registered_names = context.builtin_adapter.register(context.registry) @when("I call activate on the builtin adapter") def step_when_activate(context: Any) -> None: context.activate_exception = None try: context.builtin_adapter.activate() except Exception as exc: context.activate_exception = exc @when("I call deactivate on the builtin adapter") def step_when_deactivate(context: Any) -> None: context.deactivate_exception = None try: context.builtin_adapter.deactivate() except Exception as exc: context.deactivate_exception = exc @when("I infer resource slots for the schema") def step_when_infer_slots(context: Any) -> None: context.inferred_slots = MCPToolAdapter.infer_resource_slots( context.mcp_tool_name, context.mcp_input_schema, ) # --------------------------------------------------------------------------- # Thens # --------------------------------------------------------------------------- @then("the builtin adapter should not be discovered yet") def step_then_not_discovered(context: Any) -> None: assert not context.builtin_adapter.is_discovered, ( "Expected is_discovered to be False before calling discover()" ) @then("the builtin adapter should be discovered") def step_then_is_discovered(context: Any) -> None: assert context.builtin_adapter.is_discovered, ( "Expected is_discovered to be True after calling discover()" ) @then("the discovered tools should include all file tools") def step_then_includes_file_tools(context: Any) -> None: discovered_names = {t.name for t in context.discovered_tools} for spec in ALL_FILE_TOOLS: assert spec.name in discovered_names, ( f"File tool '{spec.name}' not in discovered tools" ) @then("the discovered tools should include all git tools") def step_then_includes_git_tools(context: Any) -> None: discovered_names = {t.name for t in context.discovered_tools} for spec in ALL_GIT_TOOLS: assert spec.name in discovered_names, ( f"Git tool '{spec.name}' not in discovered tools" ) @then("the discovered tools should include all subplan tools") def step_then_includes_subplan_tools(context: Any) -> None: discovered_names = {t.name for t in context.discovered_tools} for spec in ALL_SUBPLAN_TOOLS: assert spec.name in discovered_names, ( f"Subplan tool '{spec.name}' not in discovered tools" ) @then("the total discovered tool count should be {count:d}") def step_then_discovered_count(context: Any, count: int) -> None: actual = len(context.discovered_tools) assert actual == count, f"Expected {count} discovered tools, got {actual}" @then("the builtin adapter lifecycle calls should succeed without error") def step_then_lifecycle_no_exception(context: Any) -> None: assert context.activate_exception is None, ( f"activate raised: {context.activate_exception}" ) assert context.deactivate_exception is None, ( f"deactivate raised: {context.deactivate_exception}" ) @then('all registered tools should have source "{source}"') def step_then_all_source(context: Any, source: str) -> None: tools: list[ToolSpec] = context.registry.list_tools() for spec in tools: assert spec.source == source, ( f"Tool '{spec.name}' has source='{spec.source}', expected '{source}'" ) @then("the inferred slots should be empty") def step_then_slots_empty(context: Any) -> None: assert len(context.inferred_slots) == 0, ( f"Expected no inferred slots, got {len(context.inferred_slots)}: " f"{[s.name for s in context.inferred_slots]}" ) @then("the inferred slot count should be {count:d}") def step_then_slot_count(context: Any, count: int) -> None: actual = len(context.inferred_slots) assert actual == count, ( f"Expected {count} inferred slot(s), got {actual}: " f"{[s.name for s in context.inferred_slots]}" ) @then('the inferred slots should contain a "{slot_name}" slot') def step_then_slot_exists(context: Any, slot_name: str) -> None: names = [s.name for s in context.inferred_slots] assert slot_name in names, ( f"Slot '{slot_name}' not found in inferred slots: {names}" ) @then('the "{slot_name}" slot should have resource_type "{expected}"') def step_then_slot_resource_type(context: Any, slot_name: str, expected: str) -> None: slot = _find_slot(context.inferred_slots, slot_name) assert slot.resource_type == expected, ( f"Slot '{slot_name}' resource_type='{slot.resource_type}', " f"expected '{expected}'" ) @then('the "{slot_name}" slot should have access "{expected}"') def step_then_slot_access(context: Any, slot_name: str, expected: str) -> None: slot = _find_slot(context.inferred_slots, slot_name) assert slot.access.value == expected, ( f"Slot '{slot_name}' access='{slot.access.value}', expected '{expected}'" ) @then('the "{slot_name}" slot should have binding "{expected}"') def step_then_slot_binding(context: Any, slot_name: str, expected: str) -> None: slot = _find_slot(context.inferred_slots, slot_name) assert slot.binding.value == expected, ( f"Slot '{slot_name}' binding='{slot.binding.value}', expected '{expected}'" ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _find_slot(slots: list[ResourceSlot], name: str) -> ResourceSlot: """Find a ResourceSlot by name or raise AssertionError.""" for slot in slots: if slot.name == name: return slot raise AssertionError(f"Slot '{name}' not found in {[s.name for s in slots]}")