"""Step definitions for Skill domain model and resolution tests.""" from typing import Any from behave import given, then, when from behave.runner import Context from pydantic import ValidationError from cleveragents.domain.models.core.skill import ( ResolvedToolEntry, Skill, SkillAgentSource, SkillInclude, SkillInlineTool, SkillMcpSource, SkillResolver, SkillToolRef, ) from cleveragents.domain.models.core.tool import ( ToolCapability, ToolSource, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_skill(**overrides: Any) -> Skill: """Create a Skill with sensible defaults, allowing overrides.""" defaults: dict[str, Any] = { "name": "local/test-skill", "description": "A test skill", } defaults.update(overrides) return Skill(**defaults) def _skill_config(**overrides: Any) -> dict[str, Any]: """Return a minimal valid config dict for Skill.from_config.""" cfg: dict[str, Any] = { "name": "local/my-skill", "description": "A skill from config", } cfg.update(overrides) return cfg # --------------------------------------------------------------------------- # Skill Creation Steps # --------------------------------------------------------------------------- @when('I create a skill_model with name "{name}" and description "{desc}"') def skill_model_create_basic(context: Context, name: str, desc: str) -> None: """Create a skill with name and description.""" context.skill_model = Skill(name=name, description=desc) context.skill_model_error = None @when('I create a skill_model with tool refs "{refs}"') def skill_model_create_with_refs(context: Context, refs: str) -> None: """Create a skill with tool refs.""" ref_list = [r.strip() for r in refs.split(",") if r.strip()] context.skill_model = _make_skill(tool_refs=ref_list) context.skill_model_error = None @when("I create a skill_model with all fields") def skill_model_create_all_fields(context: Context) -> None: """Create a skill with all fields populated.""" context.skill_model = Skill( name="local/full-skill", description="Full skill", tool_refs=["local/edit-file", "local/read-file"], includes=[SkillInclude(name="local/basic-tools")], anonymous_tools=[ SkillInlineTool( description="Inline helper", source=ToolSource.CUSTOM, code="print('hello')", ), ], mcp_servers=[ SkillMcpSource( server="npx @mcp/server-fs", tools=["read_file", "write_file"], ), ], agent_skills=[SkillAgentSource(path="./skills/code-review")], overrides={"local/edit-file": {"timeout": 600}}, ) context.skill_model_error = None # --------------------------------------------------------------------------- # Skill Assertions # --------------------------------------------------------------------------- @then("the skill_model should be created") def skill_model_check_created(context: Context) -> None: """Verify the skill was created.""" assert context.skill_model is not None, "Skill should be created" @then('the skill_model name should be "{expected}"') def skill_model_check_name(context: Context, expected: str) -> None: """Check skill name.""" assert context.skill_model.name == expected, ( f"Expected name '{expected}', got '{context.skill_model.name}'" ) @then('the skill_model description should be "{expected}"') def skill_model_check_description(context: Context, expected: str) -> None: """Check skill description.""" assert context.skill_model.description == expected @then('the skill_model namespace should be "{expected}"') def skill_model_check_namespace(context: Context, expected: str) -> None: """Check skill namespace property.""" assert context.skill_model.namespace == expected @then('the skill_model short_name should be "{expected}"') def skill_model_check_short_name(context: Context, expected: str) -> None: """Check skill short_name property.""" assert context.skill_model.short_name == expected @then("the skill_model should have {count:d} tool refs") def skill_model_check_ref_count(context: Context, count: int) -> None: """Check skill tool_refs count.""" actual = len(context.skill_model.tool_refs) assert actual == count, f"Expected {count} tool refs, got {actual}" @then("the skill_model should have {count:d} include") def skill_model_check_include_count(context: Context, count: int) -> None: """Check skill includes count.""" actual = len(context.skill_model.includes) assert actual == count, f"Expected {count} includes, got {actual}" @then("the skill_model should have {count:d} anonymous tool") def skill_model_check_anon_count(context: Context, count: int) -> None: """Check skill anonymous_tools count.""" actual = len(context.skill_model.anonymous_tools) assert actual == count, f"Expected {count} anonymous tools, got {actual}" @then("the skill_model should have {count:d} mcp server") def skill_model_check_mcp_count(context: Context, count: int) -> None: """Check skill mcp_servers count.""" actual = len(context.skill_model.mcp_servers) assert actual == count, f"Expected {count} mcp servers, got {actual}" @then("the skill_model should have {count:d} agent skill") def skill_model_check_agent_count(context: Context, count: int) -> None: """Check skill agent_skills count.""" actual = len(context.skill_model.agent_skills) assert actual == count, f"Expected {count} agent skills, got {actual}" # --------------------------------------------------------------------------- # Name Validation # --------------------------------------------------------------------------- @when('I try to create a skill_model with invalid name "{name}"') def skill_model_try_invalid_name(context: Context, name: str) -> None: """Attempt to create a skill with an invalid name.""" context.skill_model_error = None context.skill_model = None try: context.skill_model = _make_skill(name=name) except ValidationError as e: context.skill_model_error = e @when("I try to create a skill_model with empty description") def skill_model_try_empty_desc(context: Context) -> None: """Attempt to create a skill with empty description.""" context.skill_model_error = None context.skill_model = None try: context.skill_model = Skill(name="local/bad", description="") except ValidationError as e: context.skill_model_error = e @then("a skill_model validation error should be raised") def skill_model_check_validation_error(context: Context) -> None: """Verify that a validation error was raised.""" assert context.skill_model_error is not None, ( "Expected a validation error to be raised" ) @then('the skill_model error should mention "{text}"') def skill_model_check_error_message(context: Context, text: str) -> None: """Check the error message contains expected text.""" error_str = str(context.skill_model_error) assert text in error_str, f"Expected error to mention '{text}', got: {error_str}" # --------------------------------------------------------------------------- # Include Resolution (flat) # --------------------------------------------------------------------------- @given('a skill_resolver registry with "{name}" containing refs "{refs}"') def skill_resolver_registry_with_refs(context: Context, name: str, refs: str) -> None: """Set up a registry with a skill containing tool refs.""" if not hasattr(context, "skill_registry"): context.skill_registry = {} ref_list = [r.strip() for r in refs.split(",") if r.strip()] context.skill_registry[name] = Skill( name=name, description=f"Skill {name}", tool_refs=ref_list, ) @given( 'a skill_resolver root skill "{name}" including "{includes}" with own refs "{refs}"' ) def skill_resolver_root_with_includes( context: Context, name: str, includes: str, refs: str ) -> None: """Set up the root skill to resolve.""" if not hasattr(context, "skill_registry"): context.skill_registry = {} include_list = [ SkillInclude(name=i.strip()) for i in includes.split(",") if i.strip() ] ref_list = [r.strip() for r in refs.split(",") if r.strip()] context.skill_root = Skill( name=name, description=f"Root skill {name}", tool_refs=ref_list, includes=include_list, ) context.skill_registry[name] = context.skill_root @given('a skill_resolver root skill "{name}" including "{includes}" with no own refs') def skill_resolver_root_no_refs(context: Context, name: str, includes: str) -> None: """Set up the root skill with includes and no own refs.""" if not hasattr(context, "skill_registry"): context.skill_registry = {} include_list = [ SkillInclude(name=i.strip()) for i in includes.split(",") if i.strip() ] context.skill_root = Skill( name=name, description=f"Root skill {name}", includes=include_list, ) context.skill_registry[name] = context.skill_root @when("I resolve the skill_resolver root skill") def skill_resolver_resolve_root(context: Context) -> None: """Resolve the root skill.""" resolver = SkillResolver() context.skill_resolved = resolver.resolve_tools( context.skill_root, context.skill_registry ) context.skill_resolver_error = None @then("the skill_resolver result should have {count:d} tools") def skill_resolver_check_tool_count(context: Context, count: int) -> None: """Check resolved tool count.""" actual = len(context.skill_resolved) assert actual == count, ( f"Expected {count} tools, got {actual}: " f"{[e.name for e in context.skill_resolved]}" ) @then('the skill_resolver result should contain tool "{name}"') def skill_resolver_check_contains_tool(context: Context, name: str) -> None: """Check resolved tools contain a specific tool.""" names = [e.name for e in context.skill_resolved] assert name in names, f"Expected tool '{name}' in {names}" # --------------------------------------------------------------------------- # Include Resolution (nested) # --------------------------------------------------------------------------- @given( 'a skill_resolver registry with "{name}" including "{includes}" and containing refs "{refs}"' ) def skill_resolver_registry_with_includes_and_refs( context: Context, name: str, includes: str, refs: str ) -> None: """Set up a registry skill with includes and refs.""" if not hasattr(context, "skill_registry"): context.skill_registry = {} include_list = [ SkillInclude(name=i.strip()) for i in includes.split(",") if i.strip() ] ref_list = [r.strip() for r in refs.split(",") if r.strip()] context.skill_registry[name] = Skill( name=name, description=f"Skill {name}", tool_refs=ref_list, includes=include_list, ) # --------------------------------------------------------------------------- # Cycle Detection # --------------------------------------------------------------------------- @given('a skill_resolver registry with "{name}" including "{includes}"') def skill_resolver_registry_with_includes( context: Context, name: str, includes: str ) -> None: """Set up a registry skill that includes another.""" if not hasattr(context, "skill_registry"): context.skill_registry = {} include_list = [ SkillInclude(name=i.strip()) for i in includes.split(",") if i.strip() ] context.skill_registry[name] = Skill( name=name, description=f"Skill {name}", includes=include_list, ) @when('I try to resolve skill_resolver "{name}"') def skill_resolver_try_resolve(context: Context, name: str) -> None: """Try to resolve a skill and capture any error.""" if not hasattr(context, "skill_registry"): context.skill_registry = {} # If root skill is set in registry, use it; otherwise find it skill = context.skill_registry.get(name) if skill is None: # Must be set via root skill step skill = getattr(context, "skill_root", None) assert skill is not None, f"Skill '{name}' not found in registry or root" resolver = SkillResolver() context.skill_resolver_error = None context.skill_resolved = None try: context.skill_resolved = resolver.resolve_tools(skill, context.skill_registry) except ValueError as e: context.skill_resolver_error = e @then("a skill_resolver cycle error should be raised") def skill_resolver_check_cycle_error(context: Context) -> None: """Verify that a cycle error was raised.""" assert context.skill_resolver_error is not None, ( "Expected a cycle error to be raised" ) assert "ycle" in str(context.skill_resolver_error), ( f"Expected cycle error, got: {context.skill_resolver_error}" ) @then('the skill_resolver error should mention "{text}"') def skill_resolver_check_error_mention(context: Context, text: str) -> None: """Check the resolver error mentions text.""" error_str = str(context.skill_resolver_error) assert text in error_str, f"Expected error to mention '{text}', got: {error_str}" # --------------------------------------------------------------------------- # De-duplication # --------------------------------------------------------------------------- @then('the skill_resolver tool "{name}" should come from "{source}"') def skill_resolver_check_tool_source(context: Context, name: str, source: str) -> None: """Check which skill contributed a tool.""" for entry in context.skill_resolved: if entry.name == name: assert entry.source_skill == source, ( f"Expected tool '{name}' from '{source}', got '{entry.source_skill}'" ) return raise AssertionError( f"Tool '{name}' not found in resolved: " f"{[e.name for e in context.skill_resolved]}" ) # --------------------------------------------------------------------------- # Override Application # --------------------------------------------------------------------------- @when('I create a skill_model with overrides for "{tool_name}"') def skill_model_create_with_overrides(context: Context, tool_name: str) -> None: """Create a skill with per-tool overrides.""" context.skill_model = Skill( name="local/override-skill", description="Skill with overrides", tool_refs=[tool_name], overrides={tool_name: {"timeout": 600}}, ) context.skill_model_error = None @when("I resolve the overridden skill_model") def skill_model_resolve_overridden(context: Context) -> None: """Resolve a skill with overrides.""" resolver = SkillResolver() context.skill_resolved = resolver.resolve_tools(context.skill_model, {}) @then('the skill_resolver tool "{name}" should have overrides') def skill_resolver_check_tool_overrides(context: Context, name: str) -> None: """Check that a resolved tool has overrides.""" for entry in context.skill_resolved: if entry.name == name: assert entry.overrides, f"Expected overrides on tool '{name}', got empty" return raise AssertionError(f"Tool '{name}' not found in resolved") # --------------------------------------------------------------------------- # Empty Skill # --------------------------------------------------------------------------- @when("I resolve the empty skill_model") def skill_model_resolve_empty(context: Context) -> None: """Resolve an empty skill.""" resolver = SkillResolver() context.skill_resolved = resolver.resolve_tools(context.skill_model, {}) # --------------------------------------------------------------------------- # from_config Loading # --------------------------------------------------------------------------- @when('I load a skill_model from config with name "{name}"') def skill_model_load_from_config(context: Context, name: str) -> None: """Load a skill from a config dict.""" config = _skill_config(name=name) context.skill_model = Skill.from_config(config) context.skill_model_error = None @when('I try to load a skill_model from config missing "{field}"') def skill_model_try_load_missing(context: Context, field: str) -> None: """Try loading a skill config with missing field.""" config = _skill_config() del config[field] context.skill_model_config_error = None try: Skill.from_config(config) except ValueError as e: context.skill_model_config_error = e @then('a skill_model config error should be raised with "{field}"') def skill_model_check_config_error(context: Context, field: str) -> None: """Check that a config error mentioning field was raised.""" assert context.skill_model_config_error is not None, ( f"Expected ValueError for missing '{field}'" ) assert field in str(context.skill_model_config_error), ( f"Expected error to mention '{field}', got: {context.skill_model_config_error}" ) @when("I load a skill_model from full config") def skill_model_load_full_config(context: Context) -> None: """Load a skill from a full config with all sections.""" config = { "name": "local/full-config", "description": "Full config skill", "tools": ["local/edit-file", "local/read-file"], "includes": [{"name": "local/basic-tools"}], "anonymous_tools": [ { "description": "Inline helper", "source": "custom", "code": "print('hi')", }, ], "mcp_servers": [ { "server": "npx @mcp/server-fs", "tools": ["read_file"], }, ], "agent_skills": [{"path": "./skills/review"}], "overrides": {"local/edit-file": {"timeout": 600}}, } context.skill_model = Skill.from_config(config) context.skill_model_error = None @when("I load a skill_model from config with string includes") def skill_model_load_config_string_includes(context: Context) -> None: """Load a skill from config where includes are plain strings.""" config = { "name": "local/string-inc", "description": "String includes", "includes": ["local/basic-tools"], } context.skill_model = Skill.from_config(config) context.skill_model_error = None @when("I load a skill_model from config with string agent skills") def skill_model_load_config_string_agents(context: Context) -> None: """Load a skill from config where agent_skills are plain strings.""" config = { "name": "local/string-agents", "description": "String agents", "agent_skills": ["./skills/review"], } context.skill_model = Skill.from_config(config) context.skill_model_error = None # --------------------------------------------------------------------------- # as_cli_dict # --------------------------------------------------------------------------- @when("I create a skill_model and call as_cli_dict") def skill_model_create_and_cli_dict(context: Context) -> None: """Create a skill and get CLI dict.""" context.skill_model = _make_skill() context.skill_cli_dict = context.skill_model.as_cli_dict() @when("I create a skill_model with all fields and call as_cli_dict") def skill_model_create_all_cli_dict(context: Context) -> None: """Create a skill with all fields and get CLI dict.""" context.skill_model = Skill( name="local/full-cli", description="Full CLI skill", tool_refs=["local/edit-file"], includes=[SkillInclude(name="local/base", overrides={"x": 1})], anonymous_tools=[ SkillInlineTool( description="Inline", source=ToolSource.CUSTOM, code="x = 1", ), ], mcp_servers=[ SkillMcpSource(server="npx @mcp/fs", tools=["read"], env={"K": "V"}), ], agent_skills=[SkillAgentSource(path="./skills/r")], overrides={"local/edit-file": {"timeout": 600}}, ) context.skill_cli_dict = context.skill_model.as_cli_dict() @when("I get the skill_model cli dict") def skill_model_get_cli_dict(context: Context) -> None: """Get CLI dict from the current skill model.""" context.skill_cli_dict = context.skill_model.as_cli_dict() @then('the skill_model cli dict should have key "{key}"') def skill_model_check_cli_dict_key(context: Context, key: str) -> None: """Check CLI dict has expected key.""" assert key in context.skill_cli_dict, ( f"Expected key '{key}' in cli dict, keys: {list(context.skill_cli_dict)}" ) @then('the skill_model cli dict should not have key "{key}"') def skill_model_check_cli_dict_no_key(context: Context, key: str) -> None: """Check CLI dict does not have a key.""" assert key not in context.skill_cli_dict, f"Unexpected key '{key}' in cli dict" # --------------------------------------------------------------------------- # Inline Tool Validation # --------------------------------------------------------------------------- @when('I create a skill_model inline tool with source "{source}" and code "{code}"') def skill_model_create_inline_tool(context: Context, source: str, code: str) -> None: """Create a SkillInlineTool.""" context.skill_inline_tool = SkillInlineTool( description="Inline test tool", source=ToolSource(source), code=code, ) context.skill_model_error = None @then("the skill_model inline tool should be created") def skill_model_check_inline_created(context: Context) -> None: """Verify inline tool was created.""" assert context.skill_inline_tool is not None @then("the skill_model inline tool timeout should be {expected:d}") def skill_model_check_inline_timeout(context: Context, expected: int) -> None: """Check inline tool timeout.""" assert context.skill_inline_tool.timeout == expected @when("I try to create a skill_model inline tool with timeout {timeout:d}") def skill_model_try_inline_bad_timeout(context: Context, timeout: int) -> None: """Attempt to create an inline tool with invalid timeout.""" context.skill_model_error = None try: SkillInlineTool( description="Bad timeout", source=ToolSource.CUSTOM, code="x = 1", timeout=timeout, ) except ValidationError as e: context.skill_model_error = e @when("I try to create a skill_model inline tool with empty description") def skill_model_try_inline_empty_desc(context: Context) -> None: """Attempt to create an inline tool with empty description.""" context.skill_model_error = None try: SkillInlineTool( description="", source=ToolSource.CUSTOM, code="x = 1", ) except ValidationError as e: context.skill_model_error = e # --------------------------------------------------------------------------- # MCP Source Validation # --------------------------------------------------------------------------- @when('I create a skill_model mcp source with server "{server}" and tools "{tools}"') def skill_model_create_mcp_source(context: Context, server: str, tools: str) -> None: """Create a SkillMcpSource with tools.""" tool_list = [t.strip() for t in tools.split(",") if t.strip()] context.skill_mcp_source = SkillMcpSource(server=server, tools=tool_list) context.skill_model_error = None @when('I create a skill_model mcp source with server "{server}" and no tools') def skill_model_create_mcp_no_tools(context: Context, server: str) -> None: """Create a SkillMcpSource without specific tools (all tools).""" context.skill_mcp_source = SkillMcpSource(server=server) context.skill_model_error = None @when("I create a skill_model mcp source with env vars") def skill_model_create_mcp_env(context: Context) -> None: """Create a SkillMcpSource with env vars.""" context.skill_mcp_source = SkillMcpSource( server="npx @mcp/server", env={"API_KEY": "test123"}, ) context.skill_model_error = None @when("I try to create a skill_model mcp source with empty server") def skill_model_try_mcp_empty_server(context: Context) -> None: """Attempt to create MCP source with empty server.""" context.skill_model_error = None try: SkillMcpSource(server="") except ValidationError as e: context.skill_model_error = e @then("the skill_model mcp source should be created") def skill_model_check_mcp_created(context: Context) -> None: """Verify MCP source was created.""" assert context.skill_mcp_source is not None @then("the skill_model mcp source should have {count:d} tools") def skill_model_check_mcp_tool_count(context: Context, count: int) -> None: """Check MCP source tool count.""" assert context.skill_mcp_source.tools is not None actual = len(context.skill_mcp_source.tools) assert actual == count, f"Expected {count} tools, got {actual}" @then("the skill_model mcp source tools should be none") def skill_model_check_mcp_tools_none(context: Context) -> None: """Check MCP source tools is None.""" assert context.skill_mcp_source.tools is None # --------------------------------------------------------------------------- # Agent Skill Source Validation # --------------------------------------------------------------------------- @when('I create a skill_model agent source with path "{path}"') def skill_model_create_agent_source(context: Context, path: str) -> None: """Create a SkillAgentSource.""" context.skill_agent_source = SkillAgentSource(path=path) context.skill_model_error = None @when("I try to create a skill_model agent source with empty path") def skill_model_try_agent_empty_path(context: Context) -> None: """Attempt to create agent source with empty path.""" context.skill_model_error = None try: SkillAgentSource(path="") except ValidationError as e: context.skill_model_error = e @then("the skill_model agent source should be created") def skill_model_check_agent_created(context: Context) -> None: """Verify agent source was created.""" assert context.skill_agent_source is not None # --------------------------------------------------------------------------- # CapabilitySummary Computation # --------------------------------------------------------------------------- @when("I compute skill_model capability summary for a skill with inline tools") def skill_model_compute_summary(context: Context) -> None: """Compute capability summary for a skill with various tools.""" skill = Skill( name="local/summary-skill", description="Summary test", tool_refs=["local/some-ref"], anonymous_tools=[ SkillInlineTool( description="Read-only tool", source=ToolSource.CUSTOM, code="x = 1", capability=ToolCapability(read_only=True), ), SkillInlineTool( description="Write tool", source=ToolSource.CUSTOM, code="x = 2", capability=ToolCapability( writes=True, side_effects=["filesystem"], ), ), ], mcp_servers=[SkillMcpSource(server="npx @mcp/fs")], agent_skills=[SkillAgentSource(path="./skills/r")], ) resolver = SkillResolver() resolved = resolver.resolve_tools(skill, {}) context.skill_summary = resolver.compute_capability_summary(skill, resolved) @when("I compute skill_model capability summary for an empty skill") def skill_model_compute_empty_summary(context: Context) -> None: """Compute capability summary for an empty skill.""" skill = Skill( name="local/empty-summary", description="Empty summary test", ) resolver = SkillResolver() resolved = resolver.resolve_tools(skill, {}) context.skill_summary = resolver.compute_capability_summary(skill, resolved) @then("the skill_model summary total tools should be {expected:d}") def skill_model_check_summary_total(context: Context, expected: int) -> None: """Check summary total_tools.""" assert context.skill_summary.total_tools == expected @then("the skill_model summary read_only_tools should be {expected:d}") def skill_model_check_summary_readonly(context: Context, expected: int) -> None: """Check summary read_only_tools.""" assert context.skill_summary.read_only_tools == expected @then("the skill_model summary write_tools should be {expected:d}") def skill_model_check_summary_writes(context: Context, expected: int) -> None: """Check summary write_tools.""" assert context.skill_summary.write_tools == expected @then("the skill_model summary has_side_effects should be true") def skill_model_check_summary_side_effects(context: Context) -> None: """Check summary has_side_effects.""" assert context.skill_summary.has_side_effects is True @then("the skill_model summary mcp_sources should be {expected:d}") def skill_model_check_summary_mcp(context: Context, expected: int) -> None: """Check summary mcp_sources.""" assert context.skill_summary.mcp_sources == expected @then("the skill_model summary agent_skill_sources should be {expected:d}") def skill_model_check_summary_agents(context: Context, expected: int) -> None: """Check summary agent_skill_sources.""" assert context.skill_summary.agent_skill_sources == expected # --------------------------------------------------------------------------- # SkillToolRef # --------------------------------------------------------------------------- @when('I create a skill_model tool ref with name "{name}"') def skill_model_create_tool_ref(context: Context, name: str) -> None: """Create a SkillToolRef.""" context.skill_tool_ref = SkillToolRef(name=name) @then("the skill_model tool ref should be created") def skill_model_check_tool_ref(context: Context) -> None: """Verify tool ref was created.""" assert context.skill_tool_ref is not None # --------------------------------------------------------------------------- # SkillInclude # --------------------------------------------------------------------------- @when('I create a skill_model include with name "{name}" and overrides') def skill_model_create_include_with_overrides(context: Context, name: str) -> None: """Create a SkillInclude with overrides.""" context.skill_include = SkillInclude(name=name, overrides={"timeout": 600}) @when('I create a skill_model include with name "{name}" without overrides') def skill_model_create_include_no_overrides(context: Context, name: str) -> None: """Create a SkillInclude without overrides.""" context.skill_include = SkillInclude(name=name) @then("the skill_model include should be created") def skill_model_check_include_created(context: Context) -> None: """Verify include was created.""" assert context.skill_include is not None @then("the skill_model include should have overrides") def skill_model_check_include_overrides(context: Context) -> None: """Check include has overrides.""" assert context.skill_include.overrides is not None # --------------------------------------------------------------------------- # Missing included skill # --------------------------------------------------------------------------- @then("a skill_resolver missing skill error should be raised") def skill_resolver_check_missing_error(context: Context) -> None: """Verify a missing skill error was raised.""" assert context.skill_resolver_error is not None, "Expected a missing skill error" assert "not found" in str(context.skill_resolver_error), ( f"Expected 'not found' in error, got: {context.skill_resolver_error}" ) # --------------------------------------------------------------------------- # MCP and agent skill resolution # --------------------------------------------------------------------------- @when("I create and resolve a skill_model with mcp and agent sources") def skill_model_resolve_mcp_agent(context: Context) -> None: """Create and resolve a skill with MCP and agent sources.""" skill = Skill( name="local/mcp-agent", description="MCP and agent test", tool_refs=["local/base-tool"], mcp_servers=[ SkillMcpSource( server="npx @mcp/fs", tools=["read_file"], ), ], agent_skills=[SkillAgentSource(path="./skills/review")], ) resolver = SkillResolver() context.skill_resolved = resolver.resolve_tools(skill, {}) # --------------------------------------------------------------------------- # ResolvedToolEntry # --------------------------------------------------------------------------- @when("I create a skill_resolver resolved tool entry") def skill_resolver_create_entry(context: Context) -> None: """Create a ResolvedToolEntry.""" context.skill_resolved_entry = ResolvedToolEntry( name="local/test-tool", source_skill="local/my-skill", ) @then("the skill_resolver entry should be created") def skill_resolver_check_entry(context: Context) -> None: """Verify entry was created.""" assert context.skill_resolved_entry is not None assert context.skill_resolved_entry.name == "local/test-tool" assert context.skill_resolved_entry.is_inline is False # --------------------------------------------------------------------------- # Robot Smoke Mirror Steps (Behave equivalents of robot/skill_resolution.robot) # --------------------------------------------------------------------------- @then('the skill_resolver tool at index {index:d} should be "{expected}"') def skill_resolver_check_tool_at_index( context: Context, index: int, expected: str ) -> None: """Check the tool name at a specific index in the resolved list.""" resolved = context.skill_resolved assert index < len(resolved), ( f"Index {index} out of range (resolved has {len(resolved)} tools)" ) actual = resolved[index].name assert actual == expected, ( f"Expected tool at index {index} to be '{expected}', got '{actual}'" ) @when("I create and resolve a skill_model with inline tools") def skill_model_resolve_inline_tools(context: Context) -> None: """Create and resolve a skill with inline tools.""" skill = Skill( name="local/inline-test", description="Inline tool test", tool_refs=["local/ref-tool"], anonymous_tools=[ SkillInlineTool( description="First inline", source=ToolSource.CUSTOM, code="return 1", ), SkillInlineTool( description="Second inline", source=ToolSource.CUSTOM, code="return 2", ), ], ) resolver = SkillResolver() context.skill_resolved = resolver.resolve_tools(skill, {}) @then("the skill_resolver inline tool at index {index:d} should be marked inline") def skill_resolver_check_inline_at_index(context: Context, index: int) -> None: """Check that the resolved entry at a specific index is marked inline.""" resolved = context.skill_resolved assert index < len(resolved), ( f"Index {index} out of range (resolved has {len(resolved)} tools)" ) entry = resolved[index] assert entry.is_inline is True, ( f"Expected entry at index {index} ('{entry.name}') to be inline" )