hub-core/tests/test_runtime.py
tegwick e89d621f18
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / pytest-smoke (push) Waiting to run
Close HUB-WP-0009 conformance gaps (C2, C7, C9, C10); mark blocked workplans
Implements the four residual conformance checks left open by the T04
minimal vertical:

- C2: GET /ports/registry/registrations/{hub_slug} resolves missing (404),
  ambiguous (shared reuse_surface_id across hub_slugs), and stale
  (deprecated/retired descriptor) registrations; a new .../audit route
  exposes queryable registration history from the existing in-memory
  history and the PostgreSQL runtime_audit_ledger.
- C7: harness proof that disabled compatibility groups deny access
  (404) with no fixture credentials involved, matching the existing
  fail-closed compat router behavior.
- C9: harness proof plus a dedicated test that /readyz degrades only on
  an unavailable configured dependency while unrelated disabled
  projections stay non-blocking.
- C10: ContractValidator now negotiates contract_version_min/max against
  the runtime's contract version and rejects incompatible or inverted
  ranges with an explicit 422 instead of silently accepting them.

HUB-WP-0009 is now finished. HUB-WP-0006 is marked blocked: its only open
task (T06) has no remaining hub-core code path and waits on an external
Forgejo identity/production deployment gate. HUB-WP-0011 is marked
blocked: T02/T03 already waited on external credential/deployment
review, and T01 needs a source/destination ownership and retention
decision against live message data before it can be implemented safely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 310936@bnt-lap001
Assistant-Session: 00cd9abe-09a0-416b-88e0-f907b9101629
2026-09-27 23:59:46 +02:00

312 lines
10 KiB
Python

