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
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""Step definitions for subplan service coverage boost scenarios.
|
|
|
|
Targets uncovered lines in subplan_service.py:
|
|
- Lines 129-131: SpawnValidationError.__init__
|
|
- Line 167: decision_service property
|
|
- Line 207: spawn() config=None guard
|
|
- Line 209: spawn() spawn_entries=None guard
|
|
- Line 220: spawn() raising SpawnValidationError
|
|
- Line 301: validate_spawn() config=None guard
|
|
- Line 303: validate_spawn() spawn_entries=None guard
|
|
- Lines 334-336: empty action_name validation
|
|
- Line 375: get_spawn_decisions() empty plan_id guard
|
|
- Line 414: build_spawn_entries() decisions=None guard
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.application.services.subplan_service import (
|
|
SpawnEntry,
|
|
SpawnValidationError,
|
|
SpawnValidationResult,
|
|
SubplanService,
|
|
)
|
|
from cleveragents.domain.models.core.decision import (
|
|
ContextSnapshot,
|
|
Decision,
|
|
DecisionType,
|
|
)
|
|
from cleveragents.domain.models.core.plan import (
|
|
ExecutionMode,
|
|
NamespacedName,
|
|
Plan,
|
|
PlanIdentity,
|
|
SubplanConfig,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Shared ULID constants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COV_PLAN_ID = "01HGZ6FE0AQDYTR4BXVQZ6CV00"
|
|
_COV_DEC_ID = "01HGZ6FE0AQDYTR4BXVQZ6CD00"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _mock_decision_service_cov() -> MagicMock:
|
|
"""Create a mock DecisionService for coverage tests."""
|
|
svc = MagicMock()
|
|
svc.list_by_type = MagicMock(return_value=[])
|
|
return svc
|
|
|
|
|
|
def _make_decision_cov(
|
|
decision_id: str = _COV_DEC_ID,
|
|
decision_type: DecisionType = DecisionType.SUBPLAN_SPAWN,
|
|
chosen_option: str = "local/cov-action",
|
|
) -> Decision:
|
|
"""Create a Decision for coverage tests."""
|
|
return Decision(
|
|
decision_id=decision_id,
|
|
plan_id=_COV_PLAN_ID,
|
|
decision_type=decision_type,
|
|
sequence_number=0,
|
|
question="Coverage test spawn?",
|
|
chosen_option=chosen_option,
|
|
context_snapshot=ContextSnapshot(),
|
|
)
|
|
|
|
|
|
def _make_plan_cov() -> Plan:
|
|
"""Create a minimal Plan for coverage tests."""
|
|
return Plan(
|
|
identity=PlanIdentity(plan_id=_COV_PLAN_ID),
|
|
namespaced_name=NamespacedName(namespace="local", name="cov-plan"),
|
|
description="Coverage boost test plan",
|
|
action_name="local/cov-action",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Given steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a subplan service constructed with a mock decision service")
|
|
def step_given_service_with_mock(context: Context) -> None:
|
|
context.cov_mock_ds = _mock_decision_service_cov()
|
|
context.cov_service = SubplanService(decision_service=context.cov_mock_ds)
|
|
|
|
|
|
@given("a parent plan for coverage tests")
|
|
def step_given_parent_plan_cov(context: Context) -> None:
|
|
context.cov_parent_plan = _make_plan_cov()
|
|
|
|
|
|
@given("a default subplan config for coverage tests")
|
|
def step_given_default_config_cov(context: Context) -> None:
|
|
context.cov_config = SubplanConfig()
|
|
|
|
|
|
@given("a subplan config with merge strategy forcibly set to None")
|
|
def step_given_config_merge_none(context: Context) -> None:
|
|
config = SubplanConfig(execution_mode=ExecutionMode.SEQUENTIAL)
|
|
# Force merge_strategy to None to trigger validation error
|
|
object.__setattr__(config, "merge_strategy", None)
|
|
context.cov_config = config
|
|
|
|
|
|
@given("a spawn entry with a valid spawn decision for coverage tests")
|
|
def step_given_valid_spawn_entry_cov(context: Context) -> None:
|
|
dec = _make_decision_cov()
|
|
context.cov_spawn_entries = [
|
|
SpawnEntry(
|
|
decision=dec,
|
|
action_name="local/cov-sub",
|
|
description="Coverage spawn entry",
|
|
)
|
|
]
|
|
|
|
|
|
@given("a spawn entry with an empty action_name")
|
|
def step_given_entry_empty_action(context: Context) -> None:
|
|
dec = _make_decision_cov()
|
|
context.cov_spawn_entries = [
|
|
SpawnEntry(
|
|
decision=dec,
|
|
action_name="",
|
|
description="Entry with empty action_name",
|
|
)
|
|
]
|
|
|
|
|
|
@given("a spawn entry with a whitespace-only action_name")
|
|
def step_given_entry_whitespace_action(context: Context) -> None:
|
|
dec = _make_decision_cov()
|
|
context.cov_spawn_entries = [
|
|
SpawnEntry(
|
|
decision=dec,
|
|
action_name=" ",
|
|
description="Entry with whitespace action_name",
|
|
)
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# When steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when('a SpawnValidationError is created with errors "{err1}" and "{err2}"')
|
|
def step_when_create_spawn_validation_error(
|
|
context: Context, err1: str, err2: str
|
|
) -> None:
|
|
context.cov_sve = SpawnValidationError([err1, err2])
|
|
|
|
|
|
@when("the decision_service property is accessed")
|
|
def step_when_access_property(context: Context) -> None:
|
|
context.cov_returned_ds = context.cov_service.decision_service
|
|
|
|
|
|
@when("spawn is called with config set to None")
|
|
def step_when_spawn_config_none(context: Context) -> None:
|
|
context.cov_error = None
|
|
dec = _make_decision_cov()
|
|
entries = [SpawnEntry(decision=dec, action_name="local/x")]
|
|
try:
|
|
context.cov_service.spawn(
|
|
parent_plan=context.cov_parent_plan,
|
|
config=None, # type: ignore[arg-type]
|
|
spawn_entries=entries,
|
|
)
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("spawn is called with spawn_entries set to None")
|
|
def step_when_spawn_entries_none(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.spawn(
|
|
parent_plan=context.cov_parent_plan,
|
|
config=context.cov_config,
|
|
spawn_entries=None, # type: ignore[arg-type]
|
|
)
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("spawn is called and validation is expected to fail")
|
|
def step_when_spawn_validation_fail(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.spawn(
|
|
parent_plan=context.cov_parent_plan,
|
|
config=context.cov_config,
|
|
spawn_entries=context.cov_spawn_entries,
|
|
)
|
|
except SpawnValidationError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("validate_spawn is called with config set to None")
|
|
def step_when_validate_config_none(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.validate_spawn(
|
|
config=None, # type: ignore[arg-type]
|
|
spawn_entries=context.cov_spawn_entries,
|
|
)
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("validate_spawn is called with spawn_entries set to None")
|
|
def step_when_validate_entries_none(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.validate_spawn(
|
|
config=context.cov_config,
|
|
spawn_entries=None, # type: ignore[arg-type]
|
|
)
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("validate_spawn is called for coverage tests")
|
|
def step_when_validate_cov(context: Context) -> None:
|
|
context.cov_validation_result = context.cov_service.validate_spawn(
|
|
config=context.cov_config,
|
|
spawn_entries=context.cov_spawn_entries,
|
|
)
|
|
|
|
|
|
@when("get_spawn_decisions is called with an empty plan_id")
|
|
def step_when_get_decisions_empty(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.get_spawn_decisions("")
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("get_spawn_decisions is called with a whitespace-only plan_id")
|
|
def step_when_get_decisions_whitespace(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.get_spawn_decisions(" ")
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("build_spawn_entries is called with decisions set to None")
|
|
def step_when_build_entries_none(context: Context) -> None:
|
|
context.cov_error = None
|
|
try:
|
|
context.cov_service.build_spawn_entries(None) # type: ignore[arg-type]
|
|
except ValueError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Then steps
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the SpawnValidationError validation_errors list should have {n:d} entries")
|
|
def step_then_sve_count(context: Context, n: int) -> None:
|
|
assert len(context.cov_sve.validation_errors) == n, (
|
|
f"Expected {n} errors, got {len(context.cov_sve.validation_errors)}"
|
|
)
|
|
|
|
|
|
@then('the SpawnValidationError message should contain "{text}"')
|
|
def step_then_sve_message(context: Context, text: str) -> None:
|
|
msg = str(context.cov_sve)
|
|
assert text in msg, f"Expected '{text}' in message: {msg}"
|
|
|
|
|
|
@then("the returned decision service should be the injected mock")
|
|
def step_then_ds_is_same(context: Context) -> None:
|
|
assert context.cov_returned_ds is context.cov_mock_ds, (
|
|
"Property did not return the injected decision service"
|
|
)
|
|
|
|
|
|
@then('a ValueError should be raised with text "{text}"')
|
|
def step_then_value_error_text(context: Context, text: str) -> None:
|
|
assert context.cov_error is not None, (
|
|
f"Expected ValueError with '{text}' but no error was raised"
|
|
)
|
|
assert isinstance(context.cov_error, ValueError), (
|
|
f"Expected ValueError, got {type(context.cov_error).__name__}"
|
|
)
|
|
assert text in str(context.cov_error), f"Expected '{text}' in '{context.cov_error}'"
|
|
|
|
|
|
@then("a SpawnValidationError should be raised")
|
|
def step_then_sve_raised(context: Context) -> None:
|
|
assert context.cov_error is not None, (
|
|
"Expected SpawnValidationError but no error was raised"
|
|
)
|
|
assert isinstance(context.cov_error, SpawnValidationError), (
|
|
f"Expected SpawnValidationError, got {type(context.cov_error).__name__}"
|
|
)
|
|
|
|
|
|
@then('the SpawnValidationError should mention "{text}"')
|
|
def step_then_sve_mentions(context: Context, text: str) -> None:
|
|
msg = str(context.cov_error)
|
|
assert text.lower() in msg.lower(), f"Expected '{text}' in error message: {msg}"
|
|
|
|
|
|
@then("the validation result should not be valid")
|
|
def step_then_invalid_cov(context: Context) -> None:
|
|
result: SpawnValidationResult = context.cov_validation_result
|
|
assert not result.valid, (
|
|
f"Expected invalid result, but got valid (errors={result.errors})"
|
|
)
|
|
|
|
|
|
@then('the validation errors should mention "{text}"')
|
|
def step_then_errors_mention(context: Context, text: str) -> None:
|
|
# Support two contexts: SpawnValidationResult (subplan) and plain list (uko)
|
|
cov_result = getattr(context, "cov_validation_result", None)
|
|
if cov_result is not None:
|
|
errors_joined = " ".join(cov_result.errors).lower()
|
|
assert text.lower() in errors_joined, (
|
|
f"Expected '{text}' in errors: {cov_result.errors}"
|
|
)
|
|
else:
|
|
errors_list = getattr(context, "validation_errors", None)
|
|
assert errors_list is not None, (
|
|
"Neither cov_validation_result nor validation_errors found on context"
|
|
)
|
|
errors_joined = " ".join(errors_list).lower()
|
|
assert text.lower() in errors_joined, (
|
|
f"Expected '{text}' in errors: {errors_list}"
|
|
)
|