Implement PostgreSQL production store path
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s

Add the PostgreSQL backend, migration and stopped-write transfer tools, lease-aware deployment manifests, tenancy declarations, and shared conformance coverage. Persist grouping mutations in durable stores and separate process liveness from database readiness.
This commit is contained in:
tegwick 2026-08-19 14:42:01 +02:00
parent 2063470ac8
commit 749461b97b
30 changed files with 2364 additions and 71 deletions

View file

@ -1,6 +1,6 @@
"""TEN-WP-0005-T02/T04: one lifecycle contract, both store backends.
"""TEN-WP-0005-T02/T04: one lifecycle contract, every store backend.
Every test here is parametrised over the in-memory and SQLite stores so the
Every test here is parametrised over the in-memory, SQLite, and PostgreSQL stores so the
durable backend cannot silently diverge from the reference semantics -- the
divergence that matters (CAS, idempotency, retirement guards) is exactly the
kind a single-backend suite would miss.
@ -9,6 +9,7 @@ kind a single-backend suite would miss.
from datetime import UTC, datetime
import pytest
from postgres_backend import clean_postgres_store
from tenant_engine.domain import (
CapabilityRole,
@ -30,11 +31,13 @@ from tenant_engine.store import (
NOW = datetime(2026, 8, 10, 12, 0, tzinfo=UTC)
@pytest.fixture(params=["memory", "sqlite"])
@pytest.fixture(params=["memory", "sqlite", "postgres"])
def store(request, tmp_path):
if request.param == "memory":
return InMemoryTenantStore()
return SQLiteTenantStore(str(tmp_path / "tenant.db"))
if request.param == "sqlite":
return SQLiteTenantStore(str(tmp_path / "tenant.db"))
return clean_postgres_store(tmp_path)
@pytest.fixture
@ -78,6 +81,22 @@ def test_update_persists_and_bumps_version(store, tenant) -> None:
assert store.get_tenant("t-1").display_name == "Binky Ltd"
def test_grouping_mutation_persists(store, tenant) -> None:
updated, replayed = store.mutate_tenant(
tenant_id="t-1",
expected_version=1,
mutate=lambda current: current.with_grouping("large", at=NOW),
event_type="tenant_grouping_changed",
evidence={"actor": "ops", "reason": "growth", "correlation_id": "corr-grouping"},
idempotency_key="grouping-large",
request_fingerprint="fp-grouping-large",
)
assert replayed is False
assert updated.grouping == "large"
assert store.get_tenant("t-1").grouping == "large"
def test_stale_version_is_rejected(store, tenant) -> None:
_rename(store, key="k1", version=1)