"""Step definitions for ``project_context_cov3.feature``. Targets uncovered lines 156, 175, 350, 674-675, 678, 864-869 in ``src/cleveragents/cli/commands/project_context.py``. Uses the ``pccov3`` prefix on all step text to avoid Behave AmbiguousStep collisions with existing step files. """ from __future__ import annotations import json as _json from io import StringIO from typing import Any from unittest.mock import MagicMock, patch from behave import given, then, when # type: ignore[import-untyped] from sqlalchemy import create_engine, text from sqlalchemy.orm import Session, sessionmaker # ------------------------------------------------------------------ # Session wrapper - prevents close() from destroying the shared # in-memory session; rollback() resets dirty state between calls. # ------------------------------------------------------------------ class _SafeSession: """Thin wrapper: ``close()`` → ``rollback()`` instead of destroy.""" def __init__(self, real: Session) -> None: object.__setattr__(self, "_real", real) def close(self) -> None: real: Session = object.__getattribute__(self, "_real") real.rollback() def __getattr__(self, name: str) -> Any: return getattr(object.__getattribute__(self, "_real"), name) def __setattr__(self, name: str, value: Any) -> None: setattr(object.__getattribute__(self, "_real"), name, value) def _make_session_factory(context: Any) -> tuple[Any, Any]: from cleveragents.infrastructure.database.models import Base engine = create_engine( "sqlite:///:memory:", echo=False, connect_args={"check_same_thread": False}, ) Base.metadata.create_all(engine) real = sessionmaker( bind=engine, expire_on_commit=False, autoflush=True, autocommit=False )() wrapper = _SafeSession(real) return engine, lambda: wrapper # ------------------------------------------------------------------ # Mock container + runner # ------------------------------------------------------------------ def _mock_container(context: Any) -> MagicMock: from cleveragents.application.services.context_tiers import ContextTierService mc = MagicMock() mc.namespaced_project_repo.return_value = context.pccov3_project_repo mc.session_factory.return_value = context.pccov3_session_factory if not hasattr(context, "pccov3_tier_service"): context.pccov3_tier_service = ContextTierService() mc.context_tier_service.return_value = context.pccov3_tier_service return mc def _run(context: Any, func: Any, *args: Any, **kwargs: Any) -> None: """Execute a CLI function with a mocked DI container.""" import typer from rich.console import Console as RichConsole mc = _mock_container(context) buf = StringIO() test_console = RichConsole( file=buf, no_color=True, highlight=False, force_terminal=False, width=500, soft_wrap=True, ) def _test_format_output(data: Any, format_type: Any) -> Any: import sys as _sys from io import StringIO as _SIO from cleveragents.cli.formatting import format_output as _fo _b = _SIO() _old = _sys.stdout _sys.stdout = _b try: _r = _fo(data, format_type) finally: _sys.stdout = _old return _r or _b.getvalue().rstrip("\n") with ( patch( "cleveragents.application.container.get_container", return_value=mc, ), patch( "cleveragents.cli.commands.project_context.console", test_console, ), patch( "cleveragents.cli.commands.project_context.err_console", test_console, ), patch( "cleveragents.cli.commands.project_context.format_output", _test_format_output, ), ): try: func(*args, **kwargs) context.pccov3_exit_code = 0 except typer.Exit as exc: context.pccov3_exit_code = exc.exit_code except typer.Abort: context.pccov3_exit_code = 1 context.pccov3_output = buf.getvalue() # ------------------------------------------------------------------ # Helper: seed raw policy blobs into the DB # ------------------------------------------------------------------ def _seed_policy_blob(context: Any, ns: str, blob: dict) -> None: """Write a raw JSON blob into ns_projects.context_policy_json.""" session = context.pccov3_session_factory() session.execute( text( "UPDATE ns_projects SET context_policy_json = :blob " "WHERE namespaced_name = :ns" ), {"blob": _json.dumps(blob), "ns": ns}, ) session.commit() # ------------------------------------------------------------------ # Background steps # ------------------------------------------------------------------ @given("a pccov3 in-memory database is initialized") def step_pccov3_init_db(context: Any) -> None: from cleveragents.infrastructure.database.repositories import ( NamespacedProjectRepository, ) engine, sf = _make_session_factory(context) context.pccov3_engine = engine context.pccov3_session_factory = sf context.pccov3_project_repo = NamespacedProjectRepository(session_factory=sf) context.pccov3_output = "" context.pccov3_exit_code = 0 context.pccov3_error = None @given('a project "{name}" exists for pccov3') def step_pccov3_create_project(context: Any, name: str) -> None: from cleveragents.domain.models.core.project import ( NamespacedProject, parse_namespaced_name, ) parsed = parse_namespaced_name(name) proj = NamespacedProject(name=parsed.name, namespace=parsed.namespace) context.pccov3_project_repo.create(proj) # ------------------------------------------------------------------ # Given steps # ------------------------------------------------------------------ @given('the pccov3 tier service has a fragment with long content for "{project}"') def step_pccov3_seed_long_content_fragment(context: Any, project: str) -> None: """Seed the tier service with a fragment whose content exceeds 100 chars. This ensures line 350 (content truncation) is exercised in ``_simulate_context_assembly``. """ from cleveragents.application.services.context_tiers import ContextTierService from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment svc = ContextTierService() svc.store( TieredFragment( fragment_id="pccov3-long-frag", content="X" * 200, # 200 chars, well over the 100-char threshold tier=ContextTier.HOT, resource_id="res:pccov3-long", project_name=project, token_count=50, access_count=3, ) ) context.pccov3_tier_service = svc @given( 'I pccov3 seed execution environment "{env}" and priority "{priority}" for "{project}"' ) def step_pccov3_seed_ee_and_eep( context: Any, env: str, priority: str, project: str ) -> None: """Seed a policy blob with both execution_environment and execution_env_priority. This ensures lines 864-869 are exercised when context_show renders the rich output with both ee and eep present. """ from cleveragents.cli.commands.project_context import _default_acms_config blob = { "default_view": { "include_resources": [], "exclude_resources": [], "include_paths": [], "exclude_paths": [], "max_file_size": None, "max_total_size": None, }, "acms_config": _default_acms_config(), "execution_environment": env, "execution_env_priority": priority, } _seed_policy_blob(context, project, blob) @given('I pccov3 seed execution environment "{env}" without priority for "{project}"') def step_pccov3_seed_ee_only(context: Any, env: str, project: str) -> None: """Seed a policy blob with execution_environment only (no priority).""" from cleveragents.cli.commands.project_context import _default_acms_config blob = { "default_view": { "include_resources": [], "exclude_resources": [], "include_paths": [], "exclude_paths": [], "max_file_size": None, "max_total_size": None, }, "acms_config": _default_acms_config(), "execution_environment": env, } _seed_policy_blob(context, project, blob) @given( 'I pccov3 seed priority "{priority}" without execution environment for "{project}"' ) def step_pccov3_seed_eep_only(context: Any, priority: str, project: str) -> None: """Seed a policy blob with execution_env_priority only (no ee).""" from cleveragents.cli.commands.project_context import _default_acms_config blob = { "default_view": { "include_resources": [], "exclude_resources": [], "include_paths": [], "exclude_paths": [], "max_file_size": None, "max_total_size": None, }, "acms_config": _default_acms_config(), "execution_env_priority": priority, } _seed_policy_blob(context, project, blob) # ------------------------------------------------------------------ # When steps # ------------------------------------------------------------------ @when('I pccov3 read the policy for "{project}"') def step_pccov3_read_policy(context: Any, project: str) -> None: """Directly call _read_policy to exercise line 156.""" from cleveragents.cli.commands.project_context import _read_policy context.pccov3_policy = _read_policy(context.pccov3_session_factory, project) @when('I pccov3 read the ACMS config for "{project}"') def step_pccov3_read_acms(context: Any, project: str) -> None: """Directly call _read_acms_config to exercise line 175.""" from cleveragents.cli.commands.project_context import _read_acms_config context.pccov3_acms = _read_acms_config(context.pccov3_session_factory, project) @when('I pccov3 run simulate_context_assembly for "{project}"') def step_pccov3_simulate_assembly(context: Any, project: str) -> None: """Run _simulate_context_assembly to exercise line 350 (content truncation).""" from cleveragents.cli.commands.project_context import ( _default_acms_config, _simulate_context_assembly, ) # Patch _get_context_tier_service to return our test tier service patcher = patch( "cleveragents.cli.commands.project_context._get_context_tier_service", return_value=context.pccov3_tier_service, ) patcher.start() context.add_cleanup(patcher.stop) try: context.pccov3_assembled = _simulate_context_assembly( project_name=project, acms_config=_default_acms_config(), budget_tokens=10000, # large budget so fragment is included ) except Exception as exc: context.pccov3_error = exc @when('I pccov3 run context set on "{project}" with depth-gradient "{gradient}"') def step_pccov3_set_depth_gradient(context: Any, project: str, gradient: str) -> None: """Run context_set with a depth-gradient to exercise lines 674-678.""" from cleveragents.cli.commands.project_context import context_set _run( context, context_set, project=project, view="default", depth_gradient=[gradient], ) @when('I pccov3 run context show on "{project}" without view') def step_pccov3_show_no_view(context: Any, project: str) -> None: """Run context_show without a view to exercise lines 864-869.""" from cleveragents.cli.commands.project_context import context_show _run(context, context_show, project=project) # ------------------------------------------------------------------ # Then steps # ------------------------------------------------------------------ @then("the pccov3 policy should be a default ProjectContextPolicy") def step_pccov3_assert_default_policy(context: Any) -> None: from cleveragents.domain.models.core.context_policy import ProjectContextPolicy default = ProjectContextPolicy() assert context.pccov3_policy.default_view == default.default_view, ( f"Expected default view {default.default_view}, " f"got {context.pccov3_policy.default_view}" ) @then("the pccov3 policy strategize_view should be None") def step_pccov3_assert_strat_none(context: Any) -> None: assert context.pccov3_policy.strategize_view is None, ( f"Expected None, got {context.pccov3_policy.strategize_view}" ) @then("the pccov3 policy execute_view should be None") def step_pccov3_assert_exec_none(context: Any) -> None: assert context.pccov3_policy.execute_view is None, ( f"Expected None, got {context.pccov3_policy.execute_view}" ) @then("the pccov3 policy apply_view should be None") def step_pccov3_assert_apply_none(context: Any) -> None: assert context.pccov3_policy.apply_view is None, ( f"Expected None, got {context.pccov3_policy.apply_view}" ) @then("the pccov3 ACMS config should equal the default ACMS config") def step_pccov3_assert_default_acms(context: Any) -> None: from cleveragents.cli.commands.project_context import _default_acms_config expected = _default_acms_config() assert context.pccov3_acms == expected, ( f"Expected {expected}, got {context.pccov3_acms}" ) @then('the pccov3 assembled fragments should have truncated content ending with "..."') def step_pccov3_assert_truncated_content(context: Any) -> None: assert context.pccov3_error is None, f"Unexpected error: {context.pccov3_error}" assembled = context.pccov3_assembled assert len(assembled.fragments) > 0, "No fragments in assembled context" # The fragment content is stored in the ContextFragment's content field. # In the simulation, content > 100 chars gets truncated to [:100] + "..." frag = assembled.fragments[0] assert frag.content.endswith("..."), ( f"Expected content to end with '...', got: {frag.content!r}" ) # The truncated content should be 103 chars (100 + "...") assert len(frag.content) == 103, f"Expected 103 chars, got {len(frag.content)}" @then("the pccov3 command should have exit code {code:d}") def step_pccov3_assert_exit_code(context: Any, code: int) -> None: assert context.pccov3_exit_code == code, ( f"Expected exit code {code}, got {context.pccov3_exit_code}. " f"Output: {context.pccov3_output}" ) @then("the pccov3 command should succeed") def step_pccov3_assert_success(context: Any) -> None: assert context.pccov3_exit_code == 0, ( f"Expected exit 0, got {context.pccov3_exit_code}. " f"Output: {context.pccov3_output}" ) @then('the pccov3 output should contain "{text}"') def step_pccov3_assert_output_contains(context: Any, text: str) -> None: assert text in context.pccov3_output, ( f"Expected '{text}' in output, got: {context.pccov3_output!r}" )