from __future__ import annotations import asyncio import json from datetime import datetime, timezone from uuid import uuid4 from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import create_async_engine from hub_core.contracts import extension_contract_root from hub_core.runtime.app import create_app from hub_core.runtime.config import RuntimeSettings from hub_core.runtime.postgres_store import PostgresPortStore from hub_core.runtime.tables import runtime_audit_ledger, runtime_metadata def test_durable_store_survives_reopen_and_keeps_event_families_separate(tmp_path) -> None: database_url = f"sqlite+aiosqlite:///{tmp_path / 'runtime.db'}" async def create_schema() -> None: engine = create_async_engine(database_url) async with engine.begin() as connection: await connection.run_sync(runtime_metadata.create_all) await engine.dispose() asyncio.run(create_schema()) package = json.loads( extension_contract_root() .joinpath("fixtures", "ops-hub.extension.json") .read_text(encoding="utf-8") ) settings = RuntimeSettings( environment="production", backend="postgresql", allow_ephemeral=False, database_url=database_url, ) first_store = PostgresPortStore.from_url(database_url) first = TestClient(create_app(settings=settings, port_store=first_store)) correlation_id = str(uuid4()) assert first.get("/readyz").status_code == 200 assert first.post( "/ports/registry/registrations", headers={"X-Correlation-ID": correlation_id}, json=package, ).status_code == 202 assert first.post( "/ports/events/progress", json=_event("hub.progress.recorded") ).status_code == 202 assert first.post( "/ports/events/interaction", json=_event("hub.interaction.recorded") ).status_code == 202 asyncio.run(first_store.aclose()) second_store = PostgresPortStore.from_url(database_url) second = TestClient(create_app(settings=settings, port_store=second_store)) registry = second.get("/ports/projections/hub_registry").json() progress = second.get("/ports/projections/progress_events").json() interaction = second.get("/ports/projections/interaction_events").json() assert registry["data"]["items"][0]["descriptor"]["hub_slug"] == "ops-hub" assert {item["family"] for item in progress["data"]["items"]} == {"progress"} assert {item["family"] for item in interaction["data"]["items"]} == {"interaction"} assert progress["provenance"]["source_system"] == "hub-core-postgresql" assert registry["data"]["rebuild_from"] == ["hub_descriptors", "hub_manifests"] assert progress["data"]["rebuild_from"] == ["progress_events"] assert interaction["data"]["rebuild_from"] == ["interaction_events"] async def audit_count() -> int: async with second_store.sessions() as session: return len((await session.execute(runtime_audit_ledger.select())).all()) assert asyncio.run(audit_count()) == 3 asyncio.run(second_store.aclose()) def test_postgresql_readiness_fails_when_database_is_unavailable() -> None: database_url = "sqlite+aiosqlite:////definitely-missing-parent/runtime.db" store = PostgresPortStore.from_url(database_url) settings = RuntimeSettings( environment="production", backend="postgresql", allow_ephemeral=False, database_url=database_url, ) response = TestClient(create_app(settings=settings, port_store=store)).get("/readyz") assert response.status_code == 503 assert response.json()["checks"]["database"] == "unavailable" asyncio.run(store.aclose()) def test_postgresql_readiness_fails_when_runtime_tables_are_unavailable(tmp_path) -> None: database_url = f"sqlite+aiosqlite:///{tmp_path / 'empty.db'}" store = PostgresPortStore.from_url(database_url) settings = RuntimeSettings( environment="production", backend="postgresql", allow_ephemeral=False, database_url=database_url, ) response = TestClient(create_app(settings=settings, port_store=store)).get("/readyz") assert response.status_code == 503 assert response.json()["checks"]["database"] == "unavailable" asyncio.run(store.aclose()) def _event(event_type: str) -> dict: return { "schema_version": "0.1.0", "correlation_id": str(uuid4()), "event_type": event_type, "occurred_at": datetime.now(timezone.utc).isoformat(), "subject_refs": {"hub": "ops-hub"}, "payload": {"result": "ok"}, }