bdc2ccd6a3
This PR implements the Invariant data model and database schema for the v3.2.0 milestone. The Invariant feature enables the system to define, store, and manage invariant rules that can be evaluated against system state. - Alembic migration m3_001_invariants_table creates the invariants table with columns: id (UUID), description (text), created_at (timestamp), is_active (bool, default True), with index on is_active for efficiency - SQLAlchemy ORM InvariantModel in cleveragents.infrastructure.database.models.InvariantModel - M3 merge migration to resolve Alembic head conflict - BDD Behave scenarios (10 test cases) in features/invariant_model.feature - Robot Framework integration tests in robot/invariant_model.robot - Updated CHANGELOG.md and CONTRIBUTORS.md - Restored status-check CI aggregation job ISSUES CLOSED: #8524
189 lines
6.1 KiB
Python
189 lines
6.1 KiB
Python
"""Helper script for Robot Framework invariant model smoke tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
# Ensure src is importable when run from workspace root
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from sqlalchemy import create_engine, inspect
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from cleveragents.infrastructure.database.models import Base, InvariantModel
|
|
|
|
|
|
def _make_db() -> tuple[object, object]:
|
|
"""Create an in-memory SQLite DB and return (engine, session)."""
|
|
engine = create_engine("sqlite:///:memory:", echo=False)
|
|
Base.metadata.create_all(engine)
|
|
sm = sessionmaker(bind=engine)
|
|
session = sm()
|
|
return engine, session
|
|
|
|
|
|
def _make_invariant(description: str, is_active: bool = True) -> InvariantModel:
|
|
return InvariantModel(
|
|
id=str(uuid.uuid4()),
|
|
description=description,
|
|
created_at=datetime.now(tz=UTC).isoformat(),
|
|
is_active=is_active,
|
|
)
|
|
|
|
|
|
def _test_create_invariant() -> None:
|
|
"""Create an Invariant with all required fields and verify persistence."""
|
|
_, session = _make_db()
|
|
inv = _make_invariant("All plans must have a goal")
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
retrieved = session.query(InvariantModel).filter_by(id=inv.id).first()
|
|
assert retrieved is not None, "Invariant not found after persist"
|
|
assert retrieved.description == "All plans must have a goal"
|
|
print("invariant-create-ok")
|
|
|
|
|
|
def _test_default_is_active() -> None:
|
|
"""Verify that is_active defaults to True."""
|
|
_, session = _make_db()
|
|
inv = _make_invariant("Default active invariant")
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
retrieved = session.query(InvariantModel).filter_by(id=inv.id).first()
|
|
assert retrieved is not None
|
|
assert retrieved.is_active is True or retrieved.is_active == 1, (
|
|
f"Expected is_active=True, got {retrieved.is_active!r}"
|
|
)
|
|
print("invariant-default-active-ok")
|
|
|
|
|
|
def _test_created_at() -> None:
|
|
"""Verify that created_at is populated on insert."""
|
|
_, session = _make_db()
|
|
inv = _make_invariant("Timestamped invariant")
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
retrieved = session.query(InvariantModel).filter_by(id=inv.id).first()
|
|
assert retrieved is not None
|
|
assert retrieved.created_at, "created_at should not be empty"
|
|
assert len(str(retrieved.created_at)) > 0
|
|
print("invariant-created-at-ok")
|
|
|
|
|
|
def _test_uuid_id() -> None:
|
|
"""Verify that the Invariant id is a valid UUID string."""
|
|
_, session = _make_db()
|
|
inv = _make_invariant("UUID invariant")
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
retrieved = session.query(InvariantModel).filter_by(id=inv.id).first()
|
|
assert retrieved is not None
|
|
try:
|
|
uuid.UUID(str(retrieved.id))
|
|
except ValueError as exc:
|
|
raise AssertionError(f"id '{retrieved.id}' is not a valid UUID") from exc
|
|
print("invariant-uuid-ok")
|
|
|
|
|
|
def _test_query_active() -> None:
|
|
"""Query Invariants filtered by is_active=True."""
|
|
_, session = _make_db()
|
|
for i in range(3):
|
|
session.add(_make_invariant(f"Active {i}", is_active=True))
|
|
for i in range(2):
|
|
session.add(_make_invariant(f"Inactive {i}", is_active=False))
|
|
session.commit()
|
|
|
|
results = session.query(InvariantModel).filter_by(is_active=True).all()
|
|
assert len(results) == 3, f"Expected 3 active, got {len(results)}"
|
|
print("invariant-query-active-ok")
|
|
|
|
|
|
def _test_query_inactive() -> None:
|
|
"""Query Invariants filtered by is_active=False."""
|
|
_, session = _make_db()
|
|
for i in range(3):
|
|
session.add(_make_invariant(f"Active {i}", is_active=True))
|
|
for i in range(2):
|
|
session.add(_make_invariant(f"Inactive {i}", is_active=False))
|
|
session.commit()
|
|
|
|
results = session.query(InvariantModel).filter_by(is_active=False).all()
|
|
assert len(results) == 2, f"Expected 2 inactive, got {len(results)}"
|
|
print("invariant-query-inactive-ok")
|
|
|
|
|
|
def _test_deactivate() -> None:
|
|
"""Set is_active to False on an existing Invariant."""
|
|
_, session = _make_db()
|
|
inv = _make_invariant("Active invariant to deactivate", is_active=True)
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
retrieved = session.query(InvariantModel).filter_by(id=inv.id).first()
|
|
assert retrieved is not None
|
|
retrieved.is_active = False
|
|
session.commit()
|
|
|
|
updated = session.query(InvariantModel).filter_by(id=inv.id).first()
|
|
assert updated is not None
|
|
assert updated.is_active is False or updated.is_active == 0, (
|
|
f"Expected is_active=False, got {updated.is_active!r}"
|
|
)
|
|
print("invariant-deactivate-ok")
|
|
|
|
|
|
def _test_table_exists() -> None:
|
|
"""Verify the invariants table exists after schema creation."""
|
|
engine, _ = _make_db()
|
|
inspector = inspect(engine)
|
|
tables = inspector.get_table_names()
|
|
assert "invariants" in tables, f"Table 'invariants' not found. Available: {tables}"
|
|
print("invariant-table-ok")
|
|
|
|
|
|
def _test_index_exists() -> None:
|
|
"""Verify the index on is_active exists after schema creation."""
|
|
engine, _ = _make_db()
|
|
inspector = inspect(engine)
|
|
indexes = inspector.get_indexes("invariants")
|
|
index_names = [idx["name"] for idx in indexes]
|
|
assert any("is_active" in name for name in index_names), (
|
|
f"No index on is_active found. Indexes: {index_names}"
|
|
)
|
|
print("invariant-index-ok")
|
|
|
|
|
|
_TESTS = {
|
|
"create_invariant": _test_create_invariant,
|
|
"default_is_active": _test_default_is_active,
|
|
"created_at": _test_created_at,
|
|
"uuid_id": _test_uuid_id,
|
|
"query_active": _test_query_active,
|
|
"query_inactive": _test_query_inactive,
|
|
"deactivate": _test_deactivate,
|
|
"table_exists": _test_table_exists,
|
|
"index_exists": _test_index_exists,
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 2:
|
|
print(f"Usage: {sys.argv[0]} <test_name>")
|
|
print(f"Available tests: {', '.join(sorted(_TESTS))}")
|
|
sys.exit(1)
|
|
|
|
test_name = sys.argv[1]
|
|
if test_name not in _TESTS:
|
|
print(f"Unknown test: {test_name}")
|
|
print(f"Available: {', '.join(sorted(_TESTS))}")
|
|
sys.exit(1)
|
|
|
|
_TESTS[test_name]()
|