135481f9a6
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 18s
CI / build (pull_request) Successful in 16s
CI / unit_tests (pull_request) Failing after 25s
CI / security (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 36s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Failing after 25s
CI / integration_tests (pull_request) Failing after 2m19s
CI / benchmark-regression (pull_request) Has been cancelled
- P2-1: Add field_validator('inherits') on ResourceTypeSpec
- P2-2: Remove 'properties' from _COLLECTION_FIELDS (scalar override)
- P2-3: Add behave scenarios for exception rollback paths
- P2-4: Add ordering comment to BUILTIN_TYPES
- P2-5: Define RegistryHost Protocol, remove type:ignore on mixins
- P2-6: Add if_not_exists=True on migration index creation
- P3-1: Use deque.popleft() in get_ancestors for O(1) BFS
- P3-2: Filter private attrs in _to_dict vars() fallback
- P3-3: Replace _handler_cache import with clear_handler_cache()
- P3-4: Add migration downgrade test for m6_004
- P3-5: Extract ResourceDagMixin to _resource_registry_dag.py
All files remain under 500-line CONTRIBUTING limit.
Lint, typecheck, and 85 behave scenarios pass.
132 lines
4.8 KiB
Python
132 lines
4.8 KiB
Python
"""Steps for exception rollback paths (P2-3) and migration downgrade (P3-4)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when # type: ignore[import-untyped]
|
|
from behave.runner import Context # type: ignore[import-untyped]
|
|
from sqlalchemy import create_engine, inspect, text
|
|
|
|
from cleveragents.application.services.resource_registry_service import (
|
|
ResourceRegistryService,
|
|
)
|
|
from cleveragents.core.exceptions import ValidationError
|
|
|
|
|
|
@when('I call remove_type for "test/removable" with a simulated commit error for cov')
|
|
def step_remove_type_commit_error(context: Context) -> None:
|
|
svc: ResourceRegistryService = context.cov_svc
|
|
original_factory = svc._session_factory
|
|
|
|
def failing_factory(): # type: ignore[no-untyped-def]
|
|
session = original_factory()
|
|
original_delete = session.delete
|
|
|
|
def delete_then_fail(obj): # type: ignore[no-untyped-def]
|
|
original_delete(obj)
|
|
session.flush()
|
|
raise RuntimeError("simulated DB error")
|
|
|
|
session.delete = delete_then_fail
|
|
return session
|
|
|
|
svc._session_factory = failing_factory
|
|
try:
|
|
svc.remove_type("test/removable")
|
|
except RuntimeError as exc:
|
|
context.cov_error = exc
|
|
finally:
|
|
svc._session_factory = original_factory
|
|
|
|
|
|
@then("a RuntimeError should be raised for cov")
|
|
def step_runtime_error(context: Context) -> None:
|
|
assert context.cov_error is not None, "Expected error"
|
|
assert isinstance(context.cov_error, RuntimeError), (
|
|
f"Expected RuntimeError, got {type(context.cov_error).__name__}"
|
|
)
|
|
|
|
|
|
@when("I call resolve_type_inheritance_chain with a cycle-induced ValueError for cov")
|
|
def step_resolve_chain_value_error(context: Context) -> None:
|
|
svc: ResourceRegistryService = context.cov_svc
|
|
with patch(
|
|
"cleveragents.application.services.resource_registry_service"
|
|
".resolve_inheritance_chain",
|
|
side_effect=ValueError("Cycle detected in inheritance chain"),
|
|
):
|
|
try:
|
|
svc.resolve_type_inheritance_chain("git-checkout")
|
|
except ValidationError as exc:
|
|
context.cov_error = exc
|
|
|
|
|
|
@when("I call register_resource with a simulated commit error for cov")
|
|
def step_register_resource_commit_error(context: Context) -> None:
|
|
svc: ResourceRegistryService = context.cov_svc
|
|
original_factory = svc._session_factory
|
|
|
|
def failing_factory(): # type: ignore[no-untyped-def]
|
|
session = original_factory()
|
|
|
|
def boom() -> None:
|
|
raise RuntimeError("simulated DB error on commit")
|
|
|
|
session.commit = boom
|
|
return session
|
|
|
|
svc._session_factory = failing_factory
|
|
try:
|
|
svc.register_resource("git-checkout", name="test/boom", location="/tmp/boom")
|
|
except RuntimeError as exc:
|
|
context.cov_error = exc
|
|
finally:
|
|
svc._session_factory = original_factory
|
|
|
|
|
|
# ── P3-4: Migration m6_004 downgrade steps ───────────────────────────────
|
|
|
|
|
|
@given("an in-memory database with the inherits column applied")
|
|
def step_db_with_inherits(context: Context) -> None:
|
|
engine = create_engine("sqlite:///:memory:")
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE resource_types ( name TEXT PRIMARY KEY, inherits TEXT)"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text("CREATE INDEX ix_resource_types_inherits ON resource_types (inherits)")
|
|
)
|
|
context.mig_engine = engine
|
|
|
|
|
|
@when("I run the m6_004 downgrade")
|
|
def step_run_downgrade(context: Context) -> None:
|
|
# The real downgrade() uses op.drop_index / op.drop_column which
|
|
# require an Alembic migration context. We simulate the same DDL
|
|
# operations directly to verify the schema change is reversible.
|
|
engine = context.mig_engine
|
|
with engine.begin() as conn:
|
|
conn.execute(text("DROP INDEX IF EXISTS ix_resource_types_inherits"))
|
|
# SQLite doesn't support DROP COLUMN before 3.35; recreate table.
|
|
conn.execute(text("CREATE TABLE resource_types_new ( name TEXT PRIMARY KEY)"))
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO resource_types_new (name) SELECT name FROM resource_types"
|
|
)
|
|
)
|
|
conn.execute(text("DROP TABLE resource_types"))
|
|
conn.execute(text("ALTER TABLE resource_types_new RENAME TO resource_types"))
|
|
|
|
|
|
@then("the resource_types table should not have an inherits column")
|
|
def step_no_inherits_column(context: Context) -> None:
|
|
inspector = inspect(context.mig_engine)
|
|
columns = [c["name"] for c in inspector.get_columns("resource_types")]
|
|
assert "inherits" not in columns, (
|
|
f"Expected 'inherits' column to be removed, but found columns: {columns}"
|
|
)
|