Files
temp/features/steps/subplan_service_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

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}"
)