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
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
428 lines
15 KiB
Python
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}"
|
|
)
|