forked from HAL9000/cleveragents-core
a808c395f9
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
288 lines
10 KiB
Python
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}"
|
|
)
|