"""Step definitions for project_context_cli_coverage_boost.feature. Targets uncovered lines in ``src/cleveragents/cli/commands/project_context.py``. """ 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 the production ``close()`` from # destroying our shared in-memory session. # ------------------------------------------------------------------ class _SafeSession: """Thin wrapper that prevents ``close()`` from destroying the shared session. Instead of a pure no-op, ``close()`` issues a ``rollback()`` on the underlying session to reset any dirty/rolled-back state accumulated during the previous call. This prevents phantom failures when the production error path (e.g. ``_save_policy_json`` rollback) leaves the session in an invalidated state. """ def __init__(self, real: Session) -> None: object.__setattr__(self, "_real", real) def close(self) -> None: """Reset session state without closing the underlying connection.""" 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 # ------------------------------------------------------------------ def _mock_container(context: Any) -> MagicMock: from cleveragents.application.services.context_tiers import ContextTierService mc = MagicMock() mc.namespaced_project_repo.return_value = context.cb_project_repo mc.session_factory.return_value = context.cb_session_factory if not hasattr(context, "cb_tier_service"): context.cb_tier_service = ContextTierService() mc.context_tier_service.return_value = context.cb_tier_service return mc def _run(context: Any, func: Any, *args: Any, **kwargs: Any) -> None: 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, format_type): 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.cb_exit_code = 0 except typer.Exit as exc: context.cb_exit_code = exc.exit_code except typer.Abort: context.cb_exit_code = 1 context.cb_output = buf.getvalue() # ------------------------------------------------------------------ # Background # ------------------------------------------------------------------ @given("a coverage-boost in-memory database is initialized") def step_cb_init_db(context: Any) -> None: from cleveragents.infrastructure.database.repositories import ( NamespacedProjectRepository, ) engine, sf = _make_session_factory(context) context.cb_engine = engine context.cb_session_factory = sf context.cb_project_repo = NamespacedProjectRepository(session_factory=sf) context.cb_output = "" context.cb_exit_code = 0 @given('a project "{name}" exists for coverage boost') def step_cb_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.cb_project_repo.create(proj) # ------------------------------------------------------------------ # Helper: direct DB operations for seeding # ------------------------------------------------------------------ def _seed_policy_blob(context: Any, ns: str, blob: dict) -> None: """Write a raw JSON blob into ns_projects.context_policy_json.""" session = context.cb_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() def _load_raw_blob(context: Any, ns: str) -> dict | None: session = context.cb_session_factory() row = session.execute( text("SELECT context_policy_json FROM ns_projects WHERE namespaced_name = :ns"), {"ns": ns}, ).fetchone() if row is None or row[0] is None: return None return _json.loads(row[0]) # ------------------------------------------------------------------ # Given steps # ------------------------------------------------------------------ @given('I seed ACMS config for "{project}" with hot_max_tokens {val:d}') def step_seed_acms_hot(context: Any, project: str, val: int) -> None: from cleveragents.cli.commands.project_context import _default_acms_config blob = {"acms_config": {**_default_acms_config(), "hot_max_tokens": val}} _seed_policy_blob(context, project, blob) @given('I seed ACMS config for "{project}" with temporal_scope "{scope}"') def step_seed_acms_temporal(context: Any, project: str, scope: str) -> None: from cleveragents.cli.commands.project_context import _default_acms_config blob = {"acms_config": {**_default_acms_config(), "temporal_scope": scope}} _seed_policy_blob(context, project, blob) @given('the tier service has fragments for "{project}" with strategy metadata') def step_seed_tier_fragments(context: Any, project: str) -> None: from cleveragents.application.services.context_tiers import ContextTierService from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment svc = ContextTierService() svc.store( TieredFragment( fragment_id="frag-alpha", content="Alpha content for testing tier retrieval", tier=ContextTier.HOT, resource_id="res:file-alpha", project_name=project, token_count=30, access_count=5, metadata={"strategy": "tier_a"}, ) ) svc.store( TieredFragment( fragment_id="frag-beta", content="Beta content with different strategy", tier=ContextTier.WARM, resource_id="res:file-beta", project_name=project, token_count=20, access_count=0, metadata={"strategy": "tier_b"}, ) ) context.cb_tier_service = svc context.cb_stored_fragment_count = 2 @given('the tier service has large fragments for "{project}" exceeding budget') def step_seed_large_fragments(context: Any, project: str) -> None: from cleveragents.application.services.context_tiers import ContextTierService from cleveragents.domain.models.acms.tiers import ContextTier, TieredFragment svc = ContextTierService() # First fragment: 60 tokens, long content, access_count > 0 svc.store( TieredFragment( fragment_id="big-1", content="A" * 200, # long content -> triggers truncation (line 339) tier=ContextTier.HOT, resource_id="res:big1", project_name=project, token_count=60, access_count=12, # > 0 triggers relevance calc (line 344) ) ) # Second fragment: 60 tokens -> total would be 120 > budget of 100 svc.store( TieredFragment( fragment_id="big-2", content="Short", # short content -> no truncation tier=ContextTier.HOT, resource_id="res:big2", project_name=project, token_count=60, access_count=0, ) ) context.cb_tier_service = svc context.cb_stored_fragment_count = 2 # ------------------------------------------------------------------ # When steps # ------------------------------------------------------------------ @when('I read the stored policy for "{project}"') def step_read_policy(context: Any, project: str) -> None: from cleveragents.cli.commands.project_context import ( _read_acms_config, _read_policy, ) context.cb_policy = _read_policy(context.cb_session_factory, project) context.cb_acms = _read_acms_config(context.cb_session_factory, project) @when('I write a policy for "{project}" without explicit ACMS config') def step_write_policy_no_acms(context: Any, project: str) -> None: from cleveragents.cli.commands.project_context import _write_policy from cleveragents.domain.models.core.context_policy import ProjectContextPolicy policy = ProjectContextPolicy() # acms_config=None triggers the preservation branch (lines 199-201) _write_policy(context.cb_session_factory, project, policy, acms_config=None) @when( 'I run coverage-boost context set on "{project}" with view "{view}" and include-resource "{res}"' ) def step_cb_set_include(context: Any, project: str, view: str, res: str) -> None: from cleveragents.cli.commands.project_context import context_set _run(context, context_set, project=project, view=view, include_resource=[res]) @when( 'I run coverage-boost context set on "{project}" with view "{view}" and clear flag' ) def step_cb_set_clear(context: Any, project: str, view: str) -> None: from cleveragents.cli.commands.project_context import context_set _run(context, context_set, project=project, view=view, clear=True) @when('I run coverage-boost context set on "{project}" with default_depth "{depth}"') def step_cb_set_depth_string(context: Any, project: str, depth: str) -> None: from cleveragents.cli.commands.project_context import context_set _run(context, context_set, project=project, view="default", default_depth=depth) @when( 'I run coverage-boost context set on "{project}" with execution_environment "{env}"' ) def step_cb_set_exec_env(context: Any, project: str, env: str) -> None: from cleveragents.cli.commands.project_context import context_set _run( context, context_set, project=project, view="default", execution_environment=env, ) @when( 'I run coverage-boost context set on "{project}" with view "{view}" include-resource "{res}" and format "{fmt}"' ) def step_cb_set_json_fmt( context: Any, project: str, view: str, res: str, fmt: str ) -> None: from cleveragents.cli.commands.project_context import context_set _run( context, context_set, project=project, view=view, include_resource=[res], output_format=fmt, ) @when('I run coverage-boost context set on "{project}" with temporal_scope "{scope}"') def step_cb_set_temporal(context: Any, project: str, scope: str) -> None: from cleveragents.cli.commands.project_context import context_set _run(context, context_set, project=project, view="default", temporal_scope=scope) @when('I run coverage-boost context set on "{project}" with all ACMS overrides') def step_cb_set_all_acms(context: Any, project: str) -> None: from cleveragents.cli.commands.project_context import context_set _run( context, context_set, project=project, view="default", hot_max_tokens=1234, warm_max_decisions=111, cold_max_decisions=222, query_limit=50, summarize=False, summary_max_tokens=999, strategy=["bfs", "dfs"], default_breadth=7, default_depth="4", skeleton_ratio=0.5, temporal_scope="recent", auto_refresh=False, ) @when('I run coverage-boost context show on "{project}" with view "{view}"') def step_cb_show_view(context: Any, project: str, view: str) -> None: from cleveragents.cli.commands.project_context import context_show _run(context, context_show, project=project, view=view) @when( 'I run coverage-boost context inspect on "{project}" with strategy filter "{strat}"' ) def step_cb_inspect_strategy(context: Any, project: str, strat: str) -> None: from cleveragents.cli.commands.project_context import context_inspect _run(context, context_inspect, project=project, strategy_filter=strat) @when('I run coverage-boost context inspect on "{project}" with focus "{uri}"') def step_cb_inspect_focus(context: Any, project: str, uri: str) -> None: from cleveragents.cli.commands.project_context import context_inspect _run(context, context_inspect, project=project, focus=[uri]) @when( 'I run coverage-boost context inspect on "{project}" with breadth {b:d} and depth "{d}"' ) def step_cb_inspect_breadth_depth(context: Any, project: str, b: int, d: str) -> None: from cleveragents.cli.commands.project_context import context_inspect _run(context, context_inspect, project=project, breadth=b, depth=d) @when('I run coverage-boost context inspect on "{project}" with view "{view}"') def step_cb_inspect_view(context: Any, project: str, view: str) -> None: from cleveragents.cli.commands.project_context import context_inspect _run(context, context_inspect, project=project, view=view) @when('I run coverage-boost context simulate on "{project}" with defaults') def step_cb_simulate(context: Any, project: str) -> None: from cleveragents.cli.commands.project_context import context_simulate _run(context, context_simulate, project=project) @when('I run coverage-boost context simulate on "{project}" with budget {b:d}') def step_cb_simulate_budget(context: Any, project: str, b: int) -> None: from cleveragents.cli.commands.project_context import context_simulate _run(context, context_simulate, project=project, budget=b) @when('I run coverage-boost context simulate on "{project}" in rich format') def step_cb_simulate_rich(context: Any, project: str) -> None: from cleveragents.cli.commands.project_context import context_simulate _run(context, context_simulate, project=project, output_format="rich") @when('I run coverage-boost context simulate on "{project}" with view "{view}"') def step_cb_simulate_view(context: Any, project: str, view: str) -> None: from cleveragents.cli.commands.project_context import context_simulate _run(context, context_simulate, project=project, view=view) @when('I run coverage-boost context simulate on "{project}" with focus "{uri}"') def step_cb_simulate_focus(context: Any, project: str, uri: str) -> None: from cleveragents.cli.commands.project_context import context_simulate _run(context, context_simulate, project=project, focus=[uri]) @when( 'I run coverage-boost context simulate on "{project}" with strategy hints "{hints}"' ) def step_cb_simulate_strategies(context: Any, project: str, hints: str) -> None: from cleveragents.cli.commands.project_context import context_simulate _run( context, context_simulate, project=project, strategy_hint=hints.split(","), ) # ------------------------------------------------------------------ # Then steps # ------------------------------------------------------------------ @then("the policy should be a fresh default policy") def step_assert_default_policy(context: Any) -> None: from cleveragents.domain.models.core.context_policy import ProjectContextPolicy default = ProjectContextPolicy() assert context.cb_policy.default_view == default.default_view assert context.cb_policy.strategize_view is None assert context.cb_policy.execute_view is None assert context.cb_policy.apply_view is None @then("the ACMS config should equal the defaults") def step_assert_default_acms(context: Any) -> None: from cleveragents.cli.commands.project_context import _default_acms_config expected = _default_acms_config() assert context.cb_acms == expected, f"{context.cb_acms} != {expected}" @then("the stored project-context ACMS config hot_max_tokens should be {val}") def step_assert_acms_hot(context: Any, val: str) -> None: blob = _load_raw_blob(context, "local/cov-app") assert blob is not None, "No policy blob stored" assert blob.get("acms_config", {}).get("hot_max_tokens") == int(val) @then("the coverage-boost command should succeed") def step_cb_ok(context: Any) -> None: assert context.cb_exit_code == 0, ( f"Expected exit 0, got {context.cb_exit_code}. Output: {context.cb_output}" ) @then("the coverage-boost command should have exit code {code:d}") def step_cb_exit(context: Any, code: int) -> None: assert context.cb_exit_code == code, ( f"Expected exit {code}, got {context.cb_exit_code}. Output: {context.cb_output}" ) @then("the stored coverage-boost default view include_resources should be empty") def step_cb_default_empty(context: Any) -> None: from cleveragents.cli.commands.project_context import _read_policy policy = _read_policy(context.cb_session_factory, "local/cov-app") assert policy.default_view.include_resources == [], ( f"Expected empty, got {policy.default_view.include_resources}" ) @then('the stored ACMS config default_depth should be "{val}"') def step_assert_acms_depth_str(context: Any, val: str) -> None: blob = _load_raw_blob(context, "local/cov-app") assert blob is not None assert blob.get("acms_config", {}).get("default_depth") == val @then('the stored execution_environment should be "{env}"') def step_assert_exec_env(context: Any, env: str) -> None: blob = _load_raw_blob(context, "local/cov-app") assert blob is not None, "No policy blob stored" assert blob.get("execution_environment") == env, ( f"Expected '{env}', got {blob.get('execution_environment')}" ) @then('the coverage-boost output should contain "{text}"') def step_cb_output_contains(context: Any, text: str) -> None: assert text in context.cb_output, f"'{text}' not in output: {context.cb_output!r}" @then("the coverage-boost output should be valid JSON") def step_cb_output_json(context: Any) -> None: try: _json.loads(context.cb_output.strip()) except _json.JSONDecodeError as exc: raise AssertionError( f"Output is not valid JSON: {exc}\nOutput: {context.cb_output!r}" ) from exc @then("the simulation should have fewer fragments than stored") def step_cb_fewer_fragments(context: Any) -> None: # The output is rich format; we already checked success. # Just verify that the budget constrained the output # (budget=100, first frag=60 tokens, second would overflow). # We additionally verify via the internal function. from cleveragents.cli.commands.project_context import ( _default_acms_config, _simulate_context_assembly, ) assembled = _simulate_context_assembly( project_name="local/cov-app", acms_config=_default_acms_config(), budget_tokens=100, ) assert len(assembled.fragments) < context.cb_stored_fragment_count, ( f"Expected fewer than {context.cb_stored_fragment_count}, " f"got {len(assembled.fragments)}" ) # ------------------------------------------------------------------ # flush() → commit() regression test: real separate sessions # ------------------------------------------------------------------ @when( 'I save a policy for "{project}" via a real session factory and read it back via a separate session' ) def step_save_and_read_real_sessions(context: Any, project: str) -> None: """Exercise _save_policy_json + _load_policy_json via real sessions. Uses a file-based SQLite database so that separate sessions truly see each other's committed data. The _SafeSession wrapper is NOT used here — if production code uses flush() instead of commit(), the second session will not see the written data. """ import os import tempfile from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker as real_sessionmaker from cleveragents.cli.commands.project_context import ( _load_policy_json, _save_policy_json, ) from cleveragents.infrastructure.database.models import Base # Create a file-based SQLite DB so separate sessions are independent. fd, db_path = tempfile.mkstemp(suffix=".db") os.close(fd) db_url = f"sqlite:///{db_path}" # M4: Use separate engines so that flush() vs commit() visibility # is truly tested — a shared engine/pool could serve the same # underlying connection, masking the difference. engine_seed = create_engine(db_url, echo=False) engine_write = create_engine(db_url, echo=False) engine_read = create_engine(db_url, echo=False) try: Base.metadata.create_all(engine_seed) factory_write = real_sessionmaker(bind=engine_write, expire_on_commit=False) factory_read = real_sessionmaker(bind=engine_read, expire_on_commit=False) # Seed the project row so UPDATE has a target. from datetime import UTC, datetime now = datetime.now(tz=UTC).isoformat() session = real_sessionmaker(bind=engine_seed, expire_on_commit=False)() try: session.execute( text( "INSERT INTO ns_projects (namespaced_name, namespace, tags_json, created_at, updated_at) " "VALUES (:ns, :nsp, :tags, :ca, :ua)" ), {"ns": project, "nsp": "local", "tags": "[]", "ca": now, "ua": now}, ) session.commit() finally: session.close() # Write policy via production code (uses commit) with write-only factory. policy_dict = { "regression_marker": "commit_test", "acms_config": {"hot_max_tokens": 42}, } _save_policy_json(factory_write, project, policy_dict) # Read back via a SEPARATE engine/factory to prove commit visibility. loaded = _load_policy_json(factory_read, project) context.cb_regression_loaded = loaded finally: engine_seed.dispose() engine_write.dispose() engine_read.dispose() # Clean up the DB file and any WAL/SHM journal files left by # SQLite WAL mode to avoid leaking temp files. for suffix in ("", "-wal", "-shm"): path = db_path + suffix if os.path.exists(path): os.unlink(path) @then("the policy read from the separate session should contain the saved data") def step_assert_regression_data(context: Any) -> None: loaded = context.cb_regression_loaded assert loaded is not None, "Policy was None — data did not persist across sessions" assert loaded.get("regression_marker") == "commit_test", ( f"Expected 'commit_test', got {loaded.get('regression_marker')}" ) assert loaded.get("acms_config", {}).get("hot_max_tokens") == 42, ( f"Expected hot_max_tokens=42, got {loaded.get('acms_config', {}).get('hot_max_tokens')}" ) @then("the stored ACMS config should reflect all overrides") def step_assert_all_acms(context: Any) -> None: blob = _load_raw_blob(context, "local/cov-app") assert blob is not None acms = blob.get("acms_config", {}) assert acms["hot_max_tokens"] == 1234 assert acms["warm_max_decisions"] == 111 assert acms["cold_max_decisions"] == 222 assert acms["query_limit"] == 50 assert acms["summarize"] is False assert acms["summary_max_tokens"] == 999 assert acms["strategies"] == ["bfs", "dfs"] assert acms["default_breadth"] == 7 assert acms["default_depth"] == 4 # "4" is parseable as int assert acms["skeleton_ratio"] == 0.5 assert acms["temporal_scope"] == "recent" assert acms["auto_refresh"] is False # ------------------------------------------------------------------ # Rollback path test for _save_policy_json (P3-14) # ------------------------------------------------------------------ @when("I attempt to save a policy with a commit that will fail") def step_save_policy_commit_fail(context: Any) -> None: """Force a commit failure and verify the rollback path. Uses the existing ``context.cb_session_factory`` (set up by the Background step which already seeded the ``local/cov-app`` project row) to ensure the UPDATE actually modifies data. Monkey-patches ``commit()`` on the underlying real session to simulate a commit failure. """ from cleveragents.cli.commands.project_context import _save_policy_json factory = context.cb_session_factory # Get the _SafeSession wrapper and the real session underneath. safe_session = factory() real: Session = object.__getattribute__(safe_session, "_real") original_commit = real.commit def failing_commit() -> None: raise RuntimeError("Simulated commit failure") real.commit = failing_commit # type: ignore[assignment] context.cb_rollback_exception = None try: _save_policy_json(factory, "local/cov-app", {"marker": "rollback_test"}) except RuntimeError as exc: context.cb_rollback_exception = exc finally: # Restore original commit so subsequent scenarios can use it. real.commit = original_commit # type: ignore[assignment] # Store the session for dirty-state check. context.cb_rollback_session = real @then("the original commit exception should be re-raised") def step_assert_commit_exception_reraised(context: Any) -> None: exc = context.cb_rollback_exception assert exc is not None, "_save_policy_json did not re-raise the commit exception" assert "Simulated commit failure" in str(exc), ( f"Expected 'Simulated commit failure', got: {exc}" ) @then("the session should not be in a dirty state after the failure") def step_assert_session_clean_after_rollback(context: Any) -> None: session: Session = context.cb_rollback_session # After rollback, the session should have no pending changes. assert not session.dirty, "Session has dirty objects after rollback" assert not session.new, "Session has new objects after rollback" # ------------------------------------------------------------------ # Nonexistent project test for _save_policy_json (P3-15) # ------------------------------------------------------------------ @when('I save a policy for a nonexistent project "{project}"') def step_save_policy_nonexistent(context: Any, project: str) -> None: """Call _save_policy_json for a project that has no ns_projects row.""" from cleveragents.cli.commands.project_context import _save_policy_json context.cb_nonexistent_exception = None try: _save_policy_json( context.cb_session_factory, project, {"marker": "nonexistent_test"}, ) except Exception as exc: context.cb_nonexistent_exception = exc @then("the save should complete without error") def step_assert_save_no_error(context: Any) -> None: exc = context.cb_nonexistent_exception # _save_policy_json does not currently check rowcount, so it # completes without error even when 0 rows are affected. assert exc is None, f"_save_policy_json raised an unexpected error: {exc}" @then('the policy should not be retrievable for "{project}"') def step_assert_policy_not_retrievable(context: Any, project: str) -> None: from cleveragents.cli.commands.project_context import _load_policy_json loaded = _load_policy_json(context.cb_session_factory, project) assert loaded is None, f"Expected None for nonexistent project, got: {loaded}"