c1dca18654
The helper used the CLI default Rich table renderer and substring-checked for the invariant text in the captured output. With CliRunner's narrow default console width the Rich table soft-wraps "Must validate inputs" across two visual rows, so the substring check always failed even though the invariant was correctly persisted and retrieved from the database. Pass --format json on the list invocations: the JSON serialiser emits the text verbatim so the substring assertion now reflects persistence rather than column-width formatting. Verified locally by running all three helper subcommands against a fresh SQLite DB — all three now print their ok markers. ISSUES CLOSED: #8573
142 lines
5.0 KiB
Python
142 lines
5.0 KiB
Python
"""Helper script for tdd_invariant_persistence.robot (bug #1022, now fixed).
|
|
|
|
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 #8573 / #1022 is now FIXED: InvariantService uses SQLite-based persistence
|
|
via the configured ``database_url``, so invariants added in one CLI invocation
|
|
persist across process restarts.
|
|
|
|
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).
|
|
# Use --format json so the invariant text is emitted verbatim instead of
|
|
# being soft-wrapped by the default Rich table renderer (which breaks
|
|
# substring matching when the text spans multiple visual rows).
|
|
svc2 = InvariantService()
|
|
with patch("cleveragents.cli.commands.invariant._get_service", return_value=svc2):
|
|
list_result = runner.invoke(
|
|
invariant_app,
|
|
["list", "--project", "local/test-proj", "--format", "json"],
|
|
)
|
|
|
|
# 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", "--format", "json"]
|
|
)
|
|
|
|
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]()
|