forked from HAL9000/cleveragents-core
1878998b7a
Rename the TDD tag system from tdd_bug/tdd_bug_<N> to tdd_issue/tdd_issue_<N> across the entire codebase. The tdd_expected_fail tag is unchanged. The TDD expected-failure workflow is not limited to bug fixes — it applies equally to any issue type (features, tasks, refactors). The _bug suffix was misleading and narrowed the perceived scope. The new _issue suffix accurately reflects that the TDD tagging system applies to any Forgejo issue. Changes span 92 files: - features/environment.py: validate_tdd_tags(), should_invert_result(), and apply_tdd_inversion() updated — regex, variables, error messages - robot/tdd_expected_fail_listener.py: _validate_tdd_tags(), _should_invert_result(), start_test(), end_test() updated consistently - 33 Behave .feature files: all @tdd_bug/@tdd_bug_<N> tags renamed - 29 Robot .robot files: all tdd_bug/tdd_bug_<N> tags renamed - 3 Robot fixture files renamed (tdd_bug_alone, tdd_missing_tdd_bug, tdd_expected_fail_missing_bug_n) with content and references updated - Tag validation tests and helpers updated (function names, command dispatch keys, output strings, fixture references) - CONTRIBUTING.md: section renamed from 'TDD Bug Test Tags' to 'TDD Issue Test Tags', all tag references and examples updated - noxfile.py: comment references updated - Step definition files, mock helpers, and benchmark files: docstring references updated ISSUES CLOSED: #965
175 lines
7.0 KiB
Python
175 lines
7.0 KiB
Python
"""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}"
|
|
)
|