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
266 lines
9.9 KiB
Python
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}"
|
|
)
|