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