Files
cleveragents-core/robot/helper_schema_parity_migration.py
CoreRasurae f8233000cc
CI / push-validation (pull_request) Successful in 28s
CI / helm (pull_request) Successful in 36s
CI / build (pull_request) Successful in 4m1s
CI / lint (pull_request) Successful in 4m14s
CI / quality (pull_request) Successful in 4m33s
CI / typecheck (pull_request) Successful in 4m54s
CI / security (pull_request) Successful in 4m55s
CI / unit_tests (pull_request) Successful in 7m48s
CI / e2e_tests (pull_request) Successful in 7m51s
CI / integration_tests (pull_request) Successful in 7m59s
CI / docker (pull_request) Successful in 2m7s
CI / coverage (pull_request) Successful in 15m56s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Waiting to run
CI / benchmark-regression (push) Waiting to run
CI / push-validation (push) Successful in 22s
CI / helm (push) Successful in 27s
CI / build (push) Successful in 3m47s
CI / lint (push) Successful in 3m56s
CI / quality (push) Successful in 4m21s
CI / typecheck (push) Successful in 4m34s
CI / security (push) Successful in 4m44s
CI / e2e_tests (push) Successful in 6m43s
CI / unit_tests (push) Successful in 7m19s
CI / integration_tests (push) Successful in 7m44s
CI / docker (push) Successful in 1m29s
CI / coverage (push) Successful in 15m57s
CI / status-check (push) Successful in 4s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1h9m42s
fix(db): add missing link_type, FK constraints, and partial index per spec DDL
Align resource_links, checkpoint_metadata, and decisions schema behavior with the spec DDL. Add SQLite runtime trigger guards for checkpoint foreign-key semantics and migration-focused Behave/Robot checks that orphan checkpoint references are rejected.

ISSUES CLOSED: #922
2026-04-21 16:53:08 +00:00

444 lines
16 KiB
Python

