"""Step definitions for Agent Skills Loader Behave feature tests. Covers AgentSkillSpec parsing, folder discovery, tool descriptor mapping, progressive disclosure, namespaced naming, step ordering, and path safety. """ from __future__ import annotations import textwrap from pathlib import Path from behave import given, then, when from behave.runner import Context from cleveragents.skills.agent_skills_loader import AgentSkillLoader, AgentSkillSpec # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _write_skill_md(folder: Path, content: str) -> None: """Write dedented content to SKILL.md in *folder*.""" folder.mkdir(parents=True, exist_ok=True) (folder / "SKILL.md").write_text(textwrap.dedent(content).lstrip()) # --------------------------------------------------------------------------- # Given — SKILL.md setup # --------------------------------------------------------------------------- @given("a SKILL.md file with frontmatter:") def step_skill_md_with_frontmatter(context: Context) -> None: """Create a temporary SKILL.md with the given content.""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir _write_skill_md(tmp_dir, context.text) context.skill_parse_error = None context.skill_parse_result = None @given("an empty SKILL.md file") def step_empty_skill_md(context: Context) -> None: """Create an empty SKILL.md file.""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_empty_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir (tmp_dir / "SKILL.md").write_text("") context.skill_parse_error = None context.skill_parse_result = None @given("an agent skills folder with a valid SKILL.md:") def step_agent_skills_folder_with_skill_md(context: Context) -> None: """Create a temporary agent skills folder with a valid SKILL.md.""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_folder_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir _write_skill_md(tmp_dir, context.text) context.skill_load_error = None context.loaded_skill = None context.tool_descriptor = None context.discover_result = None context.activate_result = None @given("an agent skills folder without a SKILL.md file") def step_agent_skills_folder_no_skill_md(context: Context) -> None: """Create a folder without SKILL.md.""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_no_md_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir context.skill_load_error = None context.loaded_skill = None @given("a non-existent agent skills folder path") def step_nonexistent_agent_skills_folder(context: Context) -> None: """Set a path that does not exist.""" context.skill_folder = Path("/tmp/__nonexistent_agent_skill_path_xyz__") context.skill_load_error = None context.loaded_skill = None @given('the folder contains a script file "{filename}" with content "{content}"') def step_folder_has_script_file(context: Context, filename: str, content: str) -> None: """Add a file to the scripts/ subfolder.""" scripts_dir: Path = context.skill_folder / "scripts" scripts_dir.mkdir(exist_ok=True) (scripts_dir / filename).write_text(content) @given('the folder contains a reference file "{filename}" with content "{content}"') def step_folder_has_reference_file( context: Context, filename: str, content: str ) -> None: """Add a file to the references/ subfolder.""" refs_dir: Path = context.skill_folder / "references" refs_dir.mkdir(exist_ok=True) (refs_dir / filename).write_text(content) @given('the folder contains an asset file "{filename}" with content "{content}"') def step_folder_has_asset_file(context: Context, filename: str, content: str) -> None: """Add a file to the assets/ subfolder.""" assets_dir: Path = context.skill_folder / "assets" assets_dir.mkdir(exist_ok=True) (assets_dir / filename).write_text(content) # --------------------------------------------------------------------------- # When — Parsing and loading actions # --------------------------------------------------------------------------- @when("I parse the SKILL.md file") def step_parse_skill_md(context: Context) -> None: """Parse the SKILL.md file using AgentSkillSpec.""" skill_md_path = context.skill_folder / "SKILL.md" context.skill_parse_result = AgentSkillSpec.from_file(skill_md_path) context.skill_parse_error = None @when("I parse the SKILL.md file expecting an error") def step_parse_skill_md_expecting_error(context: Context) -> None: """Attempt to parse the SKILL.md file, expecting a ValueError.""" skill_md_path = context.skill_folder / "SKILL.md" try: context.skill_parse_result = AgentSkillSpec.from_file(skill_md_path) context.skill_parse_error = None except (ValueError, FileNotFoundError) as exc: context.skill_parse_error = exc context.skill_parse_result = None @when("I load the agent skills folder") def step_load_agent_skills_folder(context: Context) -> None: """Load an agent skills folder using AgentSkillLoader.""" context.loaded_skill = AgentSkillLoader.from_folder(context.skill_folder) context.skill_load_error = None @when("I load the agent skills folder expecting an error") def step_load_agent_skills_folder_expecting_error(context: Context) -> None: """Attempt to load an agent skills folder, expecting a ValueError.""" try: context.loaded_skill = AgentSkillLoader.from_folder(context.skill_folder) context.skill_load_error = None except (ValueError, FileNotFoundError) as exc: context.skill_load_error = exc context.loaded_skill = None @when("I convert the loaded agent skill to a tool descriptor") def step_convert_to_tool_descriptor(context: Context) -> None: """Convert the loaded AgentSkillLoader to a ToolDescriptor.""" context.tool_descriptor = context.loaded_skill.to_tool_descriptor() @when("I call discover on the agent skill") def step_call_discover(context: Context) -> None: """Call discover() which returns metadata-only descriptor.""" context.loaded_skill = AgentSkillLoader.from_folder(context.skill_folder) context.discover_result = context.loaded_skill.discover() @when("I call activate on the agent skill") def step_call_activate(context: Context) -> None: """Call activate() to load the full SKILL.md body.""" context.activate_result = context.loaded_skill.activate() @when("I call deactivate on the agent skill") def step_call_deactivate(context: Context) -> None: """Call deactivate() to clear the loaded body.""" context.loaded_skill.deactivate() @when("I list available resources") def step_list_available_resources(context: Context) -> None: """List available resource files in the skill folder.""" context.available_resources = context.loaded_skill.list_resources() # --------------------------------------------------------------------------- # Then — Assertions for parsing # --------------------------------------------------------------------------- @then('the parsed skill name should be "{expected}"') def step_parsed_skill_name(context: Context, expected: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert result.name == expected, f"Expected name '{expected}', got '{result.name}'" @then('the parsed skill description should be "{expected}"') def step_parsed_skill_description(context: Context, expected: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert result.description == expected, ( f"Expected description '{expected}', got '{result.description}'" ) @then('the parsed skill body should contain "{expected}"') def step_parsed_skill_body_contains(context: Context, expected: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert expected in result.body, ( f"Expected body to contain '{expected}', got:\n{result.body}" ) @then('the parsed skill version should be "{expected}"') def step_parsed_skill_version(context: Context, expected: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert result.version == expected, ( f"Expected version '{expected}', got '{result.version}'" ) @then('the parsed skill allowed tools should include "{tool_name}"') def step_parsed_skill_allowed_tools_include(context: Context, tool_name: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert tool_name in result.allowed_tools, ( f"Expected allowed_tools to include '{tool_name}', got: {result.allowed_tools}" ) @then('the parsed skill metadata should have key "{key}" with value "{value}"') def step_parsed_skill_metadata_key(context: Context, key: str, value: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert result.metadata is not None, "No metadata on parsed skill" actual = str(result.metadata.get(key)) assert actual == value, f"Expected metadata['{key}'] = '{value}', got '{actual}'" @then('the agent skill parse error should mention "{keyword}"') def step_agent_skill_parse_error_mentions(context: Context, keyword: str) -> None: err = context.skill_parse_error assert err is not None, "Expected a parse error but none was raised" assert keyword.lower() in str(err).lower(), ( f"Expected error to mention '{keyword}', got: {err}" ) # --------------------------------------------------------------------------- # Then — Assertions for folder loading # --------------------------------------------------------------------------- @then('the loaded agent skill name should be "{expected}"') def step_loaded_skill_name(context: Context, expected: str) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.spec.name == expected, ( f"Expected skill name '{expected}', got '{skill.spec.name}'" ) @then('the loaded agent skill description should be "{expected}"') def step_loaded_skill_description(context: Context, expected: str) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.spec.description == expected, ( f"Expected description '{expected}', got '{skill.spec.description}'" ) @then('the loaded agent skill script paths should include "{filename}"') def step_loaded_skill_script_paths_include(context: Context, filename: str) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" names = [p.name for p in skill.script_paths] assert filename in names, ( f"Expected script paths to include '{filename}', got: {names}" ) @then('the loaded agent skill reference paths should include "{filename}"') def step_loaded_skill_reference_paths_include(context: Context, filename: str) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" names = [p.name for p in skill.reference_paths] assert filename in names, ( f"Expected reference paths to include '{filename}', got: {names}" ) @then('the loaded agent skill asset paths should include "{filename}"') def step_loaded_skill_asset_paths_include(context: Context, filename: str) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" names = [p.name for p in skill.asset_paths] assert filename in names, ( f"Expected asset paths to include '{filename}', got: {names}" ) @then("the loaded agent skill script paths should be empty") def step_loaded_skill_script_paths_empty(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.script_paths == [], ( f"Expected empty script paths, got: {skill.script_paths}" ) @then("the loaded agent skill reference paths should be empty") def step_loaded_skill_reference_paths_empty(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.reference_paths == [], ( f"Expected empty reference paths, got: {skill.reference_paths}" ) @then("the loaded agent skill asset paths should be empty") def step_loaded_skill_asset_paths_empty(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.asset_paths == [], ( f"Expected empty asset paths, got: {skill.asset_paths}" ) @then('the agent skill load error should mention "{keyword}"') def step_agent_skill_load_error_mentions(context: Context, keyword: str) -> None: err = context.skill_load_error assert err is not None, "Expected a load error but none was raised" assert keyword.lower() in str(err).lower(), ( f"Expected error to mention '{keyword}', got: {err}" ) # --------------------------------------------------------------------------- # Then — Assertions for tool descriptor mapping # --------------------------------------------------------------------------- @then('the tool descriptor source should be "{expected}"') def step_tool_descriptor_source(context: Context, expected: str) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" assert td.source == expected, f"Expected source '{expected}', got '{td.source}'" @then('the tool descriptor name should be "{expected}"') def step_tool_descriptor_name(context: Context, expected: str) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" assert td.name == expected, ( f"Expected tool descriptor name '{expected}', got '{td.name}'" ) @then("the tool descriptor should have read_only set to True") def step_tool_descriptor_read_only_true(context: Context) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" assert td.read_only is True, f"Expected read_only=True, got {td.read_only}" @then("the tool descriptor should have writes set to False") def step_tool_descriptor_writes_false(context: Context) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" assert td.writes is False, f"Expected writes=False, got {td.writes}" @then('the tool descriptor allowed tools should include "{tool_name}"') def step_tool_descriptor_allowed_tools_include( context: Context, tool_name: str ) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" assert tool_name in td.allowed_tools, ( f"Expected allowed_tools to include '{tool_name}', got: {td.allowed_tools}" ) @then("the tool descriptor agent_skill_path should be set") def step_tool_descriptor_agent_skill_path_set(context: Context) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" assert td.agent_skill_path is not None, "Expected agent_skill_path to be set" assert td.agent_skill_path != "", "Expected agent_skill_path to be non-empty" # --------------------------------------------------------------------------- # Then — Assertions for progressive disclosure # --------------------------------------------------------------------------- @then('the discover result should have name "{expected}"') def step_discover_result_name(context: Context, expected: str) -> None: dr = context.discover_result assert dr is not None, "No discover result available" assert dr.name == expected, f"Expected discover name '{expected}', got '{dr.name}'" @then('the discover result should have description "{expected}"') def step_discover_result_description(context: Context, expected: str) -> None: dr = context.discover_result assert dr is not None, "No discover result available" assert dr.description == expected, ( f"Expected discover description '{expected}', got '{dr.description}'" ) @then("the discover result body should be empty") def step_discover_result_body_empty(context: Context) -> None: dr = context.discover_result assert dr is not None, "No discover result available" assert dr.body == "" or dr.body is None, ( f"Expected empty body on discover, got: '{dr.body}'" ) @then('the activate result body should contain "{expected}"') def step_activate_result_body_contains(context: Context, expected: str) -> None: ar = context.activate_result assert ar is not None, "No activate result available" assert expected in ar.body, ( f"Expected activate body to contain '{expected}', got:\n{ar.body}" ) @then('the resources list should include "{filename}"') def step_resources_list_includes(context: Context, filename: str) -> None: resources = context.available_resources assert resources is not None, "No resources list available" names = [p.name for p in resources] assert filename in names, ( f"Expected resources to include '{filename}', got: {names}" ) @then("the agent skill body should be cleared") def step_agent_skill_body_cleared(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.active_body is None or skill.active_body == "", ( f"Expected body to be cleared after deactivate, got: '{skill.active_body}'" ) # --------------------------------------------------------------------------- # Then — Assertions for ordering # --------------------------------------------------------------------------- @then( "the parsed skill body steps should appear in order: " '"{first}", "{second}", "{third}"' ) def step_parsed_skill_body_order( context: Context, first: str, second: str, third: str ) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" body = result.body pos_first = body.find(first) pos_second = body.find(second) pos_third = body.find(third) assert pos_first != -1, f"'{first}' not found in body" assert pos_second != -1, f"'{second}' not found in body" assert pos_third != -1, f"'{third}' not found in body" assert pos_first < pos_second < pos_third, ( f"Expected order {first!r} < {second!r} < {third!r} in body, " f"got positions {pos_first}, {pos_second}, {pos_third}" ) # --------------------------------------------------------------------------- # Then — Assertions for path safety # --------------------------------------------------------------------------- @then("all script paths should be relative to the skill folder") def step_all_script_paths_relative_to_folder(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" folder = context.skill_folder for script_path in skill.script_paths: resolved = script_path.resolve() folder_resolved = folder.resolve() assert str(resolved).startswith(str(folder_resolved)), ( f"Script path '{resolved}' is outside skill folder '{folder_resolved}'" ) # --------------------------------------------------------------------------- # Given — Edge-case setup for coverage # --------------------------------------------------------------------------- @given("a non-existent SKILL.md path") def step_nonexistent_skill_md_path(context: Context) -> None: """Set a path to a SKILL.md that does not exist.""" context.skill_folder = Path("/tmp/__nonexistent_skill_folder_xyz__") context.skill_parse_error = None context.skill_parse_result = None @given("a SKILL.md with an empty frontmatter block") def step_skill_md_empty_frontmatter_block(context: Context) -> None: """Create a SKILL.md with empty frontmatter (--- followed immediately by ---).""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_empty_fm_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir (tmp_dir / "SKILL.md").write_text("---\n---\nsome body\n") context.skill_parse_error = None context.skill_parse_result = None @given("a SKILL.md with malformed YAML frontmatter") def step_skill_md_malformed_yaml(context: Context) -> None: """Create a SKILL.md with YAML that cannot be parsed.""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_bad_yaml_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir (tmp_dir / "SKILL.md").write_text("---\nname: [\n---\nbody\n") context.skill_parse_error = None context.skill_parse_result = None @given("a SKILL.md with a YAML list as frontmatter instead of a mapping") def step_skill_md_yaml_list_frontmatter(context: Context) -> None: """Create a SKILL.md with a YAML list (not a mapping) as frontmatter.""" import tempfile tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_list_fm_")) context._cleanup_handlers.append( lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True) ) context.skill_folder = tmp_dir (tmp_dir / "SKILL.md").write_text("---\n- item1\n- item2\n---\nbody\n") context.skill_parse_error = None context.skill_parse_result = None @given("an agent skills folder path that is a file") def step_agent_skills_folder_is_a_file(context: Context) -> None: """Create a temporary file and use its path as if it were a folder.""" import os import tempfile fd, tmp_path = tempfile.mkstemp(prefix="agent_skill_file_", suffix=".md") os.close(fd) context._cleanup_handlers.append(lambda: Path(tmp_path).unlink(missing_ok=True)) context.skill_folder = Path(tmp_path) context.skill_load_error = None context.loaded_skill = None @given("the scripts folder contains a nested sub-directory") def step_scripts_folder_has_nested_subdir(context: Context) -> None: """Add a nested sub-directory inside scripts/ (should be skipped).""" scripts_dir: Path = context.skill_folder / "scripts" scripts_dir.mkdir(exist_ok=True) nested = scripts_dir / "nested_subdir" nested.mkdir(exist_ok=True) # --------------------------------------------------------------------------- # When — Edge-case parse actions # --------------------------------------------------------------------------- @when("I parse the non-existent SKILL.md file expecting an error") def step_parse_nonexistent_skill_md(context: Context) -> None: """Attempt to parse a SKILL.md at a path that doesn't exist.""" skill_md_path = context.skill_folder / "SKILL.md" try: context.skill_parse_result = AgentSkillSpec.from_file(skill_md_path) context.skill_parse_error = None except (ValueError, FileNotFoundError) as exc: context.skill_parse_error = exc context.skill_parse_result = None @when("I construct an AgentSkillLoader with None spec expecting an error") def step_construct_loader_none_spec(context: Context) -> None: """Attempt to construct AgentSkillLoader with spec=None.""" try: AgentSkillLoader( # type: ignore[call-arg] spec=None, folder=Path("/tmp"), script_paths=[], reference_paths=[], asset_paths=[], ) context.skill_load_error = None except (ValueError, TypeError) as exc: context.skill_load_error = exc @when("I construct an AgentSkillLoader with None folder expecting an error") def step_construct_loader_none_folder(context: Context) -> None: """Attempt to construct AgentSkillLoader with folder=None.""" spec = AgentSkillSpec.from_string( "---\nname: local/test\ndescription: test\n---\nbody\n" ) try: AgentSkillLoader( # type: ignore[call-arg] spec=spec, folder=None, script_paths=[], reference_paths=[], asset_paths=[], ) context.skill_load_error = None except (ValueError, TypeError, AttributeError) as exc: context.skill_load_error = exc @when("I call from_folder with None expecting an error") def step_call_from_folder_none(context: Context) -> None: """Call AgentSkillLoader.from_folder(None) and capture the error.""" try: AgentSkillLoader.from_folder(None) # type: ignore[arg-type] context.skill_load_error = None except (ValueError, TypeError, AttributeError) as exc: context.skill_load_error = exc # --------------------------------------------------------------------------- # Then — Edge-case assertions # --------------------------------------------------------------------------- @then("the loaded agent skill folder property should be set") def step_loaded_skill_folder_property(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.folder is not None, "Expected folder property to be set" assert skill.folder.is_dir(), ( f"Expected folder to be a directory, got: {skill.folder}" ) @then("the loaded agent skill script paths should not include any directories") def step_loaded_skill_script_paths_no_dirs(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" for p in skill.script_paths: assert p.is_file(), ( f"Expected only files in script_paths, but found directory: {p}" ) # --------------------------------------------------------------------------- # Then — Assertions for structured steps # --------------------------------------------------------------------------- @then("the parsed skill steps should have {count:d} entries") def step_parsed_skill_steps_count(context: Context, count: int) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" actual = len(result.steps) assert actual == count, f"Expected {count} step(s), got {actual}: {result.steps}" @then('the parsed skill step {index:d} content should be "{expected}"') def step_parsed_skill_step_content(context: Context, index: int, expected: str) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert len(result.steps) >= index, ( f"Expected at least {index} step(s), got {len(result.steps)}" ) actual = result.steps[index - 1].content assert actual == expected, ( f"Expected step {index} content '{expected}', got '{actual}'" ) @then("the parsed skill step {index:d} index should be {expected:d}") def step_parsed_skill_step_index(context: Context, index: int, expected: int) -> None: result = context.skill_parse_result assert result is not None, "No parsed skill result available" assert len(result.steps) >= index, ( f"Expected at least {index} step(s), got {len(result.steps)}" ) actual = result.steps[index - 1].index assert actual == expected, ( f"Expected step {index} to have index={expected}, got {actual}" ) # --------------------------------------------------------------------------- # Then — Assertions for resource binding slots # --------------------------------------------------------------------------- @then("the tool descriptor should have {count:d} resource slot(s)") def step_tool_descriptor_resource_slot_count(context: Context, count: int) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" actual = len(td.resource_slots) assert actual == count, ( f"Expected {count} resource slot(s), got {actual}: {td.resource_slots}" ) @then('the tool descriptor resource slot "{name}" should have access "{access}"') def step_tool_descriptor_resource_slot_access( context: Context, name: str, access: str ) -> None: td = context.tool_descriptor assert td is not None, "No tool descriptor available" slot = next((s for s in td.resource_slots if s.name == name), None) assert slot is not None, ( f"No resource slot named '{name}' found in: " f"{[s.name for s in td.resource_slots]}" ) assert slot.access == access, ( f"Expected slot '{name}' access='{access}', got '{slot.access}'" ) @then("the discover result should have {count:d} resource slot(s)") def step_discover_result_resource_slot_count(context: Context, count: int) -> None: dr = context.discover_result assert dr is not None, "No discover result available" actual = len(dr.resource_slots) assert actual == count, ( f"Expected {count} resource slot(s) in discover result, " f"got {actual}: {dr.resource_slots}" ) @then('the discover result resource slot "{name}" should have access "{access}"') def step_discover_result_resource_slot_access( context: Context, name: str, access: str ) -> None: dr = context.discover_result assert dr is not None, "No discover result available" slot = next((s for s in dr.resource_slots if s.name == name), None) assert slot is not None, ( f"No resource slot named '{name}' in discover result: " f"{[s.name for s in dr.resource_slots]}" ) assert slot.access == access, ( f"Expected discover slot '{name}' access='{access}', got '{slot.access}'" ) @then("the loaded skill resource_slots property should be empty") def step_loaded_skill_resource_slots_property_empty(context: Context) -> None: skill = context.loaded_skill assert skill is not None, "No loaded skill available" assert skill.resource_slots == [], ( f"Expected empty resource_slots property, got: {skill.resource_slots}" )