feat: add durable Core Hub absorption runtime
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 2s

This commit is contained in:
tegwick 2026-08-21 16:16:42 +02:00
parent 7e1ec03f0c
commit 8ab1d0c09a
20 changed files with 2423 additions and 15 deletions

View file

@ -0,0 +1,16 @@
{
"schemaVersion": "core-hub.migration.v1",
"source": "core-hub-test-fixture",
"sourceRevision": "fixture-revision",
"exportedAt": "2026-08-21T12:00:00Z",
"highWaterMark": "2026-08-21T11:00:00Z",
"records": {
"hubs": [{"id": "11111111-1111-4111-8111-111111111111", "slug": "ops-hub", "name": "Ops Hub", "createdAt": "2026-08-20T10:00:00Z", "updatedAt": "2026-08-20T11:00:00Z"}],
"hubCapabilityManifests": [{"id": "22222222-2222-4222-8222-222222222222", "hubId": "11111111-1111-4111-8111-111111111111", "manifestVersion": "1.0", "status": "active"}],
"apiConsumers": [{"id": "33333333-3333-4333-8333-333333333333", "slug": "ops-hub", "name": "ops-hub", "hubCapabilityManifestId": "22222222-2222-4222-8222-222222222222"}],
"apiKeys": [{"id": "44444444-4444-4444-8444-444444444444", "apiConsumerId": "33333333-3333-4333-8333-333333333333", "keyPrefix": "ch_fixture", "keyHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "scopes": "framework:read", "status": "active"}],
"widgets": [{"id": "55555555-5555-4555-8555-555555555555", "hubId": "11111111-1111-4111-8111-111111111111", "name": "Readiness", "widgetType": "ops-readiness-gate"}],
"interactionEvents": [{"id": "66666666-6666-4666-8666-666666666666", "widgetId": "55555555-5555-4555-8555-555555555555", "eventType": "ops-endpoint-verified", "metadata": {"expectedStatus": 401}, "createdAt": "2026-08-21T11:00:00Z"}],
"migrationRuns": [{"id": "77777777-7777-4777-8777-777777777777", "source": "inter-hub", "schemaVersion": "core-hub.migration.v1", "bundleSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "dryRun": false, "status": "imported", "counts": {}, "diagnostics": {}, "createdAt": "2026-08-19T09:00:00Z"}]
}
}

126
tests/test_compatibility.py Normal file
View file

@ -0,0 +1,126 @@
from __future__ import annotations
import asyncio
from fastapi.testclient import TestClient
from sqlalchemy.ext.asyncio import create_async_engine
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_metadata
GROUPS = frozenset({"system", "registry", "credentials", "interaction", "deferred", "operator"})
HEADERS = {"Authorization": "Bearer operator-token"}
def compatibility_client(tmp_path, *, write_groups: frozenset[str] = GROUPS) -> tuple[TestClient, PostgresPortStore]:
database_url = f"sqlite+aiosqlite:///{tmp_path / 'compat.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())
store = PostgresPortStore.from_url(database_url)
settings = RuntimeSettings(
environment="production",
backend="postgresql",
allow_ephemeral=False,
database_url=database_url,
api_token="operator-token",
v2_groups=GROUPS,
v2_write_groups=write_groups,
legacy_health=True,
)
return TestClient(create_app(settings=settings, port_store=store)), store
def test_route_group_configuration_rejects_two_writers() -> None:
try:
RuntimeSettings(
v2_groups=frozenset({"registry"}),
v2_write_groups=frozenset({"registry"}),
legacy_write_groups=frozenset({"registry"}),
)
except ValueError as exc:
assert "write groups overlap" in str(exc)
else:
raise AssertionError("overlapping writers were accepted")
def test_system_compatibility_and_protected_auth(tmp_path) -> None:
client, store = compatibility_client(tmp_path)
assert client.get("/healthz").json()["service"] == "core-hub"
assert client.get("/api/v2/widget-types").status_code == 200
denied = client.get("/api/v2/hubs")
assert denied.status_code == 401
assert denied.json()["detail"]["code"] == "unauthorized"
document = client.get("/api/v2/openapi.json").json()
assert "/api/v2/hubs" in document["paths"]
assert "/hubs" in document["paths"]
asyncio.run(store.aclose())
def test_ops_hub_bootstrap_and_dynamic_key(tmp_path) -> None:
client, store = compatibility_client(tmp_path)
hub = client.post(
"/api/v2/hubs",
headers=HEADERS,
json={"slug": "ops-hub", "name": "Ops Hub", "domain": "ops.coulomb.social"},
).json()
manifest = client.post(
"/api/v2/hub-capability-manifests",
headers=HEADERS,
json={"hubId": hub["id"], "manifestVersion": "1.0"},
).json()
activated = client.post(
f"/api/v2/hub-capability-manifests/{manifest['id']}/activate", headers=HEADERS
)
assert activated.json()["status"] == "active"
consumer = client.post(
"/api/v2/api-consumers",
headers=HEADERS,
json={"name": "ops-hub", "hubCapabilityManifestId": manifest["id"]},
).json()
issued = client.post(
f"/api/v2/api-consumers/{consumer['id']}/api-keys",
headers=HEADERS,
json={"scopes": "framework:read hub:ops-hub:write"},
).json()
runtime_headers = {"Authorization": f"Bearer {issued['fullKey']}"}
widget = client.post(
"/api/v2/widgets",
headers=runtime_headers,
json={"hubId": hub["id"], "name": "Readiness", "widgetType": "ops-readiness-gate"},
).json()
event = client.post(
"/api/v2/interaction-events",
headers=runtime_headers,
json={
"widgetId": widget["id"],
"eventType": "ops-endpoint-verified",
"metadata": {"expectedStatus": 401},
},
)
assert event.status_code == 201
assert client.get("/api/v2/hub-registry", headers=runtime_headers).status_code == 200
assert client.get("/api/v2/interaction-events", headers=runtime_headers).json()["count"] == 1
assert "fullKey" not in str(client.get("/api/v2/api-consumers", headers=HEADERS).json())
asyncio.run(store.aclose())
def test_read_only_group_rejects_write(tmp_path) -> None:
client, store = compatibility_client(tmp_path, write_groups=frozenset())
response = client.post(
"/api/v2/hubs", headers=HEADERS, json={"slug": "ops-hub", "name": "Ops Hub"}
)
assert response.status_code == 503
assert response.json()["detail"] == "compatibility group is read-only"
asyncio.run(store.aclose())

97
tests/test_migration.py Normal file
View file

@ -0,0 +1,97 @@
import json
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from hub_core.runtime.migration import (
bundle_digest,
export_bundle,
import_bundle,
validate_bundle,
)
from hub_core.runtime.postgres_store import PostgresPortStore
from hub_core.runtime.tables import runtime_metadata
def bundle() -> dict:
value = {
"schemaVersion": "core-hub.migration.v1",
"source": "core-hub",
"sourceRevision": "abc123",
"exportedAt": "2026-08-21T12:00:00Z",
"highWaterMark": "2026-08-21T11:00:00Z",
"records": {
"hubs": [{"id": "hub-1", "slug": "ops-hub", "name": "Ops Hub", "createdAt": "2026-08-20T10:00:00Z", "updatedAt": "2026-08-20T11:00:00Z"}],
"hubCapabilityManifests": [{"id": "manifest-1", "hubId": "hub-1", "manifestVersion": "1.0", "status": "active"}],
"apiConsumers": [{"id": "consumer-1", "slug": "ops-hub", "name": "ops-hub", "hubCapabilityManifestId": "manifest-1"}],
"apiKeys": [{"id": "key-1", "apiConsumerId": "consumer-1", "keyPrefix": "ch_prefix", "keyHash": "a" * 64, "scopes": "framework:read"}],
"widgets": [{"id": "widget-1", "hubId": "hub-1", "name": "Readiness", "widgetType": "ops-readiness-gate"}],
"interactionEvents": [{"id": "event-1", "widgetId": "widget-1", "eventType": "ops-endpoint-verified", "metadata": {"status": 401}, "createdAt": "2026-08-21T11:00:00Z"}],
"migrationRuns": [{"id": "run-1", "source": "inter-hub", "schemaVersion": "core-hub.migration.v1", "bundleSha256": "b" * 64, "dryRun": False, "status": "imported", "counts": {}, "diagnostics": {}, "createdAt": "2026-08-19T09:00:00Z"}],
},
}
value["bundleSha256"] = bundle_digest(value)
return value
async def make_store(tmp_path) -> PostgresPortStore:
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'migration.db'}")
async with engine.begin() as connection:
await connection.run_sync(runtime_metadata.create_all)
return PostgresPortStore(engine)
def test_validation_rejects_secret_material_and_digest_drift():
value = bundle()
value["records"]["apiKeys"][0]["fullKey"] = "must-not-move"
report = validate_bundle(value)
assert report["ok"] is False
assert any("secret-shaped" in error for error in report["errors"])
assert any("bundleSha256" in error for error in report["errors"])
def test_import_is_idempotent_and_exports_all_seven_collections(tmp_path):
async def run():
store = await make_store(tmp_path)
try:
return (
await import_bundle(store, bundle()),
await import_bundle(store, bundle()),
await export_bundle(store, source_revision="target-revision"),
)
finally:
await store.aclose()
first, second, exported = asyncio.run(run())
assert first["ok"] is True
assert first["counts"]["migrationRuns"]["created"] == 1
assert second["idempotent"] is True
assert second["migrationRunId"] == first["migrationRunId"]
assert set(exported["records"]) == {
"hubs", "hubCapabilityManifests", "apiConsumers", "apiKeys",
"widgets", "interactionEvents", "migrationRuns",
}
assert exported["records"]["apiKeys"][0]["keyHash"] == "a" * 64
assert "fullKey" not in json.dumps(exported)
assert validate_bundle(exported)["ok"] is True
def test_dry_run_does_not_record_import(tmp_path):
async def run():
store = await make_store(tmp_path)
try:
return (
await import_bundle(store, bundle(), dry_run=True),
await import_bundle(store, bundle()),
)
finally:
await store.aclose()
report, followup = asyncio.run(run())
assert report["dryRun"] is True
assert "migrationRunId" not in report
assert followup["idempotent"] is False

View file

@ -0,0 +1,100 @@
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"
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 _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"},
}