"""Robot helper for schema-parity migration verification.
Checks the migration-produced SQLite schema for:
1. ``resource_links.link_type`` defaulting to ``contains``.
2. ``checkpoint_metadata`` foreign keys to ``decisions`` and ``resources``.
3. ``idx_decisions_superseded`` partial index with
``WHERE superseded_by IS NOT NULL``.
4. Runtime SQLite foreign key enforcement for ``checkpoint_metadata``.
Subcommands (each independent for isolated failure reporting):
- ``schema-parity-link-type``: Verifies resource_links.link_type column
and default.
- ``schema-parity-fks``: Verifies checkpoint_metadata FK constraints and
runtime enforcement (orphan rejection + positive path).
- ``schema-parity-index``: Verifies idx_decisions_superseded partial index.
"""
from __future__ import annotations
import sys
import tempfile
from collections.abc import Callable
from pathlib import Path
from typing import Any
from sqlalchemy import create_engine, inspect, text
from sqlalchemy.exc import IntegrityError
from cleveragents.infrastructure.database.migration_runner import MigrationRunner
def _cleanup_db_files(path: str) -> None:
Path(path).unlink(missing_ok=True)
Path(f"{path}-wal").unlink(missing_ok=True)
Path(f"{path}-shm").unlink(missing_ok=True)
def _setup_migrated_db() -> tuple[Any, Any, str]:
"""Create a temp DB, run migrations, return (engine, inspector, db_path)."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp_file:
db_path = tmp_file.name
db_url = f"sqlite:///{db_path}"
MigrationRunner(db_url).init_or_upgrade()
engine = create_engine(db_url, connect_args={"check_same_thread": False})
inspector = inspect(engine)
return engine, inspector, db_path
def _teardown_db(engine: Any, db_path: str) -> None:
if engine is not None:
engine.dispose()
_cleanup_db_files(db_path)
def _ensure_test_action_and_plan(conn: Any) -> None:
"""Insert prerequisite action and plan rows for FK tests.
.. note::
This function is **not idempotent** — it performs blind INSERTs
without existence checks. It is safe only when called against a
freshly created database (as ``_setup_migrated_db`` provides).
If idempotent behaviour is needed, see the Behave counterpart in
``features/steps/db_schema_parity_steps.py``.
"""
conn.execute(
text(
"""
INSERT INTO actions (
namespaced_name, namespace, name, description,
definition_of_done, strategy_actor, execution_actor,
created_at, updated_at
) VALUES (
:namespaced_name, :namespace, :name, :description,
:definition_of_done, :strategy_actor, :execution_actor,
:created_at, :updated_at
)
"""
),
{
"namespaced_name": "local/test-action",
"namespace": "local",
"name": "test-action",
"description": "test action",
"definition_of_done": "test dod",
"strategy_actor": "local/strategy",
"execution_actor": "local/execution",
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00",
},
)
conn.execute(
text(
"""
INSERT INTO v3_plans (
plan_id, root_plan_id, action_name,
namespaced_name, namespace,
description, created_at, updated_at
) VALUES (
:plan_id, :root_plan_id, :action_name,
:namespaced_name, :namespace,
:description, :created_at, :updated_at
)
"""
),
{
"plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"root_plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"action_name": "local/test-action",
"namespaced_name": "local/test-plan",
"namespace": "local",
"description": "test plan",
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00",
},
)
# ---------------------------------------------------------------------------
# Subcommand: schema-parity-link-type
# ---------------------------------------------------------------------------
def _schema_parity_link_type() -> None:
engine, inspector, db_path = _setup_migrated_db()
try:
resource_link_columns = inspector.get_columns("resource_links")
link_type = next(
(
column
for column in resource_link_columns
if column["name"] == "link_type"
),
None,
)
assert link_type is not None, "resource_links.link_type column is missing"
default = str(link_type.get("default") or "").lower()
assert "contains" in default, (
"resource_links.link_type default must include 'contains', "
f"got {link_type.get('default')!r}"
)
print("schema-parity-link-type-ok")
finally:
_teardown_db(engine, db_path)
# ---------------------------------------------------------------------------
# Subcommand: schema-parity-fks
# ---------------------------------------------------------------------------
def _schema_parity_fks() -> None:
engine, inspector, db_path = _setup_migrated_db()
try:
checkpoint_fks = inspector.get_foreign_keys("checkpoint_metadata")
signatures = {
(
tuple(fk.get("constrained_columns") or []),
fk.get("referred_table"),
tuple(fk.get("referred_columns") or []),
)
for fk in checkpoint_fks
}
assert (("decision_id",), "decisions", ("decision_id",)) in signatures, (
"Missing checkpoint_metadata FK decision_id -> decisions.decision_id"
)
assert (("resource_id",), "resources", ("resource_id",)) in signatures, (
"Missing checkpoint_metadata FK resource_id -> resources.resource_id"
)
with engine.begin() as conn:
_ensure_test_action_and_plan(conn)
try:
conn.execute(
text(
"""
INSERT INTO checkpoint_metadata (
checkpoint_id, plan_id, decision_id,
checkpoint_type, resource_id, sandbox_ref,
filesystem_path, created_at
) VALUES (
:checkpoint_id, :plan_id, :decision_id,
:checkpoint_type, :resource_id, :sandbox_ref,
:filesystem_path, :created_at
)
"""
),
{
"checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
"plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
"checkpoint_type": "manual",
"resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FAY",
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
except IntegrityError:
pass
else:
raise AssertionError(
"checkpoint_metadata accepted orphan decision/resource references"
)
# Verify each FK independently: orphan decision_id only
with engine.begin() as conn:
try:
conn.execute(
text(
"""
INSERT INTO checkpoint_metadata (
checkpoint_id, plan_id, decision_id,
checkpoint_type, sandbox_ref,
filesystem_path, created_at
) VALUES (
:checkpoint_id, :plan_id, :decision_id,
:checkpoint_type, :sandbox_ref,
:filesystem_path, :created_at
)
"""
),
{
"checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FC0",
"plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FC1",
"checkpoint_type": "manual",
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
except IntegrityError:
pass
else:
raise AssertionError(
"checkpoint_metadata accepted orphan decision_id independently"
)
# Verify each FK independently: orphan resource_id only
with engine.begin() as conn:
try:
conn.execute(
text(
"""
INSERT INTO checkpoint_metadata (
checkpoint_id, plan_id, resource_id,
checkpoint_type, sandbox_ref,
filesystem_path, created_at
) VALUES (
:checkpoint_id, :plan_id, :resource_id,
:checkpoint_type, :sandbox_ref,
:filesystem_path, :created_at
)
"""
),
{
"checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FC2",
"plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FC3",
"checkpoint_type": "manual",
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
except IntegrityError:
pass
else:
raise AssertionError(
"checkpoint_metadata accepted orphan resource_id independently"
)
# Positive test: valid FK references should be accepted
with engine.begin() as conn:
decision_id = "01ARZ3NDEKTSV4RRFFQ69G5FC4"
resource_id = "01ARZ3NDEKTSV4RRFFQ69G5FC5"
conn.execute(
text(
"""
INSERT INTO decisions (
decision_id, plan_id, decision_type, question,
chosen_option, context_snapshot_json, sequence_number,
created_at
) VALUES (
:decision_id, :plan_id, :decision_type, :question,
:chosen_option, :context_snapshot_json, :sequence_number,
:created_at
)
"""
),
{
"decision_id": decision_id,
"plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"decision_type": "strategy_choice",
"question": "test question",
"chosen_option": "test option",
"context_snapshot_json": "{}",
"sequence_number": 1,
"created_at": "2026-01-01T00:00:00",
},
)
resource_columns = {
col["name"] for col in inspect(engine).get_columns("resources")
}
cols = "resource_id, type_name, resource_kind, created_at, updated_at"
vals = ":resource_id, :type_name, :resource_kind, :created_at, :updated_at"
res_params: dict[str, str] = {
"resource_id": resource_id,
"type_name": "git-checkout",
"resource_kind": "physical",
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00",
}
if "namespaced_name" in resource_columns:
cols = (
"resource_id, namespaced_name, type_name,"
" resource_kind, created_at, updated_at"
)
vals = (
":resource_id, :namespaced_name, :type_name,"
" :resource_kind, :created_at, :updated_at"
)
res_params["namespaced_name"] = f"local/{resource_id}"
conn.execute(
text(f"INSERT INTO resources ({cols}) VALUES ({vals})"),
res_params,
)
conn.execute(
text(
"""
INSERT INTO checkpoint_metadata (
checkpoint_id, plan_id, decision_id,
checkpoint_type, resource_id, sandbox_ref,
filesystem_path, created_at
) VALUES (
:checkpoint_id, :plan_id, :decision_id,
:checkpoint_type, :resource_id, :sandbox_ref,
:filesystem_path, :created_at
)
"""
),
{
"checkpoint_id": "01ARZ3NDEKTSV4RRFFQ69G5FC6",
"plan_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"decision_id": decision_id,
"checkpoint_type": "manual",
"resource_id": resource_id,
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
print("schema-parity-fks-ok")
finally:
_teardown_db(engine, db_path)
# ---------------------------------------------------------------------------
# Subcommand: schema-parity-index
# ---------------------------------------------------------------------------
def _schema_parity_index() -> None:
engine, inspector, db_path = _setup_migrated_db()
try:
decision_indexes = inspector.get_indexes("decisions")
partial_index = next(
(
index
for index in decision_indexes
if index.get("name") == "idx_decisions_superseded"
),
None,
)
assert partial_index is not None, "idx_decisions_superseded is missing"
assert list(partial_index.get("column_names") or []) == ["superseded_by"], (
"idx_decisions_superseded must index decisions.superseded_by"
)
with engine.connect() as conn:
row = conn.execute(
text(
"SELECT sql FROM sqlite_master "
"WHERE type = 'index' AND name = :index_name"
),
{"index_name": "idx_decisions_superseded"},
).fetchone()
assert row is not None, "sqlite_master is missing idx_decisions_superseded"
sql = str(row[0] or "").lower()
assert "where superseded_by is not null" in sql, (
"idx_decisions_superseded is not a partial index"
)
print("schema-parity-index-ok")
finally:
_teardown_db(engine, db_path)
# ---------------------------------------------------------------------------
# Legacy combined subcommand (kept for backward compatibility)
# ---------------------------------------------------------------------------
def _schema_parity() -> None:
_schema_parity_link_type()
_schema_parity_fks()
_schema_parity_index()
print("schema-parity-ok")
_COMMANDS: dict[str, Callable[[], None]] = {
"schema-parity": _schema_parity,
"schema-parity-link-type": _schema_parity_link_type,
"schema-parity-fks": _schema_parity_fks,
"schema-parity-index": _schema_parity_index,
}
def main() -> None:
if len(sys.argv) < 2:
raise SystemExit(f"Expected command argument. Valid: {', '.join(_COMMANDS)}")
command = sys.argv[1]
handler = _COMMANDS.get(command)
if handler is None:
raise SystemExit(f"Unknown command: {command}. Valid: {', '.join(_COMMANDS)}")
handler()
if __name__ == "__main__":
main()