"""Robot Framework helper for v3_plans schema alignment tests (issue #921). Verifies effective_profile_snapshot, root_plan_id self-reference, and automation_profile NOT NULL default. Usage: python robot/helper_plans_table_schema_alignment.py snapshot-roundtrip python robot/helper_plans_table_schema_alignment.py root-self-ref python robot/helper_plans_table_schema_alignment.py child-root-id """ from __future__ import annotations import sys from datetime import UTC, datetime from pathlib import Path from typing import Any _SRC = str(Path(__file__).resolve().parents[1] / "src") if _SRC not in sys.path: sys.path.insert(0, _SRC) from sqlalchemy import create_engine # noqa: E402 from sqlalchemy.orm import sessionmaker # noqa: E402 from cleveragents.domain.models.core.action import ( # noqa: E402 Action, ActionState, ) from cleveragents.domain.models.core.plan import ( # noqa: E402 AutomationProfileProvenance, AutomationProfileRef, NamespacedName, Plan, PlanIdentity, PlanPhase, PlanTimestamps, ProcessingState, ) from cleveragents.infrastructure.database.models import Base # noqa: E402 from cleveragents.infrastructure.database.repositories import ( # noqa: E402 ActionRepository, LifecyclePlanRepository, ) _NOW = datetime(2026, 3, 19, tzinfo=UTC) _ACTION_NAME = "local/schema-test" def _setup() -> tuple[Any, Any, Any]: """Create in-memory DB and return (session, plan_repo, action_repo).""" engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) sm = sessionmaker(bind=engine) session = sm() factory = lambda: session # noqa: E731 plan_repo = LifecyclePlanRepository(session_factory=factory) action_repo = ActionRepository(session_factory=factory) # Create prerequisite action for FK constraint ns = NamespacedName.parse(_ACTION_NAME) action = Action( namespaced_name=ns, description="Schema alignment test action", definition_of_done="Done", strategy_actor="local/s", execution_actor="local/e", state=ActionState.AVAILABLE, created_at=_NOW, updated_at=_NOW, ) action_repo.create(action) session.commit() return session, plan_repo, action_repo def _make_plan( plan_id: str, *, parent_plan_id: str | None = None, root_plan_id: str | None = None, effective_profile_snapshot: str = "{}", automation_profile: AutomationProfileRef | None = None, ) -> Plan: return Plan( identity=PlanIdentity( plan_id=plan_id, parent_plan_id=parent_plan_id, root_plan_id=root_plan_id, attempt=1, ), namespaced_name=NamespacedName(namespace="local", name="schema-test"), action_name=_ACTION_NAME, description="Schema alignment test plan", definition_of_done="All assertions pass", phase=PlanPhase.ACTION, processing_state=ProcessingState.QUEUED, automation_profile=automation_profile, effective_profile_snapshot=effective_profile_snapshot, strategy_actor="local/s", execution_actor="local/e", timestamps=PlanTimestamps(created_at=_NOW, updated_at=_NOW), created_by="test-schema", tags=[], reusable=True, read_only=False, ) def _cmd_snapshot_roundtrip() -> int: """Verify effective_profile_snapshot roundtrips through persistence.""" session, plan_repo, _ = _setup() snapshot = '{"profile":"balanced","thresholds":{}}' plan = _make_plan( "01HV000000000000000000SR01", effective_profile_snapshot=snapshot, ) plan_repo.create(plan) session.commit() retrieved = plan_repo.get("01HV000000000000000000SR01") if retrieved is None: print("FAIL: plan not retrieved") return 1 if retrieved.effective_profile_snapshot != snapshot: print( f"FAIL: snapshot mismatch: {retrieved.effective_profile_snapshot!r} " f"!= {snapshot!r}" ) return 1 print("schema-snapshot-ok") return 0 def _cmd_root_self_ref() -> int: """Verify root plan self-references its own plan_id.""" session, plan_repo, _ = _setup() plan_id = "01HV000000000000000000SR02" plan = _make_plan(plan_id, root_plan_id=None) plan_repo.create(plan) session.commit() retrieved = plan_repo.get(plan_id) if retrieved is None: print("FAIL: plan not retrieved") return 1 if retrieved.identity.root_plan_id != plan_id: print( f"FAIL: root_plan_id={retrieved.identity.root_plan_id!r}, " f"expected={plan_id!r}" ) return 1 print("schema-root-self-ref-ok") return 0 def _cmd_child_root_id() -> int: """Verify child plan carries parent's root_plan_id.""" session, plan_repo, _ = _setup() root_id = "01HV000000000000000000SR03" child_id = "01HV000000000000000000SR04" root_plan = _make_plan(root_id, root_plan_id=None) plan_repo.create(root_plan) session.commit() child_plan = _make_plan(child_id, parent_plan_id=root_id, root_plan_id=root_id) plan_repo.create(child_plan) session.commit() retrieved = plan_repo.get(child_id) if retrieved is None: print("FAIL: child plan not retrieved") return 1 if retrieved.identity.root_plan_id != root_id: print( f"FAIL: root_plan_id={retrieved.identity.root_plan_id!r}, " f"expected={root_id!r}" ) return 1 if retrieved.identity.parent_plan_id != root_id: print( f"FAIL: parent_plan_id={retrieved.identity.parent_plan_id!r}, " f"expected={root_id!r}" ) return 1 print("schema-child-root-ok") return 0 def _cmd_grandchild_root_id() -> int: """Verify grandchild plan carries root ancestor's plan_id (3-level).""" session, plan_repo, _ = _setup() root_id = "01HV000000000000000000SR05" child_id = "01HV000000000000000000SR06" gc_id = "01HV000000000000000000SR07" root_plan = _make_plan(root_id, root_plan_id=None) plan_repo.create(root_plan) session.commit() child_plan = _make_plan(child_id, parent_plan_id=root_id, root_plan_id=root_id) plan_repo.create(child_plan) session.commit() gc_plan = _make_plan(gc_id, parent_plan_id=child_id, root_plan_id=root_id) plan_repo.create(gc_plan) session.commit() retrieved = plan_repo.get(gc_id) if retrieved is None: print("FAIL: grandchild plan not retrieved") return 1 if retrieved.identity.root_plan_id != root_id: print( f"FAIL: root_plan_id={retrieved.identity.root_plan_id!r}, " f"expected={root_id!r}" ) return 1 if retrieved.identity.parent_plan_id != child_id: print( f"FAIL: parent_plan_id={retrieved.identity.parent_plan_id!r}, " f"expected={child_id!r}" ) return 1 print("schema-grandchild-root-ok") return 0 def _cmd_automation_profile_default() -> int: """Verify automation_profile=None roundtrips as None via 'balanced' default.""" session, plan_repo, _ = _setup() plan_id = "01HV000000000000000000SR08" plan = _make_plan(plan_id) # automation_profile defaults to None plan_repo.create(plan) session.commit() retrieved = plan_repo.get(plan_id) if retrieved is None: print("FAIL: plan not retrieved") return 1 if retrieved.automation_profile is not None: print( f"FAIL: expected automation_profile=None, " f"got {retrieved.automation_profile!r}" ) return 1 # Verify raw column value is 'balanced' from sqlalchemy import text row = session.execute( text("SELECT automation_profile FROM v3_plans WHERE plan_id = :pid"), {"pid": plan_id}, ).fetchone() if row is None or row[0] != "balanced": print(f"FAIL: raw column value={row[0] if row else 'N/A'}, expected='balanced'") return 1 print("schema-automation-profile-default-ok") return 0 def _cmd_null_root_rejected() -> int: """Verify NULL root_plan_id is rejected by the NOT NULL constraint.""" from sqlalchemy import text from sqlalchemy.exc import IntegrityError as SAIntegrityError session, _, _ = _setup() now_iso = _NOW.isoformat() try: session.execute( text( "INSERT INTO v3_plans " "(plan_id, root_plan_id, action_name, namespaced_name, " "namespace, phase, processing_state, attempt, description, " "tags_json, effective_profile_snapshot, automation_profile, " "reusable, read_only, created_at, updated_at) " "VALUES (:pid, NULL, :act, :ns, 'local', 'action', 'queued', " "1, 'test', '[]', '{}', 'balanced', 1, 0, :now, :now)" ), { "pid": "01HV000000000000000000SR09", "act": "local/schema-test", "ns": "local/schema-test", "now": now_iso, }, ) session.commit() print( "FAIL: expected IntegrityError for NULL root_plan_id, but insert succeeded" ) return 1 except SAIntegrityError: session.rollback() print("schema-null-root-rejected-ok") return 0 def _cmd_automation_profile_ref_roundtrip() -> int: """Verify AutomationProfileRef roundtrips through persistence.""" session, plan_repo, _ = _setup() plan_id = "01HV000000000000000000SR10" ref = AutomationProfileRef( profile_name="supervised", provenance=AutomationProfileProvenance.PLAN, ) plan = _make_plan(plan_id, automation_profile=ref) plan_repo.create(plan) session.commit() retrieved = plan_repo.get(plan_id) if retrieved is None: print("FAIL: plan not retrieved") return 1 if retrieved.automation_profile is None: print("FAIL: automation_profile is None after round-trip") return 1 if retrieved.automation_profile.profile_name != "supervised": print( f"FAIL: profile_name={retrieved.automation_profile.profile_name!r}, " f"expected='supervised'" ) return 1 if retrieved.automation_profile.provenance != AutomationProfileProvenance.PLAN: print( f"FAIL: provenance={retrieved.automation_profile.provenance!r}, " f"expected='plan'" ) return 1 print("schema-automation-profile-ref-roundtrip-ok") return 0 def _cmd_corrupted_snapshot_fallback() -> int: """Verify corrupted effective_profile_snapshot falls back to '{}'.""" from sqlalchemy import text as sa_text session, plan_repo, _ = _setup() plan_id = "01HV000000000000000000SR11" plan = _make_plan( plan_id, effective_profile_snapshot='{"profile":"balanced"}', ) plan_repo.create(plan) session.commit() # Corrupt the column directly session.execute( sa_text( "UPDATE v3_plans SET effective_profile_snapshot = :val WHERE plan_id = :pid" ), {"val": "not-valid-json{", "pid": plan_id}, ) session.commit() session.expire_all() retrieved = plan_repo.get(plan_id) if retrieved is None: print("FAIL: plan not retrieved after corruption") return 1 if retrieved.effective_profile_snapshot != "{}": print( f"FAIL: expected fallback '{{}}', " f"got {retrieved.effective_profile_snapshot!r}" ) return 1 print("schema-corrupted-snapshot-fallback-ok") return 0 _COMMANDS: dict[str, Any] = { "snapshot-roundtrip": _cmd_snapshot_roundtrip, "root-self-ref": _cmd_root_self_ref, "child-root-id": _cmd_child_root_id, "grandchild-root-id": _cmd_grandchild_root_id, "automation-profile-default": _cmd_automation_profile_default, "automation-profile-ref-roundtrip": _cmd_automation_profile_ref_roundtrip, "null-root-rejected": _cmd_null_root_rejected, "corrupted-snapshot-fallback": _cmd_corrupted_snapshot_fallback, } if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: print(f"Usage: {sys.argv[0]} {{{','.join(_COMMANDS)}}}", file=sys.stderr) sys.exit(2) sys.exit(_COMMANDS[sys.argv[1]]())