"""Step definitions for tdd_invariant_persistence.feature (bug #1022). TDD issue-capture tests verifying that ``InvariantService`` persists invariants across simulated CLI process restarts (separate service instances). Bug #1022: ``InvariantService`` stores invariants in an in-memory dict (``self._invariants``) with no database persistence layer. Each CLI invocation spawns a fresh process with a new ``InvariantService()`` instance, so all invariants are lost when the process exits. These steps exercise the current (buggy) behaviour by creating fresh ``InvariantService`` instances to simulate separate process invocations. When the bug is fixed, the service will use a database repository and fresh instances backed by the same database will share state. The tests carry ``@tdd_expected_fail`` so CI passes while the bug is unfixed. The tag will be removed when bug #1022 is fixed. """ from __future__ import annotations from unittest.mock import patch from behave import given, then, when from behave.runner import Context from typer.testing import CliRunner from cleveragents.application.services.invariant_service import InvariantService from cleveragents.cli.commands.invariant import app as invariant_app from cleveragents.core.exceptions import NotFoundError from cleveragents.domain.models.core.invariant import InvariantScope runner = CliRunner() # --------------------------------------------------------------------------- # Given steps — instance A # --------------------------------------------------------------------------- @given( 'I add a project invariant "{text}" to project "{project}" ' "via invariant service instance A" ) def step_add_project_invariant_instance_a( context: Context, text: str, project: str ) -> None: """Add a project-scoped invariant via a fresh InvariantService (instance A).""" context.invariant_svc_a = InvariantService() context.invariant_added_a = context.invariant_svc_a.add_invariant( text=text, scope=InvariantScope.PROJECT, source_name=project, ) @given('I add a global invariant "{text}" via invariant service instance A') def step_add_global_invariant_instance_a(context: Context, text: str) -> None: """Add a global invariant via a fresh InvariantService (instance A).""" context.invariant_svc_a = InvariantService() context.invariant_added_a = context.invariant_svc_a.add_invariant( text=text, scope=InvariantScope.GLOBAL, source_name="system", ) @given("I capture the invariant ID from instance A") def step_capture_invariant_id(context: Context) -> None: """Store the invariant ID from instance A for later use.""" context.captured_invariant_id = context.invariant_added_a.id @given( 'I invoke invariant add via CLI with "{flags}" and text "{text}" ' "using service invocation {n:d}" ) def step_invoke_add_cli(context: Context, flags: str, text: str, n: int) -> None: """Invoke ``invariant add`` via CLI with a fresh service (simulated invocation).""" svc = InvariantService() args = ["add", *flags.split(), text] with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc): result = runner.invoke(invariant_app, args) setattr(context, f"invariant_cli_result_{n}", result) setattr(context, f"invariant_svc_{n}", svc) assert result.exit_code == 0, ( f"invariant add should exit 0 but got {result.exit_code}. " f"output:\n{result.output}" ) # --------------------------------------------------------------------------- # When steps — instance B / fresh instance # --------------------------------------------------------------------------- @when("I create a fresh invariant service instance B") def step_create_fresh_instance_b(context: Context) -> None: """Create a completely new InvariantService, simulating a new CLI process.""" context.invariant_svc_b = InvariantService() @when('I list project invariants for "{project}" via instance B') def step_list_project_invariants_instance_b(context: Context, project: str) -> None: """List project-scoped invariants via the fresh instance B.""" context.invariant_list_b = context.invariant_svc_b.list_invariants( scope=InvariantScope.PROJECT, source_name=project, ) @when("I list global invariants via instance B") def step_list_global_invariants_instance_b(context: Context) -> None: """List global invariants via the fresh instance B.""" context.invariant_list_b = context.invariant_svc_b.list_invariants( scope=InvariantScope.GLOBAL, ) @when('I invoke invariant list via CLI with "{flags}" using service invocation {n:d}') def step_invoke_list_cli(context: Context, flags: str, n: int) -> None: """Invoke ``invariant list`` via CLI with a fresh service (simulated invocation).""" svc = InvariantService() args = ["list", *flags.split()] with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc): result = runner.invoke(invariant_app, args) setattr(context, f"invariant_cli_result_{n}", result) setattr(context, f"invariant_svc_{n}", svc) assert result.exit_code == 0, ( f"invariant list should exit 0 but got {result.exit_code}. " f"output:\n{result.output}" ) @when("I attempt to remove the captured invariant ID via instance B") def step_remove_via_instance_b(context: Context) -> None: """Attempt to soft-delete an invariant using the fresh instance B.""" try: context.invariant_svc_b.remove_invariant(context.captured_invariant_id) context.invariant_remove_b_error = None except NotFoundError as exc: context.invariant_remove_b_error = exc # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then('the invariant list from instance B should contain "{text}"') def step_assert_list_b_contains(context: Context, text: str) -> None: """Assert the invariant list from instance B contains the given text.""" inv_texts = [inv.text for inv in context.invariant_list_b] assert text in inv_texts, ( f"Expected invariant '{text}' in instance B list but got: {inv_texts}" ) @then('the CLI list output from invocation {n:d} should contain "{text}"') def step_assert_cli_list_contains(context: Context, n: int, text: str) -> None: """Assert the CLI list output from invocation N contains the given text.""" result = getattr(context, f"invariant_cli_result_{n}") assert text in result.output, ( f"Expected '{text}' in CLI invocation {n} output but got:\n{result.output}" ) @then("the remove operation via instance B should succeed without NotFoundError") def step_assert_remove_b_success(context: Context) -> None: """Assert that the remove operation succeeded (no NotFoundError).""" assert context.invariant_remove_b_error is None, ( f"Expected remove to succeed but got NotFoundError: " f"{context.invariant_remove_b_error}" )