feat: add hub runtime and extension contract
This commit is contained in:
parent
fce19f193f
commit
7e1ec03f0c
44 changed files with 3875 additions and 84 deletions
68
tests/test_conformance.py
Normal file
68
tests/test_conformance.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from hub_core.conformance import ConformanceHarness, find_secret_violations
|
||||
from hub_core.runtime.app import create_app
|
||||
from hub_core.runtime.cli import build_parser
|
||||
from hub_core.runtime.config import RuntimeSettings
|
||||
from hub_core.runtime.store import InMemoryPortStore
|
||||
|
||||
|
||||
def isolated_target() -> TestClient:
|
||||
settings = RuntimeSettings(environment="test", backend="memory", allow_ephemeral=True)
|
||||
return TestClient(create_app(settings=settings, port_store=InMemoryPortStore()))
|
||||
|
||||
|
||||
def test_implemented_tier_2_and_3_profile_passes_reference_runtime() -> None:
|
||||
report = ConformanceHarness(isolated_target()).run()
|
||||
|
||||
assert report.passed
|
||||
assert report.passed_count == 8
|
||||
assert {check.check_id for check in report.checks} == {
|
||||
"C1",
|
||||
"C3",
|
||||
"C4",
|
||||
"C5",
|
||||
"C6",
|
||||
"C8",
|
||||
"F2",
|
||||
"F3",
|
||||
}
|
||||
assert all(check.status == "pass" for check in report.checks)
|
||||
assert report.to_dict()["summary"] == {"passed": 8, "total": 8}
|
||||
|
||||
|
||||
def test_projection_rebuild_scenario_leaves_separate_provenance_bearing_views() -> None:
|
||||
target = isolated_target()
|
||||
assert ConformanceHarness(target).run().passed
|
||||
|
||||
progress = target.get("/ports/projections/progress_events").json()
|
||||
interaction = target.get("/ports/projections/interaction_events").json()
|
||||
|
||||
assert progress["data"]["rebuild_from"] == ["progress_events"]
|
||||
assert interaction["data"]["rebuild_from"] == ["interaction_events"]
|
||||
assert {item["family"] for item in progress["data"]["items"]} == {"progress"}
|
||||
assert {item["family"] for item in interaction["data"]["items"]} == {"interaction"}
|
||||
assert progress["provenance"]["content_hash"]
|
||||
assert interaction["provenance"]["content_hash"]
|
||||
|
||||
|
||||
def test_secret_heuristic_reports_paths_without_echoing_values() -> None:
|
||||
value = {
|
||||
"nested": {"api_token": "do-not-echo"},
|
||||
"database": "postgresql://runtime:do-not-echo@example.invalid/hub",
|
||||
"safe": "https://ops-hub.example.invalid/docs",
|
||||
}
|
||||
|
||||
assert find_secret_violations(value) == ["$.nested.api_token", "$.database"]
|
||||
|
||||
|
||||
def test_cli_exposes_remote_conformance_runner() -> None:
|
||||
args = build_parser(RuntimeSettings()).parse_args(
|
||||
["conformance", "--base-url", "http://runtime.invalid", "--json"]
|
||||
)
|
||||
|
||||
assert args.command == "conformance"
|
||||
assert args.base_url == "http://runtime.invalid"
|
||||
assert args.as_json is True
|
||||
152
tests/test_contracts.py
Normal file
152
tests/test_contracts.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
|
||||
from hub_core.contracts import CONTRACT_ID, CONTRACT_VERSION, extension_contract_root
|
||||
|
||||
|
||||
ROOT = extension_contract_root()
|
||||
SCHEMAS = ROOT.joinpath("schemas")
|
||||
FIXTURE = ROOT.joinpath("fixtures", "ops-hub.extension.json")
|
||||
REBUILD_FIXTURE = ROOT.joinpath("fixtures", "projection-rebuild.json")
|
||||
CATALOG = ROOT.joinpath("catalogs", "event-types.json")
|
||||
OPENAPI = ROOT.joinpath("openapi", "ports.openapi.json")
|
||||
COMPATIBILITY = ROOT.joinpath("compatibility-matrix.json")
|
||||
|
||||
PORT_IDS = {
|
||||
"port.registry",
|
||||
"port.addressing",
|
||||
"port.messaging",
|
||||
"port.events.progress",
|
||||
"port.events.interaction",
|
||||
"port.projection.query",
|
||||
"port.repo",
|
||||
"port.work",
|
||||
"port.policy",
|
||||
"port.telemetry",
|
||||
"port.schedule",
|
||||
}
|
||||
|
||||
SECRET_KEYS = {
|
||||
"api_key",
|
||||
"credential",
|
||||
"password",
|
||||
"passwd",
|
||||
"private_key",
|
||||
"secret",
|
||||
"token",
|
||||
}
|
||||
|
||||
|
||||
def load_json(resource: Any) -> Any:
|
||||
return json.loads(resource.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def validate(instance: Any, schema_name: str) -> None:
|
||||
schema = load_json(SCHEMAS.joinpath(schema_name))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
Draft202012Validator(schema, format_checker=FormatChecker()).validate(instance)
|
||||
|
||||
|
||||
def iter_keys(value: Any) -> set[str]:
|
||||
if isinstance(value, Mapping):
|
||||
keys = {str(key).lower() for key in value}
|
||||
for child in value.values():
|
||||
keys.update(iter_keys(child))
|
||||
return keys
|
||||
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||
keys: set[str] = set()
|
||||
for child in value:
|
||||
keys.update(iter_keys(child))
|
||||
return keys
|
||||
return set()
|
||||
|
||||
|
||||
def test_packaged_contract_identity_and_artifacts() -> None:
|
||||
assert CONTRACT_ID == "helixforge.hub-extension"
|
||||
assert CONTRACT_VERSION == "0.1.0"
|
||||
assert ROOT.joinpath("README.md").is_file()
|
||||
assert FIXTURE.is_file()
|
||||
assert REBUILD_FIXTURE.is_file()
|
||||
assert CATALOG.is_file()
|
||||
assert OPENAPI.is_file()
|
||||
assert COMPATIBILITY.is_file()
|
||||
|
||||
|
||||
def test_ops_hub_fixture_validates_against_descriptor_and_manifest_schemas() -> None:
|
||||
package = load_json(FIXTURE)
|
||||
|
||||
validate(package["descriptor"], "hub-descriptor.schema.json")
|
||||
validate(package["manifest"], "hub-manifest.schema.json")
|
||||
|
||||
assert package["descriptor"]["reuse_surface_id"] == package["manifest"]["reuse_surface_id"]
|
||||
|
||||
|
||||
def test_event_catalog_validates_and_keeps_event_families_distinct() -> None:
|
||||
catalog = load_json(CATALOG)
|
||||
validate(catalog, "event-type-catalog.schema.json")
|
||||
|
||||
event_types = catalog["event_types"]
|
||||
names = [entry["type"] for entry in event_types]
|
||||
families = {entry["family"] for entry in event_types}
|
||||
assert len(names) == len(set(names))
|
||||
assert {"progress", "interaction"} <= families
|
||||
|
||||
|
||||
def test_fixture_events_resolve_in_catalog() -> None:
|
||||
package = load_json(FIXTURE)
|
||||
catalog = load_json(CATALOG)
|
||||
known = {entry["type"] for entry in catalog["event_types"]}
|
||||
declared = set(package["manifest"]["events_emitted"])
|
||||
declared.update(package["manifest"]["events_consumed"])
|
||||
assert declared <= known
|
||||
|
||||
|
||||
def test_contract_examples_contain_no_secret_fields() -> None:
|
||||
assert not (iter_keys(load_json(FIXTURE)) & SECRET_KEYS)
|
||||
assert not (iter_keys(load_json(REBUILD_FIXTURE)) & SECRET_KEYS)
|
||||
assert not (iter_keys(load_json(CATALOG)) & SECRET_KEYS)
|
||||
|
||||
|
||||
def test_openapi_declares_every_named_port() -> None:
|
||||
document = load_json(OPENAPI)
|
||||
assert document["openapi"] == "3.1.0"
|
||||
assert document["info"]["version"] == CONTRACT_VERSION
|
||||
|
||||
operations = [
|
||||
operation
|
||||
for path_item in document["paths"].values()
|
||||
for method, operation in path_item.items()
|
||||
if method in {"get", "post", "put", "patch", "delete"}
|
||||
]
|
||||
assert {operation["x-port-id"] for operation in operations} == PORT_IDS
|
||||
assert all(operation["x-direction"] in {"in", "out"} for operation in operations)
|
||||
assert all(operation.get("operationId") for operation in operations)
|
||||
assert all(operation.get("responses") for operation in operations)
|
||||
|
||||
|
||||
def test_manifest_port_enum_matches_openapi_ports() -> None:
|
||||
manifest_schema = load_json(SCHEMAS.joinpath("hub-manifest.schema.json"))
|
||||
declared = set(manifest_schema["$defs"]["portId"]["enum"])
|
||||
openapi = load_json(OPENAPI)
|
||||
implemented = {
|
||||
operation["x-port-id"]
|
||||
for path_item in openapi["paths"].values()
|
||||
for method, operation in path_item.items()
|
||||
if method in {"get", "post", "put", "patch", "delete"}
|
||||
}
|
||||
assert declared == implemented == PORT_IDS
|
||||
|
||||
|
||||
def test_compatibility_matrix_covers_current_and_core_hub_adapter() -> None:
|
||||
matrix = load_json(COMPATIBILITY)
|
||||
assert matrix["contract_id"] == CONTRACT_ID
|
||||
assert matrix["current_version"] == CONTRACT_VERSION
|
||||
assert any(entry["version"] == CONTRACT_VERSION for entry in matrix["versions"])
|
||||
assert any(
|
||||
adapter["source"] == "core-hub.hub-manifest"
|
||||
and adapter["target_version"] == CONTRACT_VERSION
|
||||
for adapter in matrix["migration_adapters"]
|
||||
)
|
||||
195
tests/test_runtime.py
Normal file
195
tests/test_runtime.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
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_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_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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue