Files
cleveragents-core/features/steps/db_schema_parity_steps.py
T
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

428 lines
15 KiB
Python

"""Step definitions for db_migration_lifecycle.feature — schema parity.
FK structure verification, orphan rejection, valid references, and
partial index checks for the ``resource_links``, ``checkpoint_metadata``,
and ``decisions`` tables.
"""
from __future__ import annotations
from typing import Any
from behave import then
from sqlalchemy import inspect as sa_inspect
from sqlalchemy import text
from sqlalchemy.exc import IntegrityError
# ---------------------------------------------------------------------------
# Shared test-data helpers
# ---------------------------------------------------------------------------
def _ensure_test_action_and_plan(conn: Any, action_name: str, plan_id: str) -> None:
"""Insert prerequisite action and plan rows if absent."""
existing_action = conn.execute(
text("SELECT 1 FROM actions WHERE namespaced_name = :n"),
{"n": action_name},
).fetchone()
existing_plan = conn.execute(
text("SELECT 1 FROM v3_plans WHERE plan_id = :pid"),
{"pid": plan_id},
).fetchone()
if existing_action is not None and existing_plan is not None:
return
if existing_action is None:
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": action_name,
"namespace": "local",
"name": action_name.split("/")[-1],
"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",
},
)
if existing_plan is None:
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": plan_id,
"root_plan_id": plan_id,
"action_name": action_name,
"namespaced_name": "local/test-plan-fk",
"namespace": "local",
"description": "test plan",
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00",
},
)
def _insert_test_resource(
conn: Any,
resource_id: str,
type_name: str = "git-checkout",
resource_kind: str = "physical",
) -> None:
"""Insert a test resource row, handling the optional namespaced_name column."""
resource_columns = {
col["name"] for col in sa_inspect(conn).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"
params: dict[str, str] = {
"resource_id": resource_id,
"type_name": type_name,
"resource_kind": resource_kind,
"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"
)
params["namespaced_name"] = f"local/{resource_id}"
conn.execute(text(f"INSERT INTO resources ({cols}) VALUES ({vals})"), params)
# ---------------------------------------------------------------------------
# Then — link_type default verification
# ---------------------------------------------------------------------------
@then('the "resource_links" table should include "link_type" with default "contains"')
def step_resource_links_has_link_type_default(context: Any) -> None:
inspector = sa_inspect(context.engine)
columns = inspector.get_columns("resource_links")
link_type = next(
(column for column in 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, (
"Expected resource_links.link_type default to include 'contains', "
f"got: {link_type.get('default')!r}"
)
# ---------------------------------------------------------------------------
# Then — checkpoint FK structure verification
# ---------------------------------------------------------------------------
@then("checkpoint_metadata should enforce decision and resource foreign keys")
def step_checkpoint_metadata_foreign_keys(context: Any) -> None:
inspector = sa_inspect(context.engine)
foreign_keys = 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 foreign_keys
}
decision_fk = (("decision_id",), "decisions", ("decision_id",))
resource_fk = (("resource_id",), "resources", ("resource_id",))
assert decision_fk in signatures, (
"Missing checkpoint_metadata foreign key for decision_id -> "
"decisions.decision_id"
)
assert resource_fk in signatures, (
"Missing checkpoint_metadata foreign key for resource_id -> "
"resources.resource_id"
)
# ---------------------------------------------------------------------------
# Then — orphan FK rejection (combined)
# ---------------------------------------------------------------------------
@then("checkpoint_metadata foreign keys should reject orphan references")
def step_checkpoint_metadata_foreign_keys_reject_orphans(context: Any) -> None:
action_name = "local/test-action"
plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
with context.engine.begin() as conn:
_ensure_test_action_and_plan(conn, action_name, plan_id)
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": plan_id,
"decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
"checkpoint_type": "manual",
"resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FAY",
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
except IntegrityError:
return
raise AssertionError(
"checkpoint_metadata accepted orphan decision/resource references"
)
# ---------------------------------------------------------------------------
# Then — independent orphan FK rejection
# ---------------------------------------------------------------------------
@then("checkpoint_metadata should reject orphan decision_id independently")
def step_checkpoint_reject_orphan_decision_only(context: Any) -> None:
action_name = "local/test-action-fk-d"
plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB0"
with context.engine.begin() as conn:
_ensure_test_action_and_plan(conn, action_name, plan_id)
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": "01ARZ3NDEKTSV4RRFFQ69G5FB1",
"plan_id": plan_id,
"decision_id": "01ARZ3NDEKTSV4RRFFQ69G5FB2",
"checkpoint_type": "manual",
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
except IntegrityError:
return
raise AssertionError(
"checkpoint_metadata accepted orphan decision_id with NULL resource_id"
)
@then("checkpoint_metadata should reject orphan resource_id independently")
def step_checkpoint_reject_orphan_resource_only(context: Any) -> None:
action_name = "local/test-action-fk-r"
plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB3"
with context.engine.begin() as conn:
_ensure_test_action_and_plan(conn, action_name, plan_id)
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": "01ARZ3NDEKTSV4RRFFQ69G5FB4",
"plan_id": plan_id,
"resource_id": "01ARZ3NDEKTSV4RRFFQ69G5FB5",
"checkpoint_type": "manual",
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
except IntegrityError:
return
raise AssertionError(
"checkpoint_metadata accepted orphan resource_id with NULL decision_id"
)
# ---------------------------------------------------------------------------
# Then — valid FK acceptance
# ---------------------------------------------------------------------------
@then("checkpoint_metadata should accept valid decision and resource references")
def step_checkpoint_accept_valid_references(context: Any) -> None:
action_name = "local/test-action-fk-v"
plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FB6"
decision_id = "01ARZ3NDEKTSV4RRFFQ69G5FB7"
resource_id = "01ARZ3NDEKTSV4RRFFQ69G5FB8"
with context.engine.begin() as conn:
_ensure_test_action_and_plan(conn, action_name, plan_id)
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": plan_id,
"decision_type": "strategy_choice",
"question": "test question",
"chosen_option": "test option",
"context_snapshot_json": "{}",
"sequence_number": 1,
"created_at": "2026-01-01T00:00:00",
},
)
_insert_test_resource(conn, resource_id)
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": "01ARZ3NDEKTSV4RRFFQ69G5FB9",
"plan_id": plan_id,
"decision_id": decision_id,
"checkpoint_type": "manual",
"resource_id": resource_id,
"sandbox_ref": "test-ref",
"filesystem_path": "",
"created_at": "2026-01-01T00:00:00",
},
)
# ---------------------------------------------------------------------------
# Then — partial index verification
# ---------------------------------------------------------------------------
@then(
'the "decisions" table should have partial index "{index_name}" on "{column_name}"'
)
def step_decisions_has_partial_index(
context: Any,
index_name: str,
column_name: str,
) -> None:
inspector = sa_inspect(context.engine)
indexes = inspector.get_indexes("decisions")
index = next((idx for idx in indexes if idx.get("name") == index_name), None)
assert index is not None, f"Index {index_name} not found on decisions"
columns = list(index.get("column_names") or [])
assert columns == [column_name], (
f"Index {index_name} expected on [{column_name!r}], got {columns!r}"
)
# Verify partial WHERE clause via sqlite_master (SQLite-only).
if context.engine.dialect.name == "sqlite":
with context.engine.connect() as conn:
row = conn.execute(
text(
"SELECT sql FROM sqlite_master "
"WHERE type = 'index' AND name = :index_name"
),
{"index_name": index_name},
).fetchone()
assert row is not None, f"sqlite_master entry missing for index {index_name}"
sql = str(row[0] or "").lower()
assert "where superseded_by is not null" in sql, (
f"Expected partial WHERE clause on {index_name}, got SQL: {row[0]!r}"
)