114 lines
5.0 KiB
Python
114 lines
5.0 KiB
Python
"""Disposable PostgreSQL route CAS and runtime-role transaction qualification."""
|
|
|
|
import importlib
|
|
import threading
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
import pytest
|
|
from sqlalchemy.orm import Session
|
|
|
|
from modelforge_api.domain.lifecycle_contracts import ApprovalDecision
|
|
from modelforge_api.persistence.models import CapabilityContract, LifecycleOperation
|
|
from modelforge_api.persistence.serving_repository import ServingRepository
|
|
from modelforge_api.services.lifecycle import LifecycleError, LifecycleService
|
|
from tests.test_lifecycle_serving import approved_plan, build_pair
|
|
from tests.test_managed_identities_postgres import IdentityDatabase, identity_database
|
|
|
|
__all__ = ["identity_database"]
|
|
|
|
|
|
def test_two_committers_switch_once_and_exact_rollback_increments_generation(
|
|
identity_database: IdentityDatabase,
|
|
) -> None:
|
|
database = identity_database
|
|
importlib.import_module("alembic.command").upgrade(database.migration(), "head")
|
|
with Session(database.runtime) as session:
|
|
pair = build_pair(session)
|
|
_, _, current, candidate, selection, _ = pair
|
|
_, _, operation, _ = approved_plan(pair)
|
|
operation_id, current_id, candidate_id = operation.id, current.id, candidate.id
|
|
contract_id, generation = (
|
|
current.capability_contract_id,
|
|
selection.expected_route_generation,
|
|
)
|
|
|
|
barrier = threading.Barrier(2)
|
|
|
|
def contender(_index: int) -> str:
|
|
with Session(database.runtime) as session:
|
|
barrier.wait(timeout=30)
|
|
try:
|
|
return (
|
|
LifecycleService(session)
|
|
.commit_operation(
|
|
operation_id,
|
|
ApprovalDecision(
|
|
actor="qualification", reason="Concurrent explicit commit qualification"
|
|
),
|
|
)
|
|
.stage
|
|
)
|
|
except LifecycleError:
|
|
session.rollback()
|
|
return "CONFLICT"
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
# Same-operation retries return its persisted result, without a second switch.
|
|
assert list(pool.map(contender, [1, 2])) == ["COMMITTED", "COMMITTED"]
|
|
with Session(database.runtime) as session:
|
|
assert ServingRepository(session).stable_deployment(contract_id).id == candidate_id
|
|
assert session.get(CapabilityContract, contract_id).serving_generation == generation + 1
|
|
assert session.get(LifecycleOperation, operation_id).stage == "COMMITTED"
|
|
restored = LifecycleService(session).rollback_operation(
|
|
operation_id,
|
|
ApprovalDecision(actor="qualification", reason="Restore exact prior serving route"),
|
|
)
|
|
assert restored.stage == "ROLLED_BACK"
|
|
assert ServingRepository(session).stable_deployment(contract_id).id == current_id
|
|
assert session.get(CapabilityContract, contract_id).serving_generation == generation + 2
|
|
with pytest.raises(RuntimeError, match="Serving route history exists"):
|
|
importlib.import_module("alembic.command").downgrade(database.migration(), "20260909_0033")
|
|
|
|
|
|
def test_current_route_generation_survives_postgres_dump_restore(
|
|
identity_database: IdentityDatabase, tmp_path
|
|
):
|
|
from modelforge_api.services.recovery_fingerprint import (
|
|
BENCHMARK_RUN_SCHEMA,
|
|
audit_fingerprint_differences_are_compatible,
|
|
fingerprint_compatibility,
|
|
semantic_fingerprint,
|
|
)
|
|
|
|
database = identity_database
|
|
importlib.import_module("alembic.command").upgrade(database.migration(), "head")
|
|
with Session(database.runtime) as session:
|
|
pair = build_pair(session)
|
|
_, _, operation, _ = approved_plan(pair)
|
|
LifecycleService(session).commit_operation(
|
|
operation.id,
|
|
ApprovalDecision(
|
|
actor="qualification", reason="Qualify retained committed route after restore"
|
|
),
|
|
)
|
|
contract_id = pair[2].capability_contract_id
|
|
generation = session.get(CapabilityContract, contract_id).serving_generation
|
|
assert generation > 0
|
|
source = semantic_fingerprint(session.connection())
|
|
archive = tmp_path / "committed-route-current.dump"
|
|
database.archive(archive)
|
|
database.archive(archive, restore=True)
|
|
with Session(database.restored_runtime) as session:
|
|
restored = semantic_fingerprint(session.connection())
|
|
compatibility = fingerprint_compatibility(
|
|
source, restored, source_schema_revision=BENCHMARK_RUN_SCHEMA
|
|
)
|
|
assert compatibility.compatible
|
|
assert audit_fingerprint_differences_are_compatible(source, restored, compatibility)
|
|
assert session.get(CapabilityContract, contract_id).serving_generation == generation
|
|
session.get(CapabilityContract, contract_id).serving_generation = 0
|
|
session.flush()
|
|
changed = semantic_fingerprint(session.connection())
|
|
assert not audit_fingerprint_differences_are_compatible(source, changed, compatibility)
|
|
session.rollback()
|