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

266 lines
9.9 KiB
Python

"""Step definitions for multi_project_service_coverage_boost.feature.
Targets uncovered lines in multi_project_service.py:
- Line 59: constructor ValueError when decision_service is None
- Line 66: decision_service property accessor
- Line 93: initialize_scopes ValueError when plan is None
- Line 95: initialize_scopes ValueError when available_resources is None
- Line 157: resolve_context_view ValueError when plan is None
- Line 196: record_changeset ValueError when plan is None
- Lines 198-199: record_changeset ValueError when metadata not initialized
- Line 253: validate_cross_project returns error for None plan
- Line 255: validate_cross_project returns [] for plan without metadata
- Lines 283-285: validate_cross_project detects unknown source project
"""
from __future__ import annotations
from unittest.mock import MagicMock
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.multi_project_service import (
MultiProjectService,
)
from cleveragents.domain.models.core.multi_project import (
ChangeSetSummary,
CrossProjectDependency,
)
from cleveragents.domain.models.core.plan import (
NamespacedName,
Plan,
PlanIdentity,
PlanPhase,
ProcessingState,
ProjectLink,
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_PLAN_ID = "01HXBBBBBBBBBBBBBBBBBBBBBB"
def _make_plan(project_links: list[ProjectLink]) -> Plan:
"""Build a minimal Plan with the given project links."""
return Plan(
identity=PlanIdentity(plan_id=_PLAN_ID),
namespaced_name=NamespacedName.parse("local/cov-boost-test"),
description="Coverage boost test plan",
action_name="local/test-action",
phase=PlanPhase.STRATEGIZE,
processing_state=ProcessingState.QUEUED,
project_links=project_links,
)
def _mock_decision_service() -> MagicMock:
mock = MagicMock()
mock.list_by_type.return_value = []
return mock
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a fresh multi-project service")
def step_fresh_service(context: Context) -> None:
context.decision_service_mock = _mock_decision_service()
context.service = MultiProjectService(
decision_service=context.decision_service_mock,
)
context.caught_exception = None
context.validation_result = None
context.plan = None
# ---------------------------------------------------------------------------
# Constructor guard - None decision_service (line 59)
# ---------------------------------------------------------------------------
@when("I create a MultiProjectService with a None decision_service")
def step_create_service_none(context: Context) -> None:
try:
MultiProjectService(decision_service=None) # type: ignore[arg-type]
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# decision_service property (line 66)
# ---------------------------------------------------------------------------
@then("the decision_service property should return the injected mock")
def step_check_property(context: Context) -> None:
assert context.service.decision_service is context.decision_service_mock, (
"decision_service property did not return the injected mock"
)
# ---------------------------------------------------------------------------
# initialize_scopes - None plan (line 93)
# ---------------------------------------------------------------------------
@when("I call initialize_scopes with a None plan")
def step_init_scopes_none_plan(context: Context) -> None:
try:
context.service.initialize_scopes(None, {}) # type: ignore[arg-type]
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# initialize_scopes - None available_resources (line 95)
# ---------------------------------------------------------------------------
@given("a simple two-project plan")
def step_simple_two_project_plan(context: Context) -> None:
context.project_links = [
ProjectLink(project_name="proj-a"),
ProjectLink(project_name="proj-b"),
]
context.plan = _make_plan(context.project_links)
@when("I call initialize_scopes with None available_resources")
def step_init_scopes_none_resources(context: Context) -> None:
try:
context.service.initialize_scopes(context.plan, None) # type: ignore[arg-type]
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# resolve_context_view - None plan (line 157)
# ---------------------------------------------------------------------------
@when('I call resolve_context_view with a None plan and project "{proj}"')
def step_resolve_ctx_none_plan(context: Context, proj: str) -> None:
try:
context.service.resolve_context_view(None, proj) # type: ignore[arg-type]
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# record_changeset - None plan (line 196)
# ---------------------------------------------------------------------------
@when("I call record_changeset with a None plan")
def step_record_changeset_none_plan(context: Context) -> None:
summary = ChangeSetSummary(project_name="any", files_changed=1)
try:
context.service.record_changeset(None, "any", summary) # type: ignore[arg-type]
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# record_changeset - metadata not initialized (lines 198-199)
# ---------------------------------------------------------------------------
@when("I call record_changeset on a plan without metadata")
def step_record_changeset_no_metadata(context: Context) -> None:
summary = ChangeSetSummary(project_name="proj-a", files_changed=1)
try:
context.service.record_changeset(context.plan, "proj-a", summary)
except ValueError as exc:
context.caught_exception = exc
# ---------------------------------------------------------------------------
# validate_cross_project - None plan (line 253)
# ---------------------------------------------------------------------------
@when("I call validate_cross_project with a None plan")
def step_validate_none_plan(context: Context) -> None:
context.validation_result = context.service.validate_cross_project(None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# validate_cross_project - no metadata (line 255)
# ---------------------------------------------------------------------------
@when("I call validate_cross_project on a plan without metadata")
def step_validate_no_metadata(context: Context) -> None:
context.validation_result = context.service.validate_cross_project(context.plan)
# ---------------------------------------------------------------------------
# validate_cross_project - unknown source project (lines 283-285)
# ---------------------------------------------------------------------------
@given("the plan has initialized scopes with resources")
def step_init_scopes_with_resources(context: Context) -> None:
resources = {"proj-a": ["r1"], "proj-b": ["r2"]}
context.plan = context.service.initialize_scopes(context.plan, resources)
@given(
'the plan has a cross-project dependency from unknown source "{src}" to known target "{tgt}"'
)
def step_add_unknown_source_dep(context: Context, src: str, tgt: str) -> None:
dep = CrossProjectDependency(
source_project=src,
target_project=tgt,
dependency_type="imports",
)
mp = context.plan.multi_project_metadata
assert mp is not None, "metadata should be initialized before adding dependencies"
updated_deps = [*list(mp.cross_project_dependencies), dep]
updated_mp = mp.model_copy(update={"cross_project_dependencies": updated_deps})
context.plan = context.plan.model_copy(
update={"multi_project_metadata": updated_mp},
)
@when("I call validate_cross_project on the prepared plan")
def step_validate_prepared(context: Context) -> None:
context.validation_result = context.service.validate_cross_project(context.plan)
# ---------------------------------------------------------------------------
# Shared Then clauses
# ---------------------------------------------------------------------------
@then('a ValueError should have been raised with message "{fragment}"')
def step_check_value_error(context: Context, fragment: str) -> None:
# Support two contexts: caught_exception (multi-project) and ontology_error (uko)
err = getattr(context, "caught_exception", None)
if err is None:
err = getattr(context, "ontology_error", None)
assert err is not None, "Expected a ValueError but none was raised"
assert isinstance(err, ValueError), f"Expected ValueError, got {type(err).__name__}"
assert fragment in str(err), f"Expected message containing '{fragment}', got: {err}"
@then('the validation result should contain "{fragment}"')
def step_check_validation_contains(context: Context, fragment: str) -> None:
assert context.validation_result is not None, "validation_result is None"
assert any(fragment in msg for msg in context.validation_result), (
f"Expected a message containing '{fragment}' in {context.validation_result}"
)
@then("the validation result should be an empty list")
def step_check_validation_empty(context: Context) -> None:
assert context.validation_result is not None, "validation_result is None"
assert context.validation_result == [], (
f"Expected empty list, got {context.validation_result}"
)