Files
cleveragents-core/robot/helper_tdd_invariant_persistence.py
T
brent.edwards 0e407f7f19
CI / build (push) Successful in 27s
CI / lint (push) Successful in 3m20s
CI / quality (push) Successful in 3m54s
CI / typecheck (push) Successful in 4m6s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 6m55s
CI / unit_tests (push) Successful in 7m6s
CI / security (push) Successful in 8m4s
CI / e2e_tests (push) Successful in 10m47s
CI / docker (push) Successful in 1m9s
CI / coverage (push) Successful in 14m37s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Failing after 24m9s
test: add TDD bug-capture test for #1022 — InvariantService persistence (#1109)
## Summary

Add Behave BDD and Robot Framework integration tests that capture bug #1022 — `InvariantService` stores invariants in an in-memory dict only, losing them across CLI process invocations.

### Motivation

Per the project's TDD Bug Fix Workflow (CONTRIBUTING.md), every bug must first have a test written that captures the buggy behavior before the fix is implemented. This PR fulfills the TDD counterpart issue #1032 for bug #1022.

### What was done

**Behave tests** (`features/tdd_invariant_persistence.feature`): Four scenarios tagged `@tdd_expected_fail @tdd_bug @tdd_bug_1022 @mock_only` that simulate separate CLI invocations via fresh `InvariantService` instances and assert cross-instance data visibility:

1. Project invariant persistence across service instances
2. Global invariant persistence across service instances
3. CLI add/list across separate invocations
4. Cross-instance soft-delete by invariant ID

Step definitions in `features/steps/tdd_invariant_persistence_steps.py`.

**Robot integration tests** (`robot/tdd_invariant_persistence.robot`): Three integration test cases with a helper script (`robot/helper_tdd_invariant_persistence.py`) that exercises add-then-list and add-then-remove across fresh service instances.

### How it passes CI

All tests carry `@tdd_expected_fail`, which inverts the test result via the environment hooks (Behave) and listener (Robot). The underlying assertions fail (proving the bug exists), but are reported as passed to CI. The `@tdd_expected_fail` tag will be removed when bug #1022 is fixed, at which point the tests will run normally and must pass.

### Review fixes applied

- **M1**: Moved `InvariantScope` import from function body in `robot/helper_tdd_invariant_persistence.py` to module-level top imports.
- **m1**: Added `@mock_only` tag to `features/tdd_invariant_persistence.feature` — these tests use purely in-memory services and never touch the database.
- **m2**: Added `exit_code == 0` assertion to `step_invoke_list_cli` in `features/steps/tdd_invariant_persistence_steps.py`, matching the pattern used in `step_invoke_add_cli`.
- **m3**: Changed all `context: Any` parameter types to `context: Context` (from `behave.runner`), following the project-wide convention used in 335+ step files.
- **m4**: Narrowed `except Exception` to `except NotFoundError` in `robot/helper_tdd_invariant_persistence.py`, matching the specific error being tested.
- **n3**: Branch rebased onto latest `master`.

### Quality Gates

- **lint**:  passed
- **typecheck**:  passed (Pyright, 0 errors)
- **unit_tests**:  1 feature, 4 scenarios, 16 steps — all passed via `@tdd_expected_fail`
- **coverage**: ≥ 97% (test-only changes, no source modifications)

Closes #1032

Co-authored-by: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Reviewed-on: #1109
Reviewed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
2026-03-26 02:06:28 +00:00

135 lines
4.7 KiB
Python

"""Helper script for tdd_invariant_persistence.robot (bug #1022).
Exercises InvariantService cross-invocation persistence at the integration
level. Each subcommand simulates a fresh CLI process by creating a new
InvariantService instance, mirroring how the real CLI works (each
``python -m cleveragents`` call gets its own service).
Bug #1022: InvariantService stores invariants in an in-memory dict only.
Invariants added in one CLI invocation are lost when the process exits.
This helper is called from Robot Framework via ``Run Process``.
"""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import patch
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from typer.testing import CliRunner # noqa: E402
from cleveragents.application.services.invariant_service import ( # noqa: E402
InvariantService,
)
from cleveragents.cli.commands.invariant import app as invariant_app # noqa: E402
from cleveragents.core.exceptions import NotFoundError # noqa: E402
from cleveragents.domain.models.core.invariant import InvariantScope # noqa: E402
runner = CliRunner()
def add_then_list_project() -> None:
"""Add a project invariant in invocation 1, list in invocation 2.
Simulates two separate CLI invocations by using fresh InvariantService
instances. The list in invocation 2 should show the invariant added
in invocation 1 — but it won't because of bug #1022.
"""
# Invocation 1: add
svc1 = InvariantService()
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc1):
add_result = runner.invoke(
invariant_app,
["add", "--project", "local/test-proj", "Must validate inputs"],
)
if add_result.exit_code != 0:
print(f"FAIL-ADD: exit={add_result.exit_code} out={add_result.output}")
sys.exit(1)
# Invocation 2: list (fresh service — simulates new process)
svc2 = InvariantService()
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc2):
list_result = runner.invoke(
invariant_app, ["list", "--project", "local/test-proj"]
)
# The list output should contain the invariant — if it doesn't, bug exists
if "Must validate inputs" in list_result.output:
print("invariant-persist-project-ok")
else:
print(
f"FAIL-PERSIST: invariant not found in second invocation. "
f"output:\n{list_result.output}"
)
sys.exit(1)
def add_then_list_global() -> None:
"""Add a global invariant in invocation 1, list in invocation 2."""
svc1 = InvariantService()
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc1):
add_result = runner.invoke(
invariant_app,
["add", "--global", "Never expose credentials"],
)
if add_result.exit_code != 0:
print(f"FAIL-ADD: exit={add_result.exit_code} out={add_result.output}")
sys.exit(1)
svc2 = InvariantService()
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc2):
list_result = runner.invoke(invariant_app, ["list", "--global"])
if "Never expose credentials" in list_result.output:
print("invariant-persist-global-ok")
else:
print(
f"FAIL-PERSIST: invariant not found in second invocation. "
f"output:\n{list_result.output}"
)
sys.exit(1)
def add_then_remove_cross_instance() -> None:
"""Add invariant in instance 1, remove by ID in instance 2."""
svc1 = InvariantService()
inv = svc1.add_invariant(
text="Temporary constraint",
scope=InvariantScope.PROJECT,
source_name="local/temp",
)
inv_id = inv.id
# Fresh instance — simulates new CLI process
svc2 = InvariantService()
try:
svc2.remove_invariant(inv_id)
print("invariant-cross-remove-ok")
except NotFoundError as exc:
print(f"FAIL-REMOVE: {type(exc).__name__}: {exc}")
sys.exit(1)
# ---------------------------------------------------------------------------
# Dispatcher
# ---------------------------------------------------------------------------
COMMANDS = {
"add-then-list-project": add_then_list_project,
"add-then-list-global": add_then_list_global,
"add-then-remove-cross": add_then_remove_cross_instance,
}
if __name__ == "__main__":
cmd = sys.argv[1] if len(sys.argv) > 1 else None
if cmd not in COMMANDS:
print(f"Unknown command: {cmd}", file=sys.stderr)
print(f"Available: {', '.join(sorted(COMMANDS))}", file=sys.stderr)
sys.exit(2)
COMMANDS[cmd]()