from __future__ import annotations
import json
from datetime import datetime, timezone
from uuid import uuid4
from fastapi.testclient import TestClient
from hub_core.contracts import extension_contract_root
from hub_core.runtime.app import create_app
from hub_core.runtime.cli import _sync_database_url, build_parser
from hub_core.runtime.config import RuntimeSettings
from hub_core.runtime.store import InMemoryPortStore
def client(*, allow_ephemeral: bool = True, environment: str = "test") -> TestClient:
settings = RuntimeSettings(
environment=environment,
backend="memory",
allow_ephemeral=allow_ephemeral,
)
return TestClient(create_app(settings=settings, port_store=InMemoryPortStore()))
def ops_hub_package() -> dict:
fixture = extension_contract_root().joinpath("fixtures", "ops-hub.extension.json")
return json.loads(fixture.read_text(encoding="utf-8"))
def event_body(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"},
}
def test_health_and_ephemeral_readiness() -> None:
runtime = client()
health = runtime.get("/healthz")
ready = runtime.get("/readyz")
assert health.status_code == 200
assert health.json()["service"] == "hub-core"
assert ready.status_code == 200
assert ready.json()["status"] == "ok"
assert ready.json()["checks"]["active_backend"] == "memory"
def test_production_readiness_fails_closed_for_ephemeral_backend() -> None:
response = client(allow_ephemeral=False, environment="production").get("/readyz")
assert response.status_code == 503
assert response.json()["status"] == "degraded"
assert response.json()["checks"]["ephemeral_backend"] == "not_allowed"
def test_protected_compatibility_readiness_requires_authorization_dependency() -> None:
settings = RuntimeSettings(
environment="test",
backend="memory",
allow_ephemeral=True,
v2_groups=frozenset({"registry"}),
)
response = TestClient(
create_app(settings=settings, port_store=InMemoryPortStore())
).get("/readyz")
assert response.status_code == 503
assert response.json()["checks"]["authorization"] == "unavailable"
def test_registry_validates_and_registers_idempotently() -> None:
runtime = client()
package = ops_hub_package()
correlation_id = str(uuid4())
first = runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": correlation_id},
json=package,
)
second = runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": correlation_id},
json=package,
)
projection = runtime.get("/ports/projections/hub_registry")
assert first.status_code == 202
assert first.json()["status"] == "accepted"
assert second.status_code == 202
assert second.json()["status"] == "duplicate"
assert projection.status_code == 200
assert projection.json()["data"]["items"][0]["descriptor"]["hub_slug"] == "ops-hub"
def test_registry_rejects_contract_mismatch() -> None:
runtime = client()
package = ops_hub_package()
package["manifest"]["reuse_surface_id"] = "capability.operations.other-hub"
response = runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": str(uuid4())},
json=package,
)
assert response.status_code == 422
assert "reuse_surface_id must match" in response.json()["detail"]
def test_registry_rejects_incompatible_contract_version_range() -> None:
runtime = client()
package = ops_hub_package()
package["descriptor"]["contract_version_min"] = "9.9.9"
package["descriptor"]["contract_version_max"] = "9.9.9"
response = runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": str(uuid4())},
json=package,
)
assert response.status_code == 422
assert "incompatible with runtime contract version" in response.json()["detail"]
def test_registry_rejects_inverted_contract_version_range() -> None:
runtime = client()
package = ops_hub_package()
package["descriptor"]["contract_version_min"] = "0.2.0"
package["descriptor"]["contract_version_max"] = "0.1.0"
response = runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": str(uuid4())},
json=package,
)
assert response.status_code == 422
assert "must not exceed" in response.json()["detail"]
def test_registry_resolution_reports_missing_ambiguous_and_audit_history() -> None:
runtime = client()
package = ops_hub_package()
runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": str(uuid4())},
json=package,
)
missing = runtime.get("/ports/registry/registrations/never-registered")
ok_resolution = runtime.get("/ports/registry/registrations/ops-hub")
ok_audit = runtime.get("/ports/registry/registrations/ops-hub/audit")
assert missing.status_code == 404
assert ok_resolution.status_code == 200
assert ok_resolution.json()["data"]["resolution"] == "ok"
assert ok_audit.status_code == 200
assert len(ok_audit.json()["items"]) == 1
assert ok_audit.json()["items"][0]["data"]["action"] == "registry.accepted"
duplicate_package = {**package, "descriptor": {**package["descriptor"], "hub_slug": "ops-hub-2"}}
runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": str(uuid4())},
json=duplicate_package,
)
ambiguous_resolution = runtime.get("/ports/registry/registrations/ops-hub")
assert ambiguous_resolution.json()["data"]["resolution"] == "ambiguous"
assert ambiguous_resolution.json()["data"]["ambiguous_with"] == ["ops-hub-2"]
def test_registry_resolution_reports_stale_for_deprecated_descriptor() -> None:
runtime = client()
package = ops_hub_package()
package["descriptor"]["status"] = "deprecated"
runtime.post(
"/ports/registry/registrations",
headers={"X-Correlation-ID": str(uuid4())},
json=package,
)
resolution = runtime.get("/ports/registry/registrations/ops-hub")
assert resolution.json()["data"]["resolution"] == "stale"
def test_readiness_blocks_on_unavailable_dependency_but_not_disabled_ones() -> None:
class RejectingProjectionClient:
async def fetch_classification_page(self, cursor: str | None):
raise RuntimeError("upstream unavailable")
settings = RuntimeSettings(environment="test", backend="memory", allow_ephemeral=True)
runtime = TestClient(
create_app(
settings=settings,
port_store=InMemoryPortStore(),
repo_projection_client=RejectingProjectionClient(),
)
)
response = runtime.get("/readyz")
assert response.status_code == 503
assert response.json()["status"] == "degraded"
assert response.json()["checks"]["repo_manager_projection"] == "unavailable"
assert response.json()["checks"]["workload_projection"] == "not_applicable"
def test_messaging_port_writes_and_reads_conversation() -> None:
runtime = client()
conversation_id = uuid4()
body = {
"schema_version": "0.1.0",
"correlation_id": str(uuid4()),
"conversation_id": str(conversation_id),
"from_address": "agent:codex",
"to_addresses": ["hub:ops-hub", "agent:operator"],
"body": "Non-secret runtime smoke.",
"subject_refs": {"hub": "ops-hub"},
}
sent = runtime.post("/ports/messaging/messages", json=body)
listed = runtime.get(
"/ports/messaging/messages",
params={"address": "hub:ops-hub", "conversation_id": str(conversation_id)},
)
assert sent.status_code == 202
assert listed.status_code == 200
assert len(listed.json()["items"]) == 1
assert listed.json()["items"][0]["data"]["body"] == body["body"]
def test_progress_and_interaction_events_stay_separate() -> None:
runtime = client()
progress = runtime.post("/ports/events/progress", json=event_body("hub.progress.recorded"))
interaction = runtime.post(
"/ports/events/interaction",
json=event_body("hub.interaction.recorded"),
)
progress_projection = runtime.get("/ports/projections/progress_events").json()
interaction_projection = runtime.get("/ports/projections/interaction_events").json()
assert progress.status_code == 202
assert interaction.status_code == 202
assert [item["family"] for item in progress_projection["data"]["items"]] == ["progress"]
assert [item["family"] for item in interaction_projection["data"]["items"]] == [
"interaction"
]
def test_event_ports_reject_wrong_or_uncataloged_families() -> None:
runtime = client()
wrong_family = runtime.post(
"/ports/events/progress",
json=event_body("hub.interaction.recorded"),
)
unknown = runtime.post(
"/ports/events/interaction",
json=event_body("hub.interaction.unknown"),
)
assert wrong_family.status_code == 422
assert "belongs to 'interaction'" in wrong_family.json()["detail"]
assert unknown.status_code == 422
assert "is not cataloged" in unknown.json()["detail"]
def test_unknown_projection_is_not_found() -> None:
response = client().get("/ports/projections/not-a-projection")
assert response.status_code == 404
def test_runtime_openapi_marks_the_five_minimal_ports() -> None:
document = client().get("/openapi.json").json()
port_ids = {
operation["x-port-id"]
for path_item in document["paths"].values()
for method, operation in path_item.items()
if method in {"get", "post", "put", "patch", "delete"} and "x-port-id" in operation
}
assert port_ids == {
"port.registry",
"port.messaging",
"port.events.progress",
"port.events.interaction",
"port.projection.query",
}
def test_cli_exposes_api_mcp_and_migration_processes() -> None:
parser = build_parser(RuntimeSettings())
assert parser.parse_args(["api"]).command == "api"
assert parser.parse_args(["mcp"]).command == "mcp"
assert parser.parse_args(["migrate", "head", "--database-url", "postgresql://db"]).command == (
"migrate"
)
assert _sync_database_url("postgresql+asyncpg://db") == "postgresql+psycopg2://db"