Files
temp/features/steps/skills_registry_coverage_boost_steps.py
freemo a808c395f9 test(coverage): add Behave BDD tests to improve unit test coverage across 53 source modules
Add 53 new .feature files and corresponding step definition files targeting
uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts
in 7 pre-existing step files by disambiguating step text.

New tests cover: ACP clients/facade, actor CLI/config, application container,
ACMS service/strategies, async worker, automation profile CLI, autonomy
guardrail, bridge, change model, config CLI/service, context service,
cross-plan correction, database models, decision service, decomposition
clustering/service, discovery handler, langchain chat provider, langgraph
nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/
preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI,
provider registry, reactive application/route, repositories, resolver handler,
resource registry service, resume model, retry patterns, sandbox protocol,
server CLI, skill CLI/service, skills registry, subplan execution/service,
system CLI, UKO loader, UoW, and YAML template engine.

Closes #645
2026-03-09 13:01:58 -04:00

288 lines
10 KiB
Python

"""Step definitions for SkillRegistry coverage boost.
Targets uncovered lines in src/cleveragents/skills/registry.py:
- Lines 270-278: validate_plan tool-registry validation loop
- Lines 314-315: refresh exception handler for ValueError/RuntimeError
- Line 324: refresh skip for inline/mcp/agent_skill entries
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
from behave.runner import Context
from cleveragents.domain.models.core.skill import (
Skill,
SkillAgentSource,
SkillInclude,
SkillInlineTool,
SkillMcpSource,
)
from cleveragents.domain.models.core.tool import ToolSource
from cleveragents.skills.protocol import SkillDefinition, SkillMetadata
from cleveragents.skills.refresh import SkillRefreshResult
from cleveragents.skills.registry import SkillRegistry
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_skill(
name: str,
description: str = "test skill",
tool_refs: list[str] | None = None,
includes: list[SkillInclude] | None = None,
anonymous_tools: list[SkillInlineTool] | None = None,
mcp_servers: list[SkillMcpSource] | None = None,
agent_skills: list[SkillAgentSource] | None = None,
) -> Skill:
return Skill(
name=name,
description=description,
tool_refs=tool_refs or [],
includes=includes or [],
anonymous_tools=anonymous_tools or [],
mcp_servers=mcp_servers or [],
agent_skills=agent_skills or [],
)
def _make_definition(skill: Skill, resolve: bool = True) -> SkillDefinition:
if resolve:
metadata = SkillMetadata.from_skill(skill)
else:
# Build metadata without resolving (avoids ValueError for missing includes)
metadata = SkillMetadata(
name=skill.name,
description=skill.description,
)
return SkillDefinition(skill=skill, metadata=metadata)
def _register(context: Context, skill: Skill, resolve: bool = True) -> None:
defn = _make_definition(skill, resolve=resolve)
context.skill_reg.register(defn)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a fresh SkillRegistry instance")
def step_fresh_registry(context: Context) -> None:
context.skill_reg = SkillRegistry()
context.tool_registry_mock = None
context.validation_errors = None
context.refresh_result = None
context.resolver_patch_runtime_error = False
# ---------------------------------------------------------------------------
# Given: tool registry mock
# ---------------------------------------------------------------------------
@given("the registry has a tool registry mock configured")
def step_tool_registry_mock(context: Context) -> None:
mock = MagicMock()
mock.get_tool.return_value = None
mock.get.return_value = None
context.tool_registry_mock = mock
context.skill_reg = SkillRegistry(tool_registry=mock)
# ---------------------------------------------------------------------------
# Given: register skills (unique step text per variant)
# ---------------------------------------------------------------------------
@given('a registered skill "{name}" having tool ref "{ref}"')
def step_skill_with_tool_ref(context: Context, name: str, ref: str) -> None:
_register(context, _make_skill(name=name, tool_refs=[ref]))
@given('a registered skill "{name}" having only an inline tool')
def step_skill_with_inline(context: Context, name: str) -> None:
inline = SkillInlineTool(
description="An inline test tool",
source=ToolSource.CUSTOM,
timeout=300,
)
_register(context, _make_skill(name=name, anonymous_tools=[inline]))
@given('a registered skill "{name}" having only MCP tools')
def step_skill_with_mcp_only(context: Context, name: str) -> None:
mcp_source = SkillMcpSource(server="myserver", tools=["read"])
_register(context, _make_skill(name=name, mcp_servers=[mcp_source]))
@given('a registered skill "{name}" having only agent skill sources')
def step_skill_with_agent_only(context: Context, name: str) -> None:
agent = SkillAgentSource(path="/skills/my-agent")
_register(context, _make_skill(name=name, agent_skills=[agent]))
@given('a registered skill "{name}" including "{other}"')
def step_skill_with_include(context: Context, name: str, other: str) -> None:
skill = _make_skill(name=name, includes=[SkillInclude(name=other)])
_register(context, skill, resolve=False)
@given('a registered skill "{name}" that will cause RuntimeError on resolve')
def step_skill_with_runtime_error(context: Context, name: str) -> None:
_register(context, _make_skill(name=name))
context.resolver_patch_runtime_error = True
@given('a registered skill "{name}" having inline tool plus tool ref "{ref}"')
def step_skill_inline_and_ref(context: Context, name: str, ref: str) -> None:
inline = SkillInlineTool(
description="Inline lint helper",
source=ToolSource.CUSTOM,
timeout=300,
)
_register(
context, _make_skill(name=name, tool_refs=[ref], anonymous_tools=[inline])
)
@given('a registered skill "{name}" having MCP tools plus tool ref "{ref}"')
def step_skill_mcp_and_ref(context: Context, name: str, ref: str) -> None:
mcp_source = SkillMcpSource(server="myserver", tools=["read"])
_register(
context, _make_skill(name=name, tool_refs=[ref], mcp_servers=[mcp_source])
)
@given('a registered skill "{name}" having agent skill plus tool ref "{ref}"')
def step_skill_agent_and_ref(context: Context, name: str, ref: str) -> None:
agent = SkillAgentSource(path="/skills/my-agent")
_register(context, _make_skill(name=name, tool_refs=[ref], agent_skills=[agent]))
# ---------------------------------------------------------------------------
# Given: tool registry mock responses
# ---------------------------------------------------------------------------
@given('the tool registry mock returns None for tool "{tool_name}"')
def step_mock_returns_none(context: Context, tool_name: str) -> None:
mock: MagicMock = context.tool_registry_mock
mock.get_tool.return_value = None
@given('the tool registry mock returns a tool for "{tool_name}"')
def step_mock_returns_tool(context: Context, tool_name: str) -> None:
mock: MagicMock = context.tool_registry_mock
tool_obj = MagicMock()
tool_obj.name = tool_name
def _get_tool(name: str) -> Any:
if name == tool_name:
return tool_obj
return None
def _get(name: str) -> Any:
if name == tool_name:
return tool_obj
return None
mock.get_tool.side_effect = _get_tool
mock.get.side_effect = _get
# ---------------------------------------------------------------------------
# When: validate_plan
# ---------------------------------------------------------------------------
@when('I validate a plan referencing skill "{skill_name}"')
def step_validate_plan(context: Context, skill_name: str) -> None:
plan: dict[str, Any] = {"skills": [skill_name]}
context.validation_errors = context.skill_reg.validate_plan(plan)
# ---------------------------------------------------------------------------
# When: refresh
# ---------------------------------------------------------------------------
@when('I refresh the coverage boost skill "{name}"')
def step_refresh_skill_coverage(context: Context, name: str) -> None:
if context.resolver_patch_runtime_error:
with patch(
"cleveragents.skills.registry.SkillResolver.resolve_tools",
side_effect=RuntimeError("resolver broke"),
):
context.refresh_result = context.skill_reg.refresh(name)
else:
context.refresh_result = context.skill_reg.refresh(name)
# ---------------------------------------------------------------------------
# Then: validation_errors assertions
# ---------------------------------------------------------------------------
@then('the skills validation errors should contain "{message}"')
def step_errors_contain(context: Context, message: str) -> None:
errors: list[str] = context.validation_errors
assert errors is not None, "No validation errors captured"
matching = [e for e in errors if message in e]
assert matching, f"Expected error containing '{message}', got: {errors}"
# REMOVED: Duplicate @then step - already defined in skill_context_steps.py
@then("the tool registry mock get_tool should not have been called")
def step_mock_not_called(context: Context) -> None:
mock: MagicMock = context.tool_registry_mock
mock.get_tool.assert_not_called()
# ---------------------------------------------------------------------------
# Then: refresh result assertions
# ---------------------------------------------------------------------------
@then('the refresh result should have "{name}" in failed')
def step_refresh_failed(context: Context, name: str) -> None:
result: SkillRefreshResult = context.refresh_result
assert result is not None, "No refresh result captured"
assert name in result.failed, (
f"Expected '{name}' in failed, got failed={result.failed}"
)
@then("the refresh result should have zero refreshed")
def step_refresh_zero_refreshed(context: Context) -> None:
result: SkillRefreshResult = context.refresh_result
assert result is not None, "No refresh result captured"
assert len(result.refreshed) == 0, f"Expected 0 refreshed, got {result.refreshed}"
@then('the refresh failed message for "{name}" should contain "{text}"')
def step_refresh_failed_message(context: Context, name: str, text: str) -> None:
result: SkillRefreshResult = context.refresh_result
assert name in result.failed, f"'{name}' not in failed: {result.failed}"
assert text in result.failed[name], (
f"Expected '{text}' in message, got: {result.failed[name]}"
)
@then('the refresh result should have "{name}" in refreshed')
def step_refresh_refreshed(context: Context, name: str) -> None:
result: SkillRefreshResult = context.refresh_result
assert result is not None, "No refresh result captured"
assert name in result.refreshed, (
f"Expected '{name}' in refreshed, got refreshed={result.refreshed}, "
f"failed={result.failed}"